- R-I: new CultNetSelectionPageTests.cs kills Soul's two named S20 mutants - dropping `wantDocument &&` in ToEdge (a header-projection edge would carry document payload bytes anyway) and always emitting Edges instead of `selection.HasHop ? ... : null` (a hop-less selection's page would carry an empty array instead of no Edges member) - plus a matched-vs-page-length test for R-G. Fixing ToEdge's mutant coverage surfaced a real bug the new tests caught: it serialized an edge payload through CultDocumentMessagePackSerialization, which requires the payload's type to be a registered CultDocument and throws for a plain declared value type (float, in the fixture); it now serializes directly through MessagePackSerializer. - R-D: CultNetCanonicalNumberTests gains "5\n"/"5\r\n" cases - .NET's $ matches immediately before a single trailing newline even without RegexOptions.Multiline, so the door's pattern is re-anchored with \A/\z and would have accepted "5\n" as canonical without this. Caching's TryGetIndexNumberRewritesADoublesExponentNotationToPositionalDigits is updated for 1e-7's actual exact decimal expansion (the R-D commit's own effect): the old assertion pinned "0.0000001", the shortest round-trip form a pre-R-D render would have produced. - R-K: every serverThread.Join() in NetworkingTests.cs that sat outside a try/finally now sits inside one, so a failed assertion cannot leave the poll thread running past the point where `using` disposes the socket it polls (an ObjectDisposedException on that raw background Thread was a host crash, not a test failure). The bodyDemand.HasConsumers assertions wait on the condition instead of assuming SubscribeAsync's completion and PublishDemand happened in an order a network round trip does not guarantee. - Reflection fix: DatabaseSubscription_FiltersCanonicalWireChangesByRequestedSchemaAlias built a v0-shaped CultNetDatabaseSubscribeMessage to invoke CreateMatchedRecord by reflection; that method now takes the shared SubscriptionRequest (R-A), so the test builds one through the same reflection path instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
101 lines
4.7 KiB
C#
101 lines
4.7 KiB
C#
#nullable enable
|
|
using NUnit.Framework;
|
|
|
|
namespace GameCult.Networking.Tests
|
|
{
|
|
// Q-J (docs/cultnet-selection-cut.md, section 2 "Numbers", 2026-09-22): the wire's canonical
|
|
// decimal form - the door's grammar and the comparator that reads it. Pure string-in functions,
|
|
// independent of the cache and the evaluator; CultDocumentSelectionSurfaceTests (GameCult.Caching.Tests)
|
|
// pins the row side's rendering of the same form.
|
|
public sealed class CultNetCanonicalNumberTests
|
|
{
|
|
[TestCase("0")]
|
|
[TestCase("1")]
|
|
[TestCase("-1")]
|
|
[TestCase("10")]
|
|
[TestCase("0.5")]
|
|
[TestCase("-0.5")]
|
|
[TestCase("9007199254740993")]
|
|
[TestCase("123.456")]
|
|
public void IsCanonicalAcceptsCanonicalSpellings(string value) =>
|
|
Assert.That(CultNetCanonicalNumber.IsCanonical(value), Is.True);
|
|
|
|
// The door's named mutants: a leading '+', a trailing fractional zero, a leading integer zero,
|
|
// exponent notation, and negative zero are each refused, never normalised.
|
|
[TestCase("+1")]
|
|
[TestCase("1.0")]
|
|
[TestCase("01")]
|
|
[TestCase("1e3")]
|
|
[TestCase("1E3")]
|
|
[TestCase("-0")]
|
|
[TestCase("")]
|
|
[TestCase(" 1")]
|
|
[TestCase("1.")]
|
|
[TestCase("1..5")]
|
|
// R-D/Q-J (Soul's whole-cut pass, confirmed): .NET's $ matches immediately before a single
|
|
// trailing newline even without RegexOptions.Multiline, so a naive `$`-anchored pattern accepts
|
|
// "5\n" as canonical - the door must refuse it exactly as it refuses any other non-canonical
|
|
// spelling, byte for byte the same as Rust and the JSON schema's ECMA-262 $.
|
|
[TestCase("5\n")]
|
|
[TestCase("5\r\n")]
|
|
public void IsCanonicalRefusesNonCanonicalSpellings(string value) =>
|
|
Assert.That(CultNetCanonicalNumber.IsCanonical(value), Is.False);
|
|
|
|
[Test]
|
|
public void IsCanonicalRefusesNull() =>
|
|
Assert.That(CultNetCanonicalNumber.IsCanonical(null), Is.False);
|
|
|
|
// The comparator: sign first, then the integer part's length, then its digits, then the
|
|
// fraction padded on the right - never a lexicographic compare of the whole string, which
|
|
// would rank "10" below "9".
|
|
[Test]
|
|
public void CompareOrdersByMagnitudeNotLexicographically()
|
|
{
|
|
Assert.That(CultNetCanonicalNumber.Compare("10", "9"), Is.GreaterThan(0));
|
|
Assert.That(CultNetCanonicalNumber.Compare("9", "10"), Is.LessThan(0));
|
|
}
|
|
|
|
[Test]
|
|
public void CompareOrdersNegativeBelowPositive()
|
|
{
|
|
Assert.That(CultNetCanonicalNumber.Compare("-1", "1"), Is.LessThan(0));
|
|
Assert.That(CultNetCanonicalNumber.Compare("1", "-1"), Is.GreaterThan(0));
|
|
Assert.That(CultNetCanonicalNumber.Compare("-10", "-2"), Is.LessThan(0));
|
|
}
|
|
|
|
// Same leading digit, different length: "100" > "99" even though '1' < '9' as a bare character
|
|
// comparison - this is what a comparator that skips the integer part's length gets wrong.
|
|
[Test]
|
|
public void CompareOrdersByIntegerPartLengthBeforeDigits()
|
|
{
|
|
Assert.That(CultNetCanonicalNumber.Compare("100", "99"), Is.GreaterThan(0));
|
|
Assert.That(CultNetCanonicalNumber.Compare("99", "100"), Is.LessThan(0));
|
|
}
|
|
|
|
// "1.5" vs "1.45": padded to "50" vs "45" so 1.5 > 1.45. Comparing the raw, unpadded fraction
|
|
// strings would get this wrong ("5" > "45" as characters is true by luck, "45" > "5" as strings
|
|
// sorted by length first is not the rule either) - padding to equal length is the rule.
|
|
[Test]
|
|
public void CompareOrdersFractionsByPaddedDigitsNotRawLength()
|
|
{
|
|
Assert.That(CultNetCanonicalNumber.Compare("1.5", "1.45"), Is.GreaterThan(0));
|
|
Assert.That(CultNetCanonicalNumber.Compare("1.45", "1.5"), Is.LessThan(0));
|
|
}
|
|
|
|
[Test]
|
|
public void CompareTreatsEqualCanonicalStringsAsEqual()
|
|
{
|
|
Assert.That(CultNetCanonicalNumber.Compare("5", "5"), Is.EqualTo(0));
|
|
Assert.That(CultNetCanonicalNumber.Compare("-0.5", "-0.5"), Is.EqualTo(0));
|
|
}
|
|
|
|
// The row-rendering mutant named in section 2 "Numbers": a long past 2^53 (where double loses
|
|
// integer precision) must still compare correctly once rendered. This pins the comparator's
|
|
// side of that guarantee; CultDocumentSelectionSurfaceTests pins the cache's rendering side.
|
|
[Test]
|
|
public void CompareDistinguishesALongPast2Pow53FromItsFloat64Rounding()
|
|
{
|
|
Assert.That(CultNetCanonicalNumber.Compare("9007199254740993", "9007199254740992"), Is.GreaterThan(0));
|
|
}
|
|
}
|
|
}
|