feat(configmanager): add DiffService with tests

This commit is contained in:
Joseph Doherty
2026-01-19 17:44:28 -05:00
parent c8f3c0060d
commit 68da728cdf
3 changed files with 276 additions and 0 deletions
@@ -0,0 +1,65 @@
using DiffPlex;
using DiffPlex.DiffBuilder;
using DiffPlex.DiffBuilder.Model;
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Service for generating diffs between text content.
/// </summary>
public class DiffService : IDiffService
{
public DiffResult GenerateDiff(string original, string modified)
{
var diffBuilder = new InlineDiffBuilder(new Differ());
var diff = diffBuilder.BuildDiffModel(original, modified);
var lines = new List<DiffLine>();
int oldLineNum = 1;
int newLineNum = 1;
int insertions = 0;
int deletions = 0;
foreach (var line in diff.Lines)
{
var diffLine = new DiffLine
{
Text = line.Text,
Type = line.Type switch
{
ChangeType.Inserted => DiffLineType.Added,
ChangeType.Deleted => DiffLineType.Removed,
_ => DiffLineType.Unchanged
},
OldLineNumber = line.Type == ChangeType.Inserted ? null : oldLineNum,
NewLineNumber = line.Type == ChangeType.Deleted ? null : newLineNum
};
lines.Add(diffLine);
switch (line.Type)
{
case ChangeType.Inserted:
newLineNum++;
insertions++;
break;
case ChangeType.Deleted:
oldLineNum++;
deletions++;
break;
default:
oldLineNum++;
newLineNum++;
break;
}
}
return new DiffResult
{
HasChanges = insertions > 0 || deletions > 0,
Lines = lines,
Insertions = insertions,
Deletions = deletions
};
}
}
@@ -0,0 +1,38 @@
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Represents a line in a diff output.
/// </summary>
public class DiffLine
{
public required int? OldLineNumber { get; init; }
public required int? NewLineNumber { get; init; }
public required string Text { get; init; }
public required DiffLineType Type { get; init; }
}
public enum DiffLineType
{
Unchanged,
Added,
Removed
}
/// <summary>
/// Result of a diff operation.
/// </summary>
public class DiffResult
{
public bool HasChanges { get; init; }
public List<DiffLine> Lines { get; init; } = [];
public int Insertions { get; init; }
public int Deletions { get; init; }
}
/// <summary>
/// Service for generating diffs between text content.
/// </summary>
public interface IDiffService
{
DiffResult GenerateDiff(string original, string modified);
}
@@ -0,0 +1,173 @@
using JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Tests.Services;
public class DiffServiceTests
{
private readonly DiffService _sut;
public DiffServiceTests()
{
_sut = new DiffService();
}
[Fact]
public void GenerateDiff_WithNoChanges_ReturnsEmptyDiff()
{
// Arrange
var original = "line1\nline2\nline3";
var modified = "line1\nline2\nline3";
// Act
var result = _sut.GenerateDiff(original, modified);
// Assert
result.HasChanges.ShouldBeFalse();
}
[Fact]
public void GenerateDiff_WithChanges_ReturnsDiffLines()
{
// Arrange
var original = "line1\nline2\nline3";
var modified = "line1\nmodified\nline3";
// Act
var result = _sut.GenerateDiff(original, modified);
// Assert
result.HasChanges.ShouldBeTrue();
result.Lines.ShouldNotBeEmpty();
}
[Fact]
public void GenerateDiff_WithAddedLine_ReportsInsertion()
{
// Arrange
var original = "line1\nline2";
var modified = "line1\nline2\nline3";
// Act
var result = _sut.GenerateDiff(original, modified);
// Assert
result.HasChanges.ShouldBeTrue();
result.Insertions.ShouldBe(1);
result.Deletions.ShouldBe(0);
result.Lines.ShouldContain(l => l.Type == DiffLineType.Added && l.Text == "line3");
}
[Fact]
public void GenerateDiff_WithRemovedLine_ReportsDeletion()
{
// Arrange
var original = "line1\nline2\nline3";
var modified = "line1\nline3";
// Act
var result = _sut.GenerateDiff(original, modified);
// Assert
result.HasChanges.ShouldBeTrue();
result.Deletions.ShouldBe(1);
result.Lines.ShouldContain(l => l.Type == DiffLineType.Removed && l.Text == "line2");
}
[Fact]
public void GenerateDiff_WithModifiedLine_ReportsAdditionAndDeletion()
{
// Arrange
var original = "line1\noriginal\nline3";
var modified = "line1\nchanged\nline3";
// Act
var result = _sut.GenerateDiff(original, modified);
// Assert
result.HasChanges.ShouldBeTrue();
result.Insertions.ShouldBeGreaterThan(0);
result.Deletions.ShouldBeGreaterThan(0);
}
[Fact]
public void GenerateDiff_WithEmptyOriginal_ReportsAllAsInsertions()
{
// Arrange
var original = "";
var modified = "line1\nline2";
// Act
var result = _sut.GenerateDiff(original, modified);
// Assert
result.HasChanges.ShouldBeTrue();
result.Insertions.ShouldBeGreaterThan(0);
result.Deletions.ShouldBe(0);
}
[Fact]
public void GenerateDiff_WithEmptyModified_ReportsAllAsDeletions()
{
// Arrange
var original = "line1\nline2";
var modified = "";
// Act
var result = _sut.GenerateDiff(original, modified);
// Assert
result.HasChanges.ShouldBeTrue();
result.Insertions.ShouldBe(0);
result.Deletions.ShouldBeGreaterThan(0);
}
[Fact]
public void GenerateDiff_LineNumbers_AreCorrectForUnchangedLines()
{
// Arrange
var original = "line1\nline2\nline3";
var modified = "line1\nline2\nline3";
// Act
var result = _sut.GenerateDiff(original, modified);
// Assert
var firstLine = result.Lines.First();
firstLine.OldLineNumber.ShouldBe(1);
firstLine.NewLineNumber.ShouldBe(1);
}
[Fact]
public void GenerateDiff_AddedLine_HasNullOldLineNumber()
{
// Arrange
var original = "line1";
var modified = "line1\nline2";
// Act
var result = _sut.GenerateDiff(original, modified);
// Assert
var addedLine = result.Lines.FirstOrDefault(l => l.Type == DiffLineType.Added);
addedLine.ShouldNotBeNull();
addedLine.OldLineNumber.ShouldBeNull();
addedLine.NewLineNumber.ShouldNotBeNull();
}
[Fact]
public void GenerateDiff_RemovedLine_HasNullNewLineNumber()
{
// Arrange
var original = "line1\nline2";
var modified = "line1";
// Act
var result = _sut.GenerateDiff(original, modified);
// Assert
var removedLine = result.Lines.FirstOrDefault(l => l.Type == DiffLineType.Removed);
removedLine.ShouldNotBeNull();
removedLine.OldLineNumber.ShouldNotBeNull();
removedLine.NewLineNumber.ShouldBeNull();
}
}