Implement diff-reporter PoC (#5)
This commit is contained in:
55
src/Recordingtest.DiffReporter/BinaryDiffer.cs
Normal file
55
src/Recordingtest.DiffReporter/BinaryDiffer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
11
src/Recordingtest.DiffReporter/DiffModels.cs
Normal file
11
src/Recordingtest.DiffReporter/DiffModels.cs
Normal 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);
|
||||
86
src/Recordingtest.DiffReporter/Differ.cs
Normal file
86
src/Recordingtest.DiffReporter/Differ.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/Recordingtest.DiffReporter/JsonDiffer.cs
Normal file
77
src/Recordingtest.DiffReporter/JsonDiffer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/Recordingtest.DiffReporter/LineDiffer.cs
Normal file
50
src/Recordingtest.DiffReporter/LineDiffer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<AssemblyName>Recordingtest.DiffReporter</AssemblyName>
|
||||
<RootNamespace>Recordingtest.DiffReporter</RootNamespace>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user