513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
233 lines
9.3 KiB
C#
233 lines
9.3 KiB
C#
using System.Diagnostics;
|
|
|
|
using ZB.MOM.WW.ScadaBridge.Transport.Import;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Import;
|
|
|
|
public sealed class LineDifferTests
|
|
{
|
|
/// <summary>Verifies that identical text produces no additions or removals and is not truncated.</summary>
|
|
[Fact]
|
|
public void IdenticalText_NoAddsOrRemoves_NotTruncated()
|
|
{
|
|
var result = LineDiffer.Diff("a\nb\nc", "a\nb\nc");
|
|
|
|
Assert.Equal(0, result.AddedCount);
|
|
Assert.Equal(0, result.RemovedCount);
|
|
Assert.False(result.Truncated);
|
|
// All emitted lines (if any) must be Context.
|
|
Assert.All(result.Lines, l => Assert.Equal(LineDiffOp.Context, l.Op));
|
|
}
|
|
|
|
/// <summary>Verifies that diffing an empty old text against three new lines yields three additions numbered 1 to 3.</summary>
|
|
[Fact]
|
|
public void OldEmpty_NewThreeLines_ThreeAdds_NewLineNos1To3()
|
|
{
|
|
var result = LineDiffer.Diff("", "x\ny\nz");
|
|
|
|
var adds = result.Lines.Where(l => l.Op == LineDiffOp.Add).ToList();
|
|
Assert.Equal(3, result.AddedCount);
|
|
Assert.Equal(0, result.RemovedCount);
|
|
Assert.False(result.Truncated);
|
|
Assert.Equal(3, adds.Count);
|
|
Assert.Equal(new[] { 1, 2, 3 }, adds.Select(a => a.NewLineNo!.Value).ToArray());
|
|
Assert.Equal(new[] { "x", "y", "z" }, adds.Select(a => a.Text).ToArray());
|
|
// Add lines have no old line number.
|
|
Assert.All(adds, a => Assert.Null(a.OldLineNo));
|
|
}
|
|
|
|
/// <summary>Verifies that diffing two old lines against an empty new text yields two removals numbered 1 to 2.</summary>
|
|
[Fact]
|
|
public void NewEmpty_OldTwoLines_TwoRemoves_OldLineNos1To2()
|
|
{
|
|
var result = LineDiffer.Diff("p\nq", "");
|
|
|
|
var removes = result.Lines.Where(l => l.Op == LineDiffOp.Remove).ToList();
|
|
Assert.Equal(0, result.AddedCount);
|
|
Assert.Equal(2, result.RemovedCount);
|
|
Assert.False(result.Truncated);
|
|
Assert.Equal(2, removes.Count);
|
|
Assert.Equal(new[] { 1, 2 }, removes.Select(r => r.OldLineNo!.Value).ToArray());
|
|
Assert.Equal(new[] { "p", "q" }, removes.Select(r => r.Text).ToArray());
|
|
// Remove lines have no new line number.
|
|
Assert.All(removes, r => Assert.Null(r.NewLineNo));
|
|
}
|
|
|
|
/// <summary>Verifies that changing one middle line yields one removal and one addition with the surrounding context preserved.</summary>
|
|
[Fact]
|
|
public void SingleMiddleLineChange_OneRemoveOneAdd_ContextPreserved()
|
|
{
|
|
var result = LineDiffer.Diff("a\nB\nc", "a\nX\nc");
|
|
|
|
var removes = result.Lines.Where(l => l.Op == LineDiffOp.Remove).ToList();
|
|
var adds = result.Lines.Where(l => l.Op == LineDiffOp.Add).ToList();
|
|
var contexts = result.Lines.Where(l => l.Op == LineDiffOp.Context).ToList();
|
|
|
|
Assert.Equal(1, result.RemovedCount);
|
|
Assert.Equal(1, result.AddedCount);
|
|
Assert.False(result.Truncated);
|
|
|
|
var remove = Assert.Single(removes);
|
|
Assert.Equal("B", remove.Text);
|
|
Assert.Equal(2, remove.OldLineNo);
|
|
Assert.Null(remove.NewLineNo);
|
|
|
|
var add = Assert.Single(adds);
|
|
Assert.Equal("X", add.Text);
|
|
Assert.Equal(2, add.NewLineNo);
|
|
Assert.Null(add.OldLineNo);
|
|
|
|
// Context lines "a" and "c" carry both line numbers.
|
|
Assert.Equal(2, contexts.Count);
|
|
var ctxA = contexts.Single(c => c.Text == "a");
|
|
Assert.Equal(1, ctxA.OldLineNo);
|
|
Assert.Equal(1, ctxA.NewLineNo);
|
|
var ctxC = contexts.Single(c => c.Text == "c");
|
|
Assert.Equal(3, ctxC.OldLineNo);
|
|
Assert.Equal(3, ctxC.NewLineNo);
|
|
}
|
|
|
|
/// <summary>Verifies that differing CRLF and LF line endings are normalised and produce no changes.</summary>
|
|
[Fact]
|
|
public void CrlfNormalization_NoChanges()
|
|
{
|
|
var result = LineDiffer.Diff("a\r\nb", "a\nb");
|
|
|
|
Assert.Equal(0, result.AddedCount);
|
|
Assert.Equal(0, result.RemovedCount);
|
|
Assert.False(result.Truncated);
|
|
}
|
|
|
|
/// <summary>Verifies that lone carriage returns are normalised and produce no changes.</summary>
|
|
[Fact]
|
|
public void LoneCarriageReturnNormalization_NoChanges()
|
|
{
|
|
var result = LineDiffer.Diff("a\rb", "a\nb");
|
|
|
|
Assert.Equal(0, result.AddedCount);
|
|
Assert.Equal(0, result.RemovedCount);
|
|
Assert.False(result.Truncated);
|
|
}
|
|
|
|
/// <summary>Verifies that a null old text and a single new line yield one addition.</summary>
|
|
[Fact]
|
|
public void NullOld_NewSingleLine_OneAdd()
|
|
{
|
|
var result = LineDiffer.Diff(null, "x");
|
|
|
|
Assert.Equal(1, result.AddedCount);
|
|
Assert.Equal(0, result.RemovedCount);
|
|
var add = Assert.Single(result.Lines, l => l.Op == LineDiffOp.Add);
|
|
Assert.Equal("x", add.Text);
|
|
Assert.Equal(1, add.NewLineNo);
|
|
}
|
|
|
|
/// <summary>Verifies that a null new text and a single old line yield one removal.</summary>
|
|
[Fact]
|
|
public void NullNew_OldSingleLine_OneRemove()
|
|
{
|
|
var result = LineDiffer.Diff("x", null);
|
|
|
|
Assert.Equal(0, result.AddedCount);
|
|
Assert.Equal(1, result.RemovedCount);
|
|
var remove = Assert.Single(result.Lines, l => l.Op == LineDiffOp.Remove);
|
|
Assert.Equal("x", remove.Text);
|
|
Assert.Equal(1, remove.OldLineNo);
|
|
}
|
|
|
|
/// <summary>Verifies that two null texts produce no changes.</summary>
|
|
[Fact]
|
|
public void BothNull_NoChanges()
|
|
{
|
|
var result = LineDiffer.Diff(null, null);
|
|
|
|
Assert.Equal(0, result.AddedCount);
|
|
Assert.Equal(0, result.RemovedCount);
|
|
Assert.False(result.Truncated);
|
|
}
|
|
|
|
/// <summary>Verifies that a thousand new lines against an empty old text are truncated to the maximum line count.</summary>
|
|
[Fact]
|
|
public void SizeCap_OldEmpty_ThousandNewLines_TruncatedToMax()
|
|
{
|
|
var newText = string.Join("\n", Enumerable.Range(1, 1000).Select(i => $"line{i}"));
|
|
|
|
var result = LineDiffer.Diff("", newText, maxLines: 400);
|
|
|
|
Assert.True(result.Lines.Count <= 400);
|
|
Assert.True(result.Truncated);
|
|
Assert.Equal(1000, result.AddedCount);
|
|
Assert.Equal(0, result.RemovedCount);
|
|
}
|
|
|
|
/// <summary>Verifies that an interleaved change preserves the longest-common-subsequence context.</summary>
|
|
[Fact]
|
|
public void InterleavedCase_LcsContextPreserved()
|
|
{
|
|
// old: 1 2 3 4 new: 1 3 4 5 => remove 2, context 1/3/4, add 5
|
|
var result = LineDiffer.Diff("1\n2\n3\n4", "1\n3\n4\n5");
|
|
|
|
Assert.Equal(1, result.AddedCount);
|
|
Assert.Equal(1, result.RemovedCount);
|
|
Assert.False(result.Truncated);
|
|
|
|
var remove = Assert.Single(result.Lines, l => l.Op == LineDiffOp.Remove);
|
|
Assert.Equal("2", remove.Text);
|
|
Assert.Equal(2, remove.OldLineNo);
|
|
|
|
var add = Assert.Single(result.Lines, l => l.Op == LineDiffOp.Add);
|
|
Assert.Equal("5", add.Text);
|
|
Assert.Equal(4, add.NewLineNo);
|
|
|
|
var contextTexts = result.Lines
|
|
.Where(l => l.Op == LineDiffOp.Context)
|
|
.Select(l => l.Text)
|
|
.ToList();
|
|
Assert.Equal(new[] { "1", "3", "4" }, contextTexts);
|
|
|
|
// Verify ordering: the remove of "2" appears after context "1" and before context "3".
|
|
var ops = result.Lines.Select(l => (l.Op, l.Text)).ToList();
|
|
var idxCtx1 = ops.FindIndex(x => x is { Op: LineDiffOp.Context, Text: "1" });
|
|
var idxRem2 = ops.FindIndex(x => x is { Op: LineDiffOp.Remove, Text: "2" });
|
|
var idxCtx3 = ops.FindIndex(x => x is { Op: LineDiffOp.Context, Text: "3" });
|
|
Assert.True(idxCtx1 < idxRem2 && idxRem2 < idxCtx3);
|
|
}
|
|
|
|
/// <summary>Verifies that a trailing newline produces a trailing empty line.</summary>
|
|
[Fact]
|
|
public void TrailingNewline_ProducesTrailingEmptyLine()
|
|
{
|
|
// "a\n" splits to ["a", ""] — both texts identical => no changes.
|
|
var result = LineDiffer.Diff("a\n", "a\n");
|
|
|
|
Assert.Equal(0, result.AddedCount);
|
|
Assert.Equal(0, result.RemovedCount);
|
|
}
|
|
|
|
/// <summary>Verifies that oversized inputs with no common lines degrade to a summary without running the Myers algorithm.</summary>
|
|
[Fact]
|
|
public void OversizedInputs_ZeroCommonLines_DegradesToSummary_WithoutRunningMyers()
|
|
{
|
|
// Two 10,000-line inputs with zero common lines. Running the Myers trace here would
|
|
// record ~20,000 V-snapshots of ~160 KB each (multi-GB transient allocation), so the
|
|
// input-size guard must short-circuit to a summary-only result long before that.
|
|
var oldText = string.Join("\n", Enumerable.Range(1, 10_000).Select(i => $"old-line-{i}"));
|
|
var newText = string.Join("\n", Enumerable.Range(1, 10_000).Select(i => $"new-line-{i}"));
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
var result = LineDiffer.Diff(oldText, newText);
|
|
sw.Stop();
|
|
|
|
// Summary-only shape: same as the output cap — Truncated, full totals, no hunks.
|
|
Assert.True(result.Truncated);
|
|
Assert.Empty(result.Lines);
|
|
Assert.Equal(10_000, result.AddedCount);
|
|
Assert.Equal(10_000, result.RemovedCount);
|
|
|
|
// Must return within a small time budget; pathological slowness is a failure.
|
|
Assert.True(
|
|
sw.Elapsed < TimeSpan.FromSeconds(2),
|
|
$"Diff of oversized inputs took {sw.Elapsed.TotalSeconds:F1}s — the input-size guard did not short-circuit.");
|
|
}
|
|
}
|