Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Canonicalization.Json;
|
||||
|
||||
namespace StellaOps.Canonicalization.Verification;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that serialization produces identical output across runs.
|
||||
/// </summary>
|
||||
public sealed class DeterminismVerifier
|
||||
{
|
||||
public DeterminismResult Verify<T>(T value, int iterations = 10)
|
||||
{
|
||||
var outputs = new HashSet<string>(StringComparer.Ordinal);
|
||||
var digests = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
var (json, digest) = CanonicalJsonSerializer.SerializeWithDigest(value);
|
||||
outputs.Add(json);
|
||||
digests.Add(digest);
|
||||
}
|
||||
|
||||
return new DeterminismResult(
|
||||
IsDeterministic: outputs.Count == 1 && digests.Count == 1,
|
||||
UniqueOutputs: outputs.Count,
|
||||
UniqueDigests: digests.Count,
|
||||
SampleOutput: outputs.FirstOrDefault() ?? string.Empty,
|
||||
SampleDigest: digests.FirstOrDefault() ?? string.Empty);
|
||||
}
|
||||
|
||||
public ComparisonResult Compare(string jsonA, string jsonB)
|
||||
{
|
||||
if (string.Equals(jsonA, jsonB, StringComparison.Ordinal))
|
||||
{
|
||||
return new ComparisonResult(true, []);
|
||||
}
|
||||
|
||||
var differences = FindDifferences(jsonA, jsonB);
|
||||
return new ComparisonResult(false, differences);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> FindDifferences(string a, string b)
|
||||
{
|
||||
var differences = new List<string>();
|
||||
using var docA = JsonDocument.Parse(a);
|
||||
using var docB = JsonDocument.Parse(b);
|
||||
CompareElements(docA.RootElement, docB.RootElement, "$", differences);
|
||||
return differences;
|
||||
}
|
||||
|
||||
private static void CompareElements(JsonElement a, JsonElement b, string path, List<string> differences)
|
||||
{
|
||||
if (a.ValueKind != b.ValueKind)
|
||||
{
|
||||
differences.Add($"{path}: type mismatch ({a.ValueKind} vs {b.ValueKind})");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (a.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
var propsA = a.EnumerateObject().ToDictionary(p => p.Name, StringComparer.Ordinal);
|
||||
var propsB = b.EnumerateObject().ToDictionary(p => p.Name, StringComparer.Ordinal);
|
||||
foreach (var key in propsA.Keys.Union(propsB.Keys).OrderBy(k => k, StringComparer.Ordinal))
|
||||
{
|
||||
var hasA = propsA.TryGetValue(key, out var propA);
|
||||
var hasB = propsB.TryGetValue(key, out var propB);
|
||||
if (!hasA) differences.Add($"{path}.{key}: missing in first");
|
||||
else if (!hasB) differences.Add($"{path}.{key}: missing in second");
|
||||
else CompareElements(propA.Value, propB.Value, $"{path}.{key}", differences);
|
||||
}
|
||||
break;
|
||||
case JsonValueKind.Array:
|
||||
var arrA = a.EnumerateArray().ToList();
|
||||
var arrB = b.EnumerateArray().ToList();
|
||||
if (arrA.Count != arrB.Count)
|
||||
differences.Add($"{path}: array length mismatch ({arrA.Count} vs {arrB.Count})");
|
||||
for (var i = 0; i < Math.Min(arrA.Count, arrB.Count); i++)
|
||||
CompareElements(arrA[i], arrB[i], $"{path}[{i}]", differences);
|
||||
break;
|
||||
default:
|
||||
if (a.GetRawText() != b.GetRawText())
|
||||
differences.Add($"{path}: value mismatch");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record DeterminismResult(
|
||||
bool IsDeterministic,
|
||||
int UniqueOutputs,
|
||||
int UniqueDigests,
|
||||
string SampleOutput,
|
||||
string SampleDigest);
|
||||
|
||||
public sealed record ComparisonResult(
|
||||
bool IsIdentical,
|
||||
IReadOnlyList<string> Differences);
|
||||
Reference in New Issue
Block a user