Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 97 additions & 9 deletions Assets/Tests/EditMode/Simulation/MatchFingerprintV1Tests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using NUnit.Framework;
using Nova.Simulation.CommandsV1;
using Nova.Simulation.Replays;
Expand Down Expand Up @@ -78,20 +80,39 @@ public void ComputeHash_IsStableAcrossInstances_AndStubHashesAreDistinct()
"D-106 rules must not match the legacy empty rules stub");
}

/// <summary>
/// The failure text of every rules-revision tripwire (#138, D-120).
/// </summary>
private const string AppendRevisionHint =
"A hashed rules constant changed under an existing rules revision. Append the next " +
"RulesRevision with its own frozen literal row (MatchFingerprint, D-120) — do not " +
"re-pin a golden hash and do not edit a frozen row.";

[Test]
public void RulesRevisionOneAndTwo_GoldenHashesRemainByteStable()
{
ulong revisionOne = MatchFingerprint.ComputeRulesHash64(MatchFingerprint.RulesRevisionV1);
ulong revisionTwo = MatchFingerprint.ComputeRulesHash64(MatchFingerprint.RulesRevisionV2);

// NOT actually frozen — see issue #138. ComputeRulesHash64 writes
// TODAY's constants into every revision, so raising
// HqBaseCapacityAE (#131) moved the revision-1 hash too. Re-pinning
// here restores green; it does NOT restore the guarantee the name
// claims. Do not read this assertion as proof of anything until #138
// is decided.
Assert.That(revisionOne, Is.EqualTo(0x1163158B8146B168UL), "revision 1 canonical stream is frozen");
Assert.That(revisionTwo, Is.EqualTo(0x259E6977B751109BUL), "revision 2 canonical stream is frozen");
// Original pins (#138, D-120): revision 1 as introduced in e1970a9
// (HQ 2,000 / storage 2,000 / decay 25 % every 10 ticks), revision 2
// as introduced in 8512e9f (plus repair 10 / low-power repair 5).
// Both rows are literals now, so no later constant edit can move
// them; a red assertion here means a frozen row was edited.
Assert.That(revisionOne, Is.EqualTo(0x531CE8F614A16CB5UL), "revision 1 canonical stream is frozen");
Assert.That(revisionTwo, Is.EqualTo(0x07725EA26668C9F8UL), "revision 2 canonical stream is frozen");
}

[Test]
public void RulesRevisionThree_FrozenLiteral_IsTheCurrentStream()
{
// Revision 3 as of e4faa75 (#131, HQ 3,000). Its pre-#131 identity
// 0x05CCA8475789AD4A is the accepted D-120 gap.
Assert.That(MatchFingerprint.ComputeRulesHash64(MatchFingerprint.RulesRevisionV3),
Is.EqualTo(0xD1B683837D12FCEFUL), "revision 3 canonical stream is frozen");
Assert.Throws<ArgumentOutOfRangeException>(() => MatchFingerprint.ComputeRulesHash64(0));
Assert.Throws<ArgumentOutOfRangeException>(() =>
MatchFingerprint.ComputeRulesHash64((ushort)(MatchFingerprint.CurrentRulesRevision + 1)));
}

[Test]
Expand All @@ -101,12 +122,79 @@ public void CurrentRulesHash_MovesPastRevisionTwo_ForD104PlacementAndRepair()
ulong current = MatchFingerprint.ComputeCurrentRulesHash64();

Assert.That(MatchFingerprint.CurrentRulesRevision, Is.EqualTo(MatchFingerprint.RulesRevisionV3));
Assert.That(current, Is.EqualTo(MatchFingerprint.ComputeRulesHash64(MatchFingerprint.RulesRevisionV3)));
Assert.That(current, Is.EqualTo(MatchFingerprint.ComputeRulesHash64(MatchFingerprint.RulesRevisionV3)),
AppendRevisionHint);
Assert.That(current, Is.EqualTo(0xD1B683837D12FCEFUL), "revision 3 canonical stream is frozen");
Assert.That(current, Is.Not.EqualTo(revisionTwo),
"D-104 placement and repair behavior must not share revision 2's rules identity");
}

[Test]
public void CurrentLiveRules_MatchTheFrozenCurrentRevision()
{
// The tripwire (#138, D-120): the live constants must equal the
// frozen literal row of the current revision, field by field. The
// message names the moved constant instead of two opaque hashes.
string[] drift = MatchFingerprint.DescribeCurrentRulesDrift();

Assert.That(drift, Is.Empty, AppendRevisionHint + "\n" + string.Join("\n", drift));
Assert.That(MatchFingerprint.ComputeCurrentRulesHash64(),
Is.EqualTo(MatchFingerprint.ComputeRulesHash64(MatchFingerprint.CurrentRulesRevision)),
AppendRevisionHint);
}

[Test]
public void FrozenRevisions_ReadNoLiveConstants()
{
// Source-text guard (the NoFloatInSimulationTests pattern): the
// frozen-row region of MatchFingerprint.cs may hold literal values
// only. A row that reads EconomySystem/ConstructionSystem/... would
// compile, hash identically today and silently unfreeze again with
// the next balance change — exactly the #138 defect.
string source = File.ReadAllText(ResolveMatchFingerprintSource());
const string OpenMarker = "// <frozen-rules-rows>";
const string CloseMarker = "// </frozen-rules-rows>";
int open = source.IndexOf(OpenMarker, StringComparison.Ordinal);
int close = source.IndexOf(CloseMarker, StringComparison.Ordinal);
Assert.That(open, Is.GreaterThanOrEqualTo(0), "frozen-row region start marker missing");
Assert.That(close, Is.GreaterThan(open), "frozen-row region end marker missing");
Assert.That(source.IndexOf(OpenMarker, open + 1, StringComparison.Ordinal), Is.EqualTo(-1),
"exactly one frozen-row region expected");

// Comments go first: they may name live constants freely.
string region = Regex.Replace(source.Substring(open, close - open), @"//[^\n]*", string.Empty);

// Every row entry is RulesField.IntXX(<tag>, "<name>", <integer literal>);
// a SimFixed field is frozen as its raw value (decimal or hex).
int entries = Regex.Matches(region, @"RulesField\s*\.\s*Int(32|64)\s*\(").Count;
int literalEntries = Regex.Matches(region,
@"RulesField\s*\.\s*Int(32|64)\s*\(\s*\d+u?\s*,\s*""[^""]*""\s*,\s*-?(0[xX][0-9A-Fa-f]+|\d+)[Ll]?\s*\)").Count;
Assert.That(entries, Is.GreaterThanOrEqualTo(4 + 6 + 13), "expected the rows of revisions 1-3");
Assert.That(literalEntries, Is.EqualTo(entries),
"every frozen row value must be an integer literal, never a live constant");

// Strings blanked: no member access other than the RulesField factories.
string code = Regex.Replace(region, @"""[^""]*""", "\"\"");
MatchCollection access = Regex.Matches(code, @"\b(?!RulesField\b)[A-Za-z_]\w*\s*\.\s*[A-Za-z_]\w*");
var offenders = new string[access.Count];
for (int i = 0; i < access.Count; i++) offenders[i] = access[i].Value;
Assert.That(offenders, Is.Empty,
"the frozen-row region must not reference live constants: " + string.Join(", ", offenders));
}

/// <summary>
/// THE ONE LANE-SPECIFIC METHOD. The .NET mirror walks up from the test
/// binary; here the anchor is Application.dataPath (the
/// NoFloatInSimulationTests pattern).
/// </summary>
private static string ResolveMatchFingerprintSource()
{
string scripts = Path.Combine(UnityEngine.Application.dataPath, "_Project", "Scripts");
Assert.That(Directory.Exists(scripts), Is.True,
$"could not locate {scripts} — Application.dataPath is {UnityEngine.Application.dataPath}");
return Path.Combine(scripts, "Simulation", "Replays", "MatchFingerprint.cs");
}

[Test]
public void ComputeHash_AndEquality_AreSensitiveToEveryField()
{
Expand Down
Loading
Loading