using ZB.MOM.WW.ScadaBridge.Transport.Import; namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Import; public sealed class LineDifferTests { [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)); } [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)); } [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)); } [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); } [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); } [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); } [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); } [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); } [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); } [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); } [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); } [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); } }