Fix normalizer sidecar log and coverage test (#4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
minsung
2026-04-07 14:17:22 +09:00
parent 7920de15b3
commit 05c7a3f388
6 changed files with 251 additions and 31 deletions

View File

@@ -6,21 +6,43 @@ 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.
/// docs/sut-catalog/json-configs.json is covered by a semantically appropriate
/// rule that is actually present in the default profile.
/// </summary>
public class CoverageTests
{
// Explicit field -> rule mapping. Each entry must be a rule that semantically
// covers the kind of value the field holds.
// *Path / *FileName / *RecentFile* -> normalize_paths
// Known volatile boolean / color / scalar settings -> mask_volatile_settings
// No catch-all to sort_json_keys for arbitrary scalars.
private static readonly Dictionary<string, string> FieldRuleMap = new(StringComparer.Ordinal)
{
// path-bearing
["AutoSaveFilePath"] = "normalize_paths",
["AutoSave_RecentFileName"] = "normalize_paths",
// volatile boolean / scalar UI settings
["CanOverrideWireColorWithFace"] = "mask_volatile_settings",
["IsSidePanelVisible"] = "mask_volatile_settings",
["OverrideFaceColor"] = "mask_volatile_settings",
["Solar_IsLocalTime"] = "mask_volatile_settings",
["VisibleGrid"] = "mask_volatile_settings",
["GridSnap"] = "mask_volatile_settings",
["MidpointOsnap"] = "mask_volatile_settings",
["GridSpacing"] = "mask_volatile_settings",
// volatile color channels
["GridColor.ALPHA"] = "mask_volatile_settings",
["GridColor.BLUE"] = "mask_volatile_settings",
["GridColor.GREEN"] = "mask_volatile_settings",
["GridColor.RED"] = "mask_volatile_settings",
["MajorGridColor.ALPHA"] = "mask_volatile_settings",
["MajorGridColor.BLUE"] = "mask_volatile_settings",
["MajorGridColor.GREEN"] = "mask_volatile_settings",
["MajorGridColor.RED"] = "mask_volatile_settings",
};
private static string FindCatalog()
{
var dir = AppContext.BaseDirectory;
@@ -51,30 +73,29 @@ public class CoverageTests
}
}
// 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 profileRules = new HashSet<string>(profile.Rules, StringComparer.Ordinal);
var uncovered = new List<string>();
var unmapped = new List<string>();
var notInProfile = 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);
if (!FieldRuleMap.TryGetValue(field, out var rule))
{
unmapped.Add(field);
continue;
}
if (!profileRules.Contains(rule))
{
notInProfile.Add($"{field} -> {rule}");
}
}
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);
Assert.True(unmapped.Count == 0,
"Suspected fields without an explicit semantic rule mapping: " + string.Join(", ", unmapped));
Assert.True(notInProfile.Count == 0,
"Mapped rules missing from default profile: " + string.Join(", ", notInProfile));
}
}