Implement diff-reporter PoC (#5)

This commit is contained in:
minsung
2026-04-07 14:12:15 +09:00
parent 3c5294a4cb
commit 7920de15b3
11 changed files with 536 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
namespace Recordingtest.DiffReporter;
internal static class BinaryDiffer
{
public static IReadOnlyList<Hunk> Diff(byte[] a, byte[] b)
{
var hunks = new List<Hunk>();
int max = Math.Max(a.Length, b.Length);
int i = 0;
while (i < max)
{
byte? av = i < a.Length ? a[i] : null;
byte? bv = i < b.Length ? b[i] : null;
if (av != bv)
{
int start = i;
while (i < max)
{
byte? av2 = i < a.Length ? a[i] : null;
byte? bv2 = i < b.Length ? b[i] : null;
if (av2 == bv2) break;
i++;
}
int len = i - start;
var beforeSlice = Slice(a, start, len);
var afterSlice = Slice(b, start, len);
string kind;
if (start >= a.Length) kind = "added";
else if (start >= b.Length) kind = "removed";
else kind = "changed";
hunks.Add(new Hunk(start, kind, ToHex(beforeSlice), ToHex(afterSlice)));
}
else
{
i++;
}
}
return hunks;
}
private static byte[] Slice(byte[] src, int start, int len)
{
if (start >= src.Length) return Array.Empty<byte>();
int actual = Math.Min(len, src.Length - start);
var r = new byte[actual];
Array.Copy(src, start, r, 0, actual);
return r;
}
private static string ToHex(byte[] data)
{
if (data.Length == 0) return string.Empty;
return Convert.ToHexString(data);
}
}

View File

@@ -0,0 +1,11 @@
namespace Recordingtest.DiffReporter;
public sealed record DiffSummary(int Added, int Removed, int Changed);
public sealed record Hunk(int LineOrOffset, string Kind, string Before, string After);
public sealed record DiffResult(
string File,
bool Identical,
IReadOnlyList<Hunk> Hunks,
DiffSummary Summary);

View File

@@ -0,0 +1,86 @@
using System.Text;
using System.Text.Json;
namespace Recordingtest.DiffReporter;
public static class Differ
{
public static DiffResult Compare(string approvedPath, string receivedPath)
{
var approved = File.ReadAllBytes(approvedPath);
var received = File.ReadAllBytes(receivedPath);
var fileName = Path.GetFileName(receivedPath);
if (approved.SequenceEqual(received))
{
return new DiffResult(fileName, true, Array.Empty<Hunk>(), new DiffSummary(0, 0, 0));
}
// Try JSON
if (TryParseJson(approved, out var aDoc) && TryParseJson(received, out var bDoc))
{
using (aDoc)
using (bDoc)
{
var hunks = JsonDiffer.Diff(aDoc!.RootElement, bDoc!.RootElement);
return Build(fileName, hunks);
}
}
// Try text
if (TryDecodeText(approved, out var aText) && TryDecodeText(received, out var bText)
&& (aText!.Contains('\n') || bText!.Contains('\n')))
{
var hunks = LineDiffer.Diff(aText!, bText!);
return Build(fileName, hunks);
}
// Binary
var binHunks = BinaryDiffer.Diff(approved, received);
return Build(fileName, binHunks);
}
private static DiffResult Build(string file, IReadOnlyList<Hunk> hunks)
{
int added = 0, removed = 0, changed = 0;
foreach (var h in hunks)
{
switch (h.Kind)
{
case "added": added++; break;
case "removed": removed++; break;
case "changed": changed++; break;
}
}
return new DiffResult(file, hunks.Count == 0, hunks, new DiffSummary(added, removed, changed));
}
private static bool TryParseJson(byte[] data, out JsonDocument? doc)
{
try
{
doc = JsonDocument.Parse(data);
return true;
}
catch
{
doc = null;
return false;
}
}
private static bool TryDecodeText(byte[] data, out string? text)
{
try
{
var enc = new UTF8Encoding(false, true);
text = enc.GetString(data);
return true;
}
catch
{
text = null;
return false;
}
}
}

View File

@@ -0,0 +1,77 @@
using System.Text.Json;
namespace Recordingtest.DiffReporter;
internal static class JsonDiffer
{
public static IReadOnlyList<Hunk> Diff(JsonElement a, JsonElement b)
{
var aMap = new SortedDictionary<string, string>(StringComparer.Ordinal);
var bMap = new SortedDictionary<string, string>(StringComparer.Ordinal);
Flatten("$", a, aMap);
Flatten("$", b, bMap);
var hunks = new List<Hunk>();
var allKeys = new SortedSet<string>(aMap.Keys, StringComparer.Ordinal);
foreach (var k in bMap.Keys) allKeys.Add(k);
int idx = 0;
foreach (var key in allKeys)
{
var hasA = aMap.TryGetValue(key, out var av);
var hasB = bMap.TryGetValue(key, out var bv);
if (hasA && hasB)
{
if (!string.Equals(av, bv, StringComparison.Ordinal))
hunks.Add(new Hunk(idx, "changed", $"{key}={av}", $"{key}={bv}"));
}
else if (hasA)
{
hunks.Add(new Hunk(idx, "removed", $"{key}={av}", string.Empty));
}
else if (hasB)
{
hunks.Add(new Hunk(idx, "added", string.Empty, $"{key}={bv}"));
}
idx++;
}
return hunks;
}
private static void Flatten(string path, JsonElement el, IDictionary<string, string> map)
{
switch (el.ValueKind)
{
case JsonValueKind.Object:
foreach (var p in el.EnumerateObject())
Flatten(path + "." + p.Name, p.Value, map);
break;
case JsonValueKind.Array:
int i = 0;
foreach (var item in el.EnumerateArray())
{
Flatten(path + "[" + i + "]", item, map);
i++;
}
break;
case JsonValueKind.String:
map[path] = "\"" + el.GetString() + "\"";
break;
case JsonValueKind.Number:
map[path] = el.GetRawText();
break;
case JsonValueKind.True:
map[path] = "true";
break;
case JsonValueKind.False:
map[path] = "false";
break;
case JsonValueKind.Null:
map[path] = "null";
break;
default:
map[path] = el.GetRawText();
break;
}
}
}

View File

@@ -0,0 +1,50 @@
namespace Recordingtest.DiffReporter;
internal static class LineDiffer
{
public static IReadOnlyList<Hunk> Diff(string a, string b)
{
var aLines = a.Replace("\r\n", "\n").Split('\n');
var bLines = b.Replace("\r\n", "\n").Split('\n');
// LCS table
int n = aLines.Length, m = bLines.Length;
var lcs = new int[n + 1, m + 1];
for (int i = n - 1; i >= 0; i--)
for (int j = m - 1; j >= 0; j--)
lcs[i, j] = aLines[i] == bLines[j]
? lcs[i + 1, j + 1] + 1
: Math.Max(lcs[i + 1, j], lcs[i, j + 1]);
var hunks = new List<Hunk>();
int x = 0, y = 0;
while (x < n && y < m)
{
if (aLines[x] == bLines[y])
{
x++; y++;
}
else if (lcs[x + 1, y] >= lcs[x, y + 1])
{
hunks.Add(new Hunk(x + 1, "removed", aLines[x], string.Empty));
x++;
}
else
{
hunks.Add(new Hunk(y + 1, "added", string.Empty, bLines[y]));
y++;
}
}
while (x < n)
{
hunks.Add(new Hunk(x + 1, "removed", aLines[x], string.Empty));
x++;
}
while (y < m)
{
hunks.Add(new Hunk(y + 1, "added", string.Empty, bLines[y]));
y++;
}
return hunks;
}
}

View File

@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>Recordingtest.DiffReporter</AssemblyName>
<RootNamespace>Recordingtest.DiffReporter</RootNamespace>
</PropertyGroup>
</Project>