2026-11-11 05:13:08 +00:00
#nullable enable
using System ;
using System.Buffers ;
using System.Collections.Generic ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Reflection ;
using GameCult.Caching ;
using GameCult.Caching.MessagePack ;
using MessagePack ;
using NUnit.Framework ;
namespace GameCult.Caching.Tests
{
// variants:ruling:unique-indexes. The canonical index text and the unique-index load verdicts in tests/vectors/unique-index.
// The C# reference is the only writer (CULTCACHE_WRITE_VECTORS=1); every other run reads the committed bytes and
// compares them byte for byte. Other runtimes are held to the same text and the same verdicts.
public class UniqueIndexVectorTests
{
private const string WriteVariable = "CULTCACHE_WRITE_VECTORS" ;
private static readonly DateTimeOffset StoredAt = new ( 2026 , 1 , 1 , 0 , 0 , 0 , TimeSpan . Zero ) ;
private static bool Writing = > Environment . GetEnvironmentVariable ( WriteVariable ) = = "1" ;
private static string VectorDirectory ( )
{
for ( var dir = new DirectoryInfo ( TestContext . CurrentContext . TestDirectory ) ; dir ! = null ; dir = dir . Parent )
{
var vectors = Path . Combine ( dir . FullName , "tests" , "vectors" ) ;
if ( Directory . Exists ( vectors ) ) return Path . Combine ( vectors , "unique-index" ) ;
}
throw new DirectoryNotFoundException ( $"tests/vectors not found above {TestContext.CurrentContext.TestDirectory}." ) ;
}
private static string VectorPath ( string name ) = > Path . Combine ( VectorDirectory ( ) , name ) ;
private static void Check ( string name , byte [ ] reference )
{
var path = VectorPath ( name ) ;
if ( Writing )
{
Directory . CreateDirectory ( Path . GetDirectoryName ( path ) ! ) ;
File . WriteAllBytes ( path , reference ) ;
return ;
}
Assert . That ( File . Exists ( path ) , Is . True , $"vector {name} is committed" ) ;
Assert . That ( Convert . ToHexString ( reference ) , Is . EqualTo ( Convert . ToHexString ( File . ReadAllBytes ( path ) ) ) , $"vector {name}" ) ;
}
private static byte [ ] Committed ( string name )
{
var path = VectorPath ( name ) ;
Assert . That ( File . Exists ( path ) , Is . True , $"vector {name} is committed" ) ;
return File . ReadAllBytes ( path ) ;
}
// ---- values.msgpack: [label, kind, value, text-or-nil] ----
private sealed record Row ( string Label , string Kind , object Value , string? Text ) ;
private static readonly MethodInfo RenderCanonicalNumber =
typeof ( CultDocumentRegistry ) . GetMethod ( "RenderCanonicalNumber" , BindingFlags . NonPublic | BindingFlags . Static ) ! ;
private static object? ByIndex ( CultCache cache , Type type , string alias , string value ) = >
typeof ( CultCache ) . GetMethod ( nameof ( CultCache . GetByIndex ) ) ! . MakeGenericMethod ( type ) . Invoke ( cache , new object [ ] { alias , value } ) ;
private static string? Number ( object raw ) = > ( string? ) RenderCanonicalNumber . Invoke ( null , new [ ] { raw } ) ;
// Strings and booleans carry the text the rule states; numbers carry whatever the reference renders, never hand-typed.
private static List < Row > Rows ( ) = > new ( )
{
new ( "plain" , "string" , "S-1" , "S-1" ) ,
new ( "empty" , "string" , "" , null ) ,
new ( "space" , "string" , " " , null ) ,
new ( "nel" , "string" , "\u0085" , null ) ,
new ( "nbsp" , "string" , "\u00A0" , null ) ,
new ( "ideographic-space" , "string" , "\u3000" , null ) ,
new ( "vertical-tab" , "string" , "\u000B" , null ) ,
new ( "bom" , "string" , "\uFEFF" , "\uFEFF" ) ,
new ( "unit-separator" , "string" , "\u001F" , "\u001F" ) ,
new ( "padded" , "string" , " a " , " a " ) ,
new ( "int64-max" , "int64" , long . MaxValue , Number ( long . MaxValue ) ) ,
new ( "int64-min" , "int64" , long . MinValue , Number ( long . MinValue ) ) ,
new ( "uint64-max" , "uint64" , ulong . MaxValue , Number ( ulong . MaxValue ) ) ,
new ( "double-zero" , "double" , 0.0 , Number ( 0.0 ) ) ,
new ( "double-negative-zero" , "double" , - 0.0 , Number ( - 0.0 ) ) ,
new ( "double-tenth" , "double" , 0.1 , Number ( 0.1 ) ) ,
new ( "double-1e21" , "double" , 1e21 , Number ( 1e21 ) ) ,
new ( "double-2.5e-7" , "double" , 2.5e-7 , Number ( 2.5e-7 ) ) ,
new ( "double-nan" , "double" , double . NaN , Number ( double . NaN ) ) ,
new ( "double-infinity" , "double" , double . PositiveInfinity , Number ( double . PositiveInfinity ) ) ,
new ( "bool-true" , "bool" , true , "True" ) ,
new ( "bool-false" , "bool" , false , "False" ) ,
} ;
private static byte [ ] ValuesFile ( )
{
var buffer = new ArrayBufferWriter < byte > ( ) ;
var writer = new MessagePackWriter ( buffer ) ;
var rows = Rows ( ) ;
writer . WriteArrayHeader ( rows . Count ) ;
foreach ( var row in rows )
{
writer . WriteArrayHeader ( 4 ) ;
writer . Write ( row . Label ) ;
writer . Write ( row . Kind ) ;
switch ( row . Value )
{
case string s : writer . Write ( s ) ; break ;
case long l : writer . Write ( l ) ; break ;
case ulong u : writer . Write ( u ) ; break ;
case double d : writer . Write ( d ) ; break ;
case bool b : writer . Write ( b ) ; break ;
default : throw new InvalidOperationException ( row . Kind ) ;
}
if ( row . Text = = null ) writer . WriteNil ( ) ; else writer . Write ( row . Text ) ;
}
writer . Flush ( ) ;
return buffer . WrittenSpan . ToArray ( ) ;
}
private static readonly Dictionary < string , Type > KindTypes = new [ ]
{
( "string" , typeof ( string ) ) , ( "int64" , typeof ( long ) ) , ( "uint64" , typeof ( ulong ) ) , ( "double" , typeof ( double ) ) , ( "bool" , typeof ( bool ) )
} . ToDictionary ( pair = > pair . Item1 , pair = > EmittedDocumentTypes . Emit (
"UqKind" + pair . Item1 , "tests.uq_kind_" + pair . Item1 , "tests.uq_kind_" + pair . Item1 + ".v1" ,
new [ ] { new EmittedDocumentTypes . Field ( "Value" , pair . Item2 , 0 , Index : "v" ) } ) ) ;
[Test]
public void ValuesFileIsTheReferencesCanonicalText ( )
{
Check ( "values.msgpack" , ValuesFile ( ) ) ;
var reader = new MessagePackReader ( Committed ( "values.msgpack" ) ) ;
var count = reader . ReadArrayHeader ( ) ;
var rows = Rows ( ) ;
Assert . That ( count , Is . EqualTo ( rows . Count ) ) ;
foreach ( var row in rows )
{
Assert . That ( reader . ReadArrayHeader ( ) , Is . EqualTo ( 4 ) ) ;
Assert . That ( reader . ReadString ( ) , Is . EqualTo ( row . Label ) ) ;
Assert . That ( reader . ReadString ( ) , Is . EqualTo ( row . Kind ) ) ;
reader . Skip ( ) ;
var text = reader . TryReadNil ( ) ? null : reader . ReadString ( ) ;
Assert . That ( text , Is . EqualTo ( row . Text ) , row . Label ) ;
var type = KindTypes [ row . Kind ] ;
var registry = CultDocumentRegistry . ForTypes ( new [ ] { type } ) ;
using var cache = CultCacheMessagePack . Create ( Path . Combine ( Path . GetTempPath ( ) , $"cultlib-uidxv-{Guid.NewGuid():N}.cc" ) , new CultCacheOpenOptions { Registry = registry } ) ;
cache . Commit ( batch = >
{
batch . Upsert ( type , EmittedDocumentTypes . New ( type , ( "Value" , row . Value ) ) , new CultRecordKey ( "one" ) ) ;
batch . Upsert ( type , EmittedDocumentTypes . New ( type , ( "Value" , row . Value ) ) , new CultRecordKey ( text = = null ? "two" : "one" ) ) ;
} ) ;
if ( text = = null )
{
Assert . That ( cache . Get ( new CultRecordKey ( "two" ) ) , Is . Not . Null , $"{row.Label}: a blank value commits twice" ) ;
foreach ( var probe in new [ ] { "" , " " , "NaN" , "0" , "Infinity" , "∞" } )
Assert . That ( ByIndex ( cache , type , "v" , probe ) , Is . Null , $"{row.Label}: found by '{probe}'" ) ;
}
else
{
Assert . That ( ByIndex ( cache , type , "v" , text ) , Is . Not . Null , $"{row.Label}: found by its text" ) ;
}
}
}
// ---- the stores: written through a role, judged by an index ----
private static readonly EmittedDocumentTypes . Field [ ] RoleFields =
{
new ( "Sku" , typeof ( string ) , 0 , Role : "Sku" ) , new ( "Category" , typeof ( string ) , 1 )
} ;
private static readonly EmittedDocumentTypes . Field [ ] IndexFields =
{
new ( "Sku" , typeof ( string ) , 0 , Index : "Sku" ) , new ( "Category" , typeof ( string ) , 1 )
} ;
private static readonly Type ItemRole = EmittedDocumentTypes . Emit ( "UqVecItemRole" , "tests.unique_item" , "tests.unique_item.v1" , RoleFields ) ;
private static readonly Type ItemIndex = EmittedDocumentTypes . Emit ( "UqVecItemIndex" , "tests.unique_item" , "tests.unique_item.v1" , IndexFields ) ;
private static readonly ( string Key , string Sku , string Category ) [ ] Items =
{
( "item:a" , "S-1" , "tools" ) , ( "item:b" , "S-1" , "tools" ) , ( "item:c" , " " , "parts" ) , ( "item:d" , "\u0085" , "parts" )
} ;
private static byte [ ] StoreBytes ( string [ ] keys )
{
var directory = Path . Combine ( Path . GetTempPath ( ) , $"cultlib-uidxs-{Guid.NewGuid():N}" ) ;
Directory . CreateDirectory ( directory ) ;
try
{
var path = Path . Combine ( directory , "store.cc" ) ;
using ( var cache = CultCacheMessagePack . Create ( path , new CultCacheOpenOptions
{
Registry = CultDocumentRegistry . ForTypes ( new [ ] { ItemRole } ) , StoreFlushOnDispose = true
} ) )
{
cache . Clock = ( ) = > StoredAt ;
foreach ( var item in Items . Where ( item = > keys . Contains ( item . Key ) ) )
cache . UpsertAsync ( ItemRole , EmittedDocumentTypes . New ( ItemRole , ( "Sku" , item . Sku ) , ( "Category" , item . Category ) ) , new CultRecordKey ( item . Key ) ) . GetAwaiter ( ) . GetResult ( ) ;
cache . FlushAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
}
return File . ReadAllBytes ( path ) ;
}
finally
{
Directory . Delete ( directory , recursive : true ) ;
}
}
private static readonly ( string File , string? Alias , string Outcome , string? K1 , string? K2 ) [ ] Outcomes =
{
( "duplicate-store.cc" , "Sku" , "refused" , "item:a" , "item:b" ) ,
( "duplicate-store.cc" , null , "open" , null , null ) ,
( "blank-store.cc" , "Sku" , "open" , null , null ) ,
} ;
private static byte [ ] OutcomesFile ( )
{
var buffer = new ArrayBufferWriter < byte > ( ) ;
var writer = new MessagePackWriter ( buffer ) ;
writer . WriteArrayHeader ( Outcomes . Length ) ;
foreach ( var row in Outcomes )
{
writer . WriteArrayHeader ( 5 ) ;
writer . Write ( row . File ) ;
foreach ( var text in new [ ] { row . Alias , row . Outcome , row . K1 , row . K2 } )
if ( text = = null ) writer . WriteNil ( ) ; else writer . Write ( text ) ;
}
writer . Flush ( ) ;
return buffer . WrittenSpan . ToArray ( ) ;
}
private static string? NextText ( ref MessagePackReader reader ) = > reader . TryReadNil ( ) ? null : reader . ReadString ( ) ;
[Test]
public void StoreVectorsHoldTheirVerdicts ( )
{
Check ( "duplicate-store.cc" , StoreBytes ( new [ ] { "item:a" , "item:b" , "item:c" , "item:d" } ) ) ;
Check ( "blank-store.cc" , StoreBytes ( new [ ] { "item:c" , "item:d" } ) ) ;
Check ( "outcomes.msgpack" , OutcomesFile ( ) ) ;
var reader = new MessagePackReader ( Committed ( "outcomes.msgpack" ) ) ;
var count = reader . ReadArrayHeader ( ) ;
Assert . That ( count , Is . EqualTo ( Outcomes . Length ) ) ;
for ( var i = 0 ; i < count ; i + + )
{
Assert . That ( reader . ReadArrayHeader ( ) , Is . EqualTo ( 5 ) ) ;
var file = reader . ReadString ( ) ! ;
var alias = NextText ( ref reader ) ;
var outcome = NextText ( ref reader ) ! ;
var k1 = NextText ( ref reader ) ;
var k2 = NextText ( ref reader ) ;
var directory = Path . Combine ( Path . GetTempPath ( ) , $"cultlib-uidxo-{Guid.NewGuid():N}" ) ;
Directory . CreateDirectory ( directory ) ;
try
{
var path = Path . Combine ( directory , "store.cc" ) ;
File . WriteAllBytes ( path , Committed ( file ) ) ;
var before = File . ReadAllBytes ( path ) ;
var type = alias = = null ? ItemRole : ItemIndex ;
var registry = CultDocumentRegistry . ForTypes ( new [ ] { type } ) ;
if ( outcome = = "open" )
{
using var cache = CultCacheMessagePack . Create ( path , new CultCacheOpenOptions { Registry = registry } ) ;
Assert . That ( cache . Get ( new CultRecordKey ( "item:c" ) ) , Is . Not . Null , file ) ;
}
else
{
var refused = Assert . Throws < InvalidOperationException > ( ( ) = > CultCacheMessagePack . Create ( path , new CultCacheOpenOptions { Registry = registry } ) . Dispose ( ) ) ! ;
Assert . That ( refused . Message , Is . EqualTo (
$"Records {k1} and {k2} (tests.unique_item) both hold unique index '{alias}'; an index value names one record." ) ) ;
Assert . That ( refused . Message , Does . Not . Contain ( "S-1" ) ) ;
}
Assert . That ( File . ReadAllBytes ( path ) , Is . EqualTo ( before ) , $"{file} is untouched" ) ;
}
finally
{
Directory . Delete ( directory , recursive : true ) ;
}
}
}
2026-11-11 10:05:32 +00:00
// ---- refusals.msgpack: [label, ops, refusal]; the pair a refusal names is a function of the resulting state ----
private sealed record Schema ( string Name , Type Role , Type Index , string SecondField ) ;
private static Schema Plain ( string suffix , string name ) = > new (
name ,
EmittedDocumentTypes . Emit ( "UqVec" + suffix + "Role" , name , name + ".v1" , RoleFields ) ,
EmittedDocumentTypes . Emit ( "UqVec" + suffix + "Index" , name , name + ".v1" , IndexFields ) ,
"Category" ) ;
private static readonly Schema [ ] Schemas =
{
new ( "tests.unique_item" , ItemRole , ItemIndex , "Category" ) ,
new ( "tests.uq_pair" ,
EmittedDocumentTypes . Emit ( "UqVecPairRole" , "tests.uq_pair" , "tests.uq_pair.v1" ,
new EmittedDocumentTypes . Field [ ] { new ( "Sku" , typeof ( string ) , 0 , Role : "Sku" ) , new ( "Alt" , typeof ( string ) , 1 , Role : "Alt" ) } ) ,
EmittedDocumentTypes . Emit ( "UqVecPairIndex" , "tests.uq_pair" , "tests.uq_pair.v1" ,
new EmittedDocumentTypes . Field [ ] { new ( "Sku" , typeof ( string ) , 0 , Index : "Sku" ) , new ( "Alt" , typeof ( string ) , 1 , Index : "Alt" ) } ) ,
"Alt" ) ,
new ( "tests.uq_alias" ,
EmittedDocumentTypes . Emit ( "UqVecAliasRole" , "tests.uq_alias" , "tests.uq_alias.v1" ,
new EmittedDocumentTypes . Field [ ] { new ( "Sku" , typeof ( string ) , 0 , Role : "Sku" ) , new ( "Alt" , typeof ( string ) , 1 , Role : "alt" ) } ) ,
EmittedDocumentTypes . Emit ( "UqVecAliasIndex" , "tests.uq_alias" , "tests.uq_alias.v1" ,
new EmittedDocumentTypes . Field [ ] { new ( "Sku" , typeof ( string ) , 0 , Index : "Sku" ) , new ( "Alt" , typeof ( string ) , 1 , Index : "alt" ) } ) ,
"Alt" ) ,
2026-11-11 13:41:10 +00:00
new ( "tests.uq_wide" ,
EmittedDocumentTypes . Emit ( "UqVecWideRole" , "tests.uq_wide" , "tests.uq_wide.v1" ,
new EmittedDocumentTypes . Field [ ] { new ( "Sku" , typeof ( string ) , 0 , Role : "\uFF21" ) , new ( "Alt" , typeof ( string ) , 1 , Role : "\U00010400" ) } ) ,
EmittedDocumentTypes . Emit ( "UqVecWideIndex" , "tests.uq_wide" , "tests.uq_wide.v1" ,
new EmittedDocumentTypes . Field [ ] { new ( "Sku" , typeof ( string ) , 0 , Index : "\uFF21" ) , new ( "Alt" , typeof ( string ) , 1 , Index : "\U00010400" ) } ) ,
"Alt" ) ,
2026-11-11 10:05:32 +00:00
Plain ( "Alpha" , "tests.uq_alpha" ) ,
Plain ( "Beta" , "tests.uq_beta" ) ,
} ;
private static Schema SchemaNamed ( string name ) = > Schemas . Single ( schema = > schema . Name = = name ) ;
private sealed record Rec ( string Key , string Schema , string Sku , string Second = "" ) ;
private sealed record Op ( string Kind , string? File = null , Rec [ ] ? Records = null ) ;
private sealed record Refusal ( string K1 , string K2 , string Schema , string Alias ) ;
private sealed record Case ( string Label , Op [ ] Ops , Refusal ? Refusal ) ;
private const string Item = "tests.unique_item" ;
private const string Pair = "tests.uq_pair" ;
private const string Alias = "tests.uq_alias" ;
2026-11-11 13:41:10 +00:00
private const string Wide = "tests.uq_wide" ;
2026-11-11 10:05:32 +00:00
private const string Alpha = "tests.uq_alpha" ;
private const string Beta = "tests.uq_beta" ;
private static Rec [ ] Items_ ( params ( string Key , string Sku ) [ ] rows ) = > rows . Select ( row = > new Rec ( row . Key , Item , row . Sku ) ) . ToArray ( ) ;
private static Op Open ( string? file = null ) = > new ( "open" , file ) ;
private static Op Pull ( string file ) = > new ( "pull" , file ) ;
private static Op Commit ( params Rec [ ] records ) = > new ( "commit" , Records : records ) ;
private static readonly Rec [ ] PairRecords = { new ( "item:a" , Pair , "S" , "X" ) , new ( "item:b" , Pair , "S" , "Y" ) , new ( "item:c" , Pair , "T" , "Y" ) } ;
private static readonly Rec [ ] SurrogateRecords = Items_ ( ( "k\uFF01" , "S" ) , ( "k\U0001F600" , "S" ) , ( "k\uE000" , "S" ) ) ;
private static readonly Rec [ ] TypeRecords =
{
new ( "b:1" , Alpha , "P" ) , new ( "a:1" , Beta , "P" ) , new ( "b:2" , Alpha , "P" ) , new ( "a:2" , Beta , "P" )
} ;
private static readonly Rec [ ] AliasRecords = { new ( "item:a" , Alias , "S" , "X" ) , new ( "item:b" , Alias , "S" , "Y" ) , new ( "item:c" , Alias , "T" , "Y" ) } ;
private static readonly Rec [ ] RepullBefore = Items_ ( ( "item:a1" , "A" ) , ( "item:b1" , "C" ) ) ;
private static readonly Rec [ ] RepullAfter = Items_ ( ( "item:a1" , "A" ) , ( "item:b1" , "B" ) , ( "item:b2" , "B" ) , ( "item:c1" , "A" ) ) ;
private static readonly Rec [ ] KeyCaseRecords = Items_ ( ( "a" , "S" ) , ( "z" , "S" ) , ( "B" , "T" ) , ( "C" , "T" ) ) ;
2026-11-11 10:47:13 +00:00
private static readonly Rec [ ] HolderCaseRecords = Items_ ( ( "a" , "S" ) , ( "A" , "S" ) ) ;
2026-11-11 11:42:27 +00:00
private static readonly Rec [ ] CrossSchemaRecords = { new ( "x:1" , Alpha , "P" ) , new ( "y:1" , Beta , "P" ) } ;
// Distinct under any trim, case-fold, NFC or NFKC of the value: 'S' and 's', 'S' and 'S ', NFC and NFD, fullwidth S.
private static readonly Rec [ ] DistinctValueRecords = Items_ (
( "k:1" , "S" ) , ( "k:2" , "s" ) , ( "k:3" , "S " ) , ( "k:4" , "\u00E9" ) , ( "k:5" , "e\u0301" ) , ( "k:6" , "\uFF33" ) , ( "k:7" , " S" ) ) ;
private static readonly Rec [ ] NormalKeyRecords = Items_ ( ( "\u00E9" , "S" ) , ( "e\u0301" , "S" ) ) ;
2026-11-11 13:41:10 +00:00
// Three holders of one value: code-unit order is e+U+0302, f, U+00E9; NFC would order f, U+00E9, e+U+0302 and NFD U+00E9, e+U+0302, f.
private static readonly Rec [ ] ThreeHolderRecords = Items_ ( ( "e\u0302" , "S" ) , ( "f" , "S" ) , ( "\u00E9" , "S" ) ) ;
// Both records violate both aliases: by code unit U+10400 (D801 DC00) precedes U+FF21, by code point U+FF21 does.
private static readonly Rec [ ] WideRecords = { new ( "item:a" , Wide , "S" , "X" ) , new ( "item:b" , Wide , "S" , "X" ) } ;
2026-11-11 10:05:32 +00:00
private static readonly Rec [ ] KeyPunctuationRecords = Items_ ( ( "_" , "S" ) , ( "a" , "S" ) , ( "Z" , "T" ) , ( "b" , "T" ) ) ;
private static readonly Rec [ ] SurrogateGroupRecords = Items_ ( ( "\uE000" , "S" ) , ( "\uE000b" , "S" ) , ( "\U0001F600" , "T" ) , ( "\U0001F600b" , "T" ) ) ;
private static readonly ( string File , Rec [ ] Records ) [ ] RefusalStores =
{
( "pair-store.cc" , PairRecords ) , ( "surrogate-store.cc" , SurrogateRecords ) , ( "types-store.cc" , TypeRecords ) ,
( "repull-before.cc" , RepullBefore ) , ( "repull-after.cc" , RepullAfter ) , ( "alias-store.cc" , AliasRecords ) ,
( "key-case-store.cc" , KeyCaseRecords ) , ( "key-punctuation-store.cc" , KeyPunctuationRecords ) ,
( "surrogate-groups-store.cc" , SurrogateGroupRecords ) ,
2026-11-11 10:47:13 +00:00
( "holder-case-store.cc" , HolderCaseRecords ) ,
2026-11-11 11:42:27 +00:00
( "cross-schema-store.cc" , CrossSchemaRecords ) , ( "distinct-values-store.cc" , DistinctValueRecords ) ,
2026-11-11 13:41:10 +00:00
( "normal-keys-store.cc" , NormalKeyRecords ) , ( "normal-three-store.cc" , ThreeHolderRecords ) , ( "wide-store.cc" , WideRecords ) ,
2026-11-11 10:05:32 +00:00
} ;
private static readonly Refusal SurrogateRefusal = new ( "k\U0001F600" , "k\uE000" , Item , "Sku" ) ;
private static readonly Refusal TypesRefusal = new ( "a:1" , "a:2" , Beta , "Sku" ) ;
private static readonly Refusal PairRefusal = new ( "item:b" , "item:c" , Pair , "Alt" ) ;
private static readonly Refusal AliasRefusal = new ( "item:a" , "item:b" , Alias , "Sku" ) ;
private static readonly Refusal RepullRefusal = new ( "item:a1" , "item:c1" , Item , "Sku" ) ;
private static readonly Refusal KeyCaseRefusal = new ( "B" , "C" , Item , "Sku" ) ;
2026-11-11 10:47:13 +00:00
private static readonly Refusal HolderCaseRefusal = new ( "A" , "a" , Item , "Sku" ) ;
2026-11-11 11:42:27 +00:00
private static readonly Refusal NormalKeyRefusal = new ( "e\u0301" , "\u00E9" , Item , "Sku" ) ;
2026-11-11 13:41:10 +00:00
private static readonly Refusal ThreeHolderRefusal = new ( "e\u0302" , "f" , Item , "Sku" ) ;
private static readonly Refusal WideRefusal = new ( "item:a" , "item:b" , Wide , "\U00010400" ) ;
2026-11-11 10:05:32 +00:00
private static readonly Refusal KeyPunctuationRefusal = new ( "Z" , "b" , Item , "Sku" ) ;
private static readonly Refusal SurrogateGroupRefusal = new ( "\U0001F600" , "\U0001F600b" , Item , "Sku" ) ;
private static readonly Case [ ] Cases =
{
new ( "batch-later-value-staged-first" , new [ ] { Open ( ) , Commit ( Items_ ( ( "x" , "A" ) , ( "y" , "A" ) , ( "a" , "B" ) , ( "b" , "B" ) ) ) } ,
new Refusal ( "a" , "b" , Item , "Sku" ) ) ,
new ( "batch-surrogate-keys" , new [ ] { Open ( ) , Commit ( SurrogateRecords ) } , SurrogateRefusal ) ,
new ( "batch-interleaved-types" , new [ ] { Open ( ) , Commit ( TypeRecords ) } , TypesRefusal ) ,
new ( "batch-two-aliases" , new [ ] { Open ( ) , Commit ( PairRecords ) } , PairRefusal ) ,
new ( "held-then-landing" , new [ ] { Open ( ) , Commit ( Items_ ( ( "m" , "S" ) ) ) , Commit ( Items_ ( ( "a" , "S" ) ) ) } ,
new Refusal ( "a" , "m" , Item , "Sku" ) ) ,
new ( "held-in-the-least-group" , new [ ] { Open ( ) , Commit ( Items_ ( ( "m" , "A" ) ) ) , Commit ( Items_ ( ( "z" , "B" ) , ( "y" , "B" ) , ( "a" , "A" ) ) ) } ,
new Refusal ( "a" , "m" , Item , "Sku" ) ) ,
new ( "swap-off-a-held-value" , new [ ] { Open ( ) , Commit ( Items_ ( ( "a1" , "A" ) ) ) , Commit ( Items_ ( ( "a1" , "B" ) , ( "c1" , "A" ) ) ) } , null ) ,
new ( "load-two-aliases" , new [ ] { Open ( "pair-store.cc" ) } , PairRefusal ) ,
new ( "load-surrogate-keys" , new [ ] { Open ( "surrogate-store.cc" ) } , SurrogateRefusal ) ,
new ( "load-interleaved-types" , new [ ] { Open ( "types-store.cc" ) } , TypesRefusal ) ,
new ( "repull-held-pair" , new [ ] { Open ( "repull-before.cc" ) , Pull ( "repull-after.cc" ) } , RepullRefusal ) ,
new ( "load-repull-after" , new [ ] { Open ( "repull-after.cc" ) } , RepullRefusal ) ,
new ( "commit-onto-a-loaded-holder" , new [ ] { Open ( "repull-before.cc" ) , Commit ( Items_ ( ( "item:z" , "A" ) ) ) } ,
new Refusal ( "item:a1" , "item:z" , Item , "Sku" ) ) ,
// Rows 14-19 extend the spec's floor of 13: orderings a code-point, case-insensitive or culture compare would flip.
new ( "batch-key-case" , new [ ] { Open ( ) , Commit ( KeyCaseRecords ) } , KeyCaseRefusal ) ,
new ( "batch-key-punctuation" , new [ ] { Open ( ) , Commit ( KeyPunctuationRecords ) } , KeyPunctuationRefusal ) ,
new ( "batch-key-case-in-a-group" , new [ ] { Open ( ) , Commit ( Items_ ( ( "a" , "S" ) , ( "B" , "S" ) ) ) } ,
new Refusal ( "B" , "a" , Item , "Sku" ) ) ,
new ( "batch-surrogate-groups" , new [ ] { Open ( ) , Commit ( SurrogateGroupRecords ) } , SurrogateGroupRefusal ) ,
new ( "batch-alias-case" , new [ ] { Open ( ) , Commit ( AliasRecords ) } , AliasRefusal ) ,
new ( "load-alias-case" , new [ ] { Open ( "alias-store.cc" ) } , AliasRefusal ) ,
// Rows 20-22 load rows 14, 15 and 17's records, so a runtime with no batch commit still walks cross-group key order.
new ( "load-key-case" , new [ ] { Open ( "key-case-store.cc" ) } , KeyCaseRefusal ) ,
new ( "load-key-punctuation" , new [ ] { Open ( "key-punctuation-store.cc" ) } , KeyPunctuationRefusal ) ,
new ( "load-surrogate-groups" , new [ ] { Open ( "surrogate-groups-store.cc" ) } , SurrogateGroupRefusal ) ,
2026-11-11 10:47:13 +00:00
// Rows 23-24: two holders of one value that differ only by case are two holders; a case-folding dedupe would see one.
new ( "batch-holders-by-case" , new [ ] { Open ( ) , Commit ( HolderCaseRecords ) } , HolderCaseRefusal ) ,
new ( "load-holders-by-case" , new [ ] { Open ( "holder-case-store.cc" ) } , HolderCaseRefusal ) ,
// Row 25: the held `a` is replaced only by the exact key `a`, never by `A`.
new ( "batch-replaced-by-case" , new [ ] { Open ( ) , Commit ( Items_ ( ( "a" , "S" ) ) ) , Commit ( Items_ ( ( "A" , "S" ) ) ) } , HolderCaseRefusal ) ,
2026-11-11 11:42:27 +00:00
// Rows 26-31: identity is exact. One value in two schemas is two groups; values differing in case, trailing or leading space, normalisation form or width are two values; keys differing only by normalisation form are two holders.
new ( "batch-value-in-two-schemas" , new [ ] { Open ( ) , Commit ( CrossSchemaRecords ) } , null ) ,
new ( "load-value-in-two-schemas" , new [ ] { Open ( "cross-schema-store.cc" ) } , null ) ,
new ( "batch-distinct-values" , new [ ] { Open ( ) , Commit ( DistinctValueRecords ) } , null ) ,
new ( "load-distinct-values" , new [ ] { Open ( "distinct-values-store.cc" ) } , null ) ,
new ( "batch-keys-by-normal-form" , new [ ] { Open ( ) , Commit ( NormalKeyRecords ) } , NormalKeyRefusal ) ,
new ( "load-keys-by-normal-form" , new [ ] { Open ( "normal-keys-store.cc" ) } , NormalKeyRefusal ) ,
// Rows 32-33: one landing record sharing two unique aliases with a held one; the least alias by code unit is named ('Alt' before 'Sku'; 'Sku' before 'alt').
new ( "put-onto-a-held-pair" , new [ ] { Open ( ) , Commit ( new Rec ( "m" , Pair , "S" , "X" ) ) , Commit ( new Rec ( "a" , Pair , "S" , "X" ) ) } ,
new Refusal ( "a" , "m" , Pair , "Alt" ) ) ,
new ( "put-onto-a-held-alias-pair" , new [ ] { Open ( ) , Commit ( new Rec ( "m" , Alias , "S" , "X" ) ) , Commit ( new Rec ( "a" , Alias , "S" , "X" ) ) } ,
new Refusal ( "a" , "m" , Alias , "Sku" ) ) ,
2026-11-11 13:41:10 +00:00
// Rows 34-35: three holders of one value; the two least by code unit are named, which no NFC, NFD or culture sort of the keys yields.
new ( "batch-three-holders-normal-form" , new [ ] { Open ( ) , Commit ( ThreeHolderRecords ) } , ThreeHolderRefusal ) ,
new ( "load-three-holders-normal-form" , new [ ] { Open ( "normal-three-store.cc" ) } , ThreeHolderRefusal ) ,
// Rows 36-37: two records violating two aliases, U+FF21 and U+10400; aliases compare by code unit, so U+10400 is named.
new ( "batch-alias-code-unit" , new [ ] { Open ( ) , Commit ( WideRecords ) } , WideRefusal ) ,
new ( "load-alias-code-unit" , new [ ] { Open ( "wide-store.cc" ) } , WideRefusal ) ,
2026-11-11 10:05:32 +00:00
} ;
private static byte [ ] RefusalStoreBytes ( Rec [ ] records )
{
var directory = Path . Combine ( Path . GetTempPath ( ) , $"cultlib-uidxr-{Guid.NewGuid():N}" ) ;
Directory . CreateDirectory ( directory ) ;
try
{
var path = Path . Combine ( directory , "store.cc" ) ;
var roles = records . Select ( record = > SchemaNamed ( record . Schema ) . Role ) . Distinct ( ) . ToArray ( ) ;
using ( var cache = CultCacheMessagePack . Create ( path , new CultCacheOpenOptions
{
Registry = CultDocumentRegistry . ForTypes ( roles ) , StoreFlushOnDispose = true
} ) )
{
cache . Clock = ( ) = > StoredAt ;
foreach ( var record in records )
{
var schema = SchemaNamed ( record . Schema ) ;
cache . UpsertAsync ( schema . Role , EmittedDocumentTypes . New ( schema . Role , ( "Sku" , record . Sku ) , ( schema . SecondField , record . Second ) ) , new CultRecordKey ( record . Key ) )
. GetAwaiter ( ) . GetResult ( ) ;
}
cache . FlushAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
}
return File . ReadAllBytes ( path ) ;
}
finally
{
Directory . Delete ( directory , recursive : true ) ;
}
}
private static byte [ ] RefusalsFile ( )
{
var buffer = new ArrayBufferWriter < byte > ( ) ;
var writer = new MessagePackWriter ( buffer ) ;
writer . WriteArrayHeader ( Cases . Length ) ;
foreach ( var row in Cases )
{
writer . WriteArrayHeader ( 3 ) ;
writer . Write ( row . Label ) ;
writer . WriteArrayHeader ( row . Ops . Length ) ;
foreach ( var op in row . Ops )
{
writer . WriteArrayHeader ( 2 ) ;
writer . Write ( op . Kind ) ;
if ( op . Records ! = null )
{
writer . WriteArrayHeader ( op . Records . Length ) ;
foreach ( var record in op . Records )
{
writer . WriteArrayHeader ( 4 ) ;
writer . Write ( record . Key ) ;
writer . Write ( record . Schema ) ;
writer . Write ( record . Sku ) ;
writer . Write ( record . Second ) ;
}
}
else if ( op . File = = null ) writer . WriteNil ( ) ;
else writer . Write ( op . File ) ;
}
if ( row . Refusal = = null ) writer . WriteNil ( ) ;
else
{
writer . WriteArrayHeader ( 4 ) ;
writer . Write ( row . Refusal . K1 ) ;
writer . Write ( row . Refusal . K2 ) ;
writer . Write ( row . Refusal . Schema ) ;
writer . Write ( row . Refusal . Alias ) ;
}
}
writer . Flush ( ) ;
return buffer . WrittenSpan . ToArray ( ) ;
}
// Performs one op of a row against the open cache; the cache's own file is the only state it leaves behind.
private static void Perform ( ref CultCache ? cache , string path , Op op )
{
var indexTypes = Schemas . Select ( schema = > schema . Index ) . ToArray ( ) ;
switch ( op . Kind )
{
case "open" :
if ( op . File ! = null ) File . WriteAllBytes ( path , Committed ( op . File ) ) ;
cache = CultCacheMessagePack . Create ( path , new CultCacheOpenOptions { Registry = CultDocumentRegistry . ForTypes ( indexTypes ) } ) ;
break ;
case "pull" :
File . WriteAllBytes ( path , Committed ( op . File ! ) ) ;
cache ! . PullAllBackingStoresAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
break ;
case "commit" :
cache ! . Commit ( batch = >
{
foreach ( var record in op . Records ! )
{
var schema = SchemaNamed ( record . Schema ) ;
batch . Upsert ( schema . Index , EmittedDocumentTypes . New ( schema . Index , ( "Sku" , record . Sku ) , ( schema . SecondField , record . Second ) ) , new CultRecordKey ( record . Key ) ) ;
}
} ) ;
break ;
default :
throw new InvalidOperationException ( op . Kind ) ;
}
}
[Test]
public void RefusalsHoldTheRule ( )
{
Check ( "refusals.msgpack" , RefusalsFile ( ) ) ;
foreach ( var ( file , records ) in RefusalStores )
Check ( file , RefusalStoreBytes ( records ) ) ;
// The committed file is what is walked, not the in-memory table.
var reader = new MessagePackReader ( Committed ( "refusals.msgpack" ) ) ;
var count = reader . ReadArrayHeader ( ) ;
Assert . That ( count , Is . EqualTo ( Cases . Length ) ) ;
var walked = 0 ;
2026-11-11 10:47:13 +00:00
var failed = new List < string > ( ) ;
2026-11-11 10:05:32 +00:00
for ( var i = 0 ; i < count ; i + + )
{
Assert . That ( reader . ReadArrayHeader ( ) , Is . EqualTo ( 3 ) ) ;
var label = reader . ReadString ( ) ! ;
var ops = new List < Op > ( ) ;
for ( var n = reader . ReadArrayHeader ( ) ; n > 0 ; n - - )
{
Assert . That ( reader . ReadArrayHeader ( ) , Is . EqualTo ( 2 ) ) ;
var kind = reader . ReadString ( ) ! ;
if ( kind = = "commit" )
{
var records = new List < Rec > ( ) ;
for ( var m = reader . ReadArrayHeader ( ) ; m > 0 ; m - - )
{
Assert . That ( reader . ReadArrayHeader ( ) , Is . EqualTo ( 4 ) ) ;
records . Add ( new Rec ( reader . ReadString ( ) ! , reader . ReadString ( ) ! , reader . ReadString ( ) ! , reader . ReadString ( ) ! ) ) ;
}
ops . Add ( new Op ( kind , Records : records . ToArray ( ) ) ) ;
}
else ops . Add ( new Op ( kind , NextText ( ref reader ) ) ) ;
}
Refusal ? refusal = null ;
if ( ! reader . TryReadNil ( ) )
{
Assert . That ( reader . ReadArrayHeader ( ) , Is . EqualTo ( 4 ) ) ;
refusal = new Refusal ( reader . ReadString ( ) ! , reader . ReadString ( ) ! , reader . ReadString ( ) ! , reader . ReadString ( ) ! ) ;
}
var directory = Path . Combine ( Path . GetTempPath ( ) , $"cultlib-uidxf-{Guid.NewGuid():N}" ) ;
Directory . CreateDirectory ( directory ) ;
CultCache ? cache = null ;
try
{
var path = Path . Combine ( directory , "store.cc" ) ;
foreach ( var op in ops . Take ( ops . Count - 1 ) )
Perform ( ref cache , path , op ) ;
var last = ops [ ^ 1 ] ;
if ( refusal = = null )
{
Assert . DoesNotThrow ( ( ) = > Perform ( ref cache , path , last ) , label ) ;
}
else
{
cache ? . FlushAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
var before = last . Kind = = "open" & & last . File ! = null ? Committed ( last . File )
: last . Kind = = "pull" ? Committed ( last . File ! )
: File . Exists ( path ) ? File . ReadAllBytes ( path ) : Array . Empty < byte > ( ) ;
CultCache ? attempt = cache ;
var refused = Assert . Throws < InvalidOperationException > ( ( ) = > Perform ( ref attempt , path , last ) , label ) ! ;
Assert . That ( refused . Message , Is . EqualTo (
$"Records {refusal.K1} and {refusal.K2} ({refusal.Schema}) both hold unique index '{refusal.Alias}'; an index value names one record." ) , label ) ;
if ( last . Kind ! = "open" ) cache ? . FlushAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
Assert . That ( File . Exists ( path ) ? File . ReadAllBytes ( path ) : Array . Empty < byte > ( ) , Is . EqualTo ( before ) , $"{label}: the file is untouched" ) ;
}
walked + + ;
}
2026-11-11 10:47:13 +00:00
catch ( AssertionException failure )
{
failed . Add ( $"{label}: {failure.Message}" ) ;
}
2026-11-11 10:05:32 +00:00
finally
{
cache ? . Dispose ( ) ;
Directory . Delete ( directory , recursive : true ) ;
}
}
2026-11-11 10:47:13 +00:00
Assert . That ( failed , Is . Empty , "rows that failed" ) ;
2026-11-11 13:41:10 +00:00
Assert . That ( walked , Is . EqualTo ( 37 ) ) ;
2026-11-11 10:05:32 +00:00
}
2026-11-11 05:13:08 +00:00
}
}