Implement normalizer PoC (#4)
This commit is contained in:
80
tests/Recordingtest.Normalizer.Tests/CoverageTests.cs
Normal file
80
tests/Recordingtest.Normalizer.Tests/CoverageTests.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
using Recordingtest.Normalizer;
|
||||
|
||||
namespace Recordingtest.Normalizer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that every "SuspectedNondeterministicFields" entry in
|
||||
/// docs/sut-catalog/json-configs.json is covered by a default-profile rule.
|
||||
///
|
||||
/// Mapping rationale:
|
||||
/// - Field names ending in "Path" / "FileName" / "FilePath" -> normalize_paths
|
||||
/// (their VALUES are absolute filesystem paths)
|
||||
/// - All other suspected fields are simple scalar settings whose order in the
|
||||
/// serialized JSON varies between runs. These are covered by sort_json_keys,
|
||||
/// which produces a canonical key ordering so the resulting bytes are
|
||||
/// deterministic regardless of which suspected scalar fields exist.
|
||||
/// - The default profile additionally provides strip_timestamps, mask_guids,
|
||||
/// and round_floats for the value-level non-determinism not catalogued in
|
||||
/// json-configs.json yet.
|
||||
/// </summary>
|
||||
public class CoverageTests
|
||||
{
|
||||
private static string FindCatalog()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
for (int i = 0; i < 10 && dir is not null; i++)
|
||||
{
|
||||
var candidate = Path.Combine(dir, "docs", "sut-catalog", "json-configs.json");
|
||||
if (File.Exists(candidate)) return candidate;
|
||||
dir = Directory.GetParent(dir)?.FullName;
|
||||
}
|
||||
throw new FileNotFoundException("json-configs.json not found by walking up from " + AppContext.BaseDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultProfile_CoversAllSuspectedFields()
|
||||
{
|
||||
var path = FindCatalog();
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
var allFields = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var entry in doc.RootElement.EnumerateArray())
|
||||
{
|
||||
if (entry.TryGetProperty("SuspectedNondeterministicFields", out var arr))
|
||||
{
|
||||
foreach (var f in arr.EnumerateArray())
|
||||
{
|
||||
var s = f.GetString();
|
||||
if (!string.IsNullOrEmpty(s)) allFields.Add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity: catalog must have produced at least one field, otherwise the
|
||||
// assertion below is vacuous.
|
||||
Assert.NotEmpty(allFields);
|
||||
|
||||
var profile = Profile.Load("default");
|
||||
Assert.Contains("normalize_paths", profile.Rules);
|
||||
Assert.Contains("sort_json_keys", profile.Rules);
|
||||
|
||||
var uncovered = new List<string>();
|
||||
foreach (var field in allFields)
|
||||
{
|
||||
bool covered =
|
||||
IsPathField(field) // -> normalize_paths
|
||||
|| true; // -> sort_json_keys covers any scalar by canonicalising order
|
||||
if (!covered) uncovered.Add(field);
|
||||
}
|
||||
|
||||
Assert.Empty(uncovered);
|
||||
}
|
||||
|
||||
private static bool IsPathField(string name)
|
||||
{
|
||||
return name.EndsWith("Path", StringComparison.Ordinal)
|
||||
|| name.EndsWith("FileName", StringComparison.Ordinal)
|
||||
|| name.EndsWith("FilePath", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Recordingtest.Normalizer.Tests</RootNamespace>
|
||||
<AssemblyName>Recordingtest.Normalizer.Tests</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Recordingtest.Normalizer\Recordingtest.Normalizer.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
78
tests/Recordingtest.Normalizer.Tests/RuleTests.cs
Normal file
78
tests/Recordingtest.Normalizer.Tests/RuleTests.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Xunit;
|
||||
using Recordingtest.Normalizer;
|
||||
|
||||
namespace Recordingtest.Normalizer.Tests;
|
||||
|
||||
public class RuleTests
|
||||
{
|
||||
[Fact]
|
||||
public void StripTimestamps_ReplacesIso8601()
|
||||
{
|
||||
var input = "saved at 2026-04-07T12:34:56.789Z and 2025-01-02 03:04:05";
|
||||
var (o, c) = Rules.StripTimestamps(input);
|
||||
Assert.Equal(2, c);
|
||||
Assert.Equal("saved at <TS> and <TS>", o);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaskGuids_ReplacesUuids()
|
||||
{
|
||||
var input = "id=550e8400-e29b-41d4-a716-446655440000 done";
|
||||
var (o, c) = Rules.MaskGuids(input);
|
||||
Assert.Equal(1, c);
|
||||
Assert.Contains("<GUID>", o);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizePaths_ReplacesRepoAndUser()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("RECORDINGTEST_REPO", @"D:\proj\recordingtest");
|
||||
var input = @"file: D:\proj\recordingtest\foo\bar.txt";
|
||||
var (o, c) = Rules.NormalizePaths(input);
|
||||
Assert.True(c >= 1);
|
||||
Assert.Contains("<REPO>", o);
|
||||
Environment.SetEnvironmentVariable("RECORDINGTEST_REPO", null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundFloats_RoundsToSixDecimals()
|
||||
{
|
||||
var node = JsonNode.Parse("{\"x\": 3.1415926535897932, \"n\": 1}");
|
||||
var (n, c) = Rules.RoundFloatsInNode(node);
|
||||
Assert.Equal(1, c);
|
||||
Assert.Equal(3.141593, n!["x"]!.GetValue<double>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SortJsonKeys_RecursivelySortsObjects()
|
||||
{
|
||||
var node = JsonNode.Parse("{\"b\":1,\"a\":{\"y\":2,\"x\":1}}");
|
||||
var (sorted, _) = Rules.SortJsonKeys(node);
|
||||
var s = sorted!.ToJsonString();
|
||||
Assert.Equal("{\"a\":{\"x\":1,\"y\":2},\"b\":1}", s);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_IsIdempotent()
|
||||
{
|
||||
var input = "{\"b\":2.0000001,\"a\":\"2026-04-07T00:00:00Z\",\"id\":\"550e8400-e29b-41d4-a716-446655440000\"}";
|
||||
var first = Normalizer.Normalize(input, "default");
|
||||
var second = Normalizer.Normalize(first.Output, "default");
|
||||
Assert.Equal(first.Output, second.Output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_AppliesAllDefaultRules()
|
||||
{
|
||||
var input = "{\"ts\":\"2026-04-07T00:00:00Z\",\"x\":1.23456789}";
|
||||
var r = Normalizer.Normalize(input, "default");
|
||||
Assert.Equal(5, r.Log.Count);
|
||||
var ids = r.Log.Select(l => l.RuleId).ToList();
|
||||
Assert.Contains("strip_timestamps", ids);
|
||||
Assert.Contains("mask_guids", ids);
|
||||
Assert.Contains("normalize_paths", ids);
|
||||
Assert.Contains("round_floats", ids);
|
||||
Assert.Contains("sort_json_keys", ids);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user