using System.Diagnostics; using System.Reflection; using game.PlayerDataStorage; using Newtonsoft.Json.Linq; if (args.Length > 0) return Child(args); var folder = Path.GetFullPath("CodexOutput/upserver/KVSyncTests/artifacts/" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss") + "-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(folder); var passed = 0; var failed = 0; void Test(string name, Action test) { try { test(); passed++; Console.WriteLine("PASS " + name); } catch (Exception e) { failed++; Console.WriteLine("FAIL " + name + ": " + e.GetType().Name + " " + e.Message); } } Fixture New() => new Fixture(Path.Combine(folder, Guid.NewGuid().ToString("N"))); void A(bool ok, string message = "Invariant failed") { if (!ok) throw new InvalidOperationException(message); } void Ok(PlayerKVStateResult r) => A(r.Success, "State result " + r.Error); PlayerKVBatch Start(PlayerKVSyncState state, long limit = long.MaxValue) { Ok(state.BeginBatch(limit, out var batch)); A(batch != null, "No batch"); return batch; } void Ready(Fixture f) { Ok(f.State.Reconcile(Data("k", "base"))); } Test("Fresh cache is not ready; failed reads cannot create an empty save", () => { using var f = New(); A(f.State.Status == PlayerKVSyncStatus.NotLoaded); A(!f.State.Update("k", "v").Success); Ok(f.State.RecordReadFailure()); A(f.State.Status == PlayerKVSyncStatus.ReadFailed && !File.Exists(f.Main)); try { f.State.ReadLocal(); throw new Exception("Read exposed"); } catch (InvalidOperationException) { } A(f.State.Reconcile(null).Error == PlayerKVChangeError.Validation); Ok(f.State.Reconcile(Data())); A(f.State.Status == PlayerKVSyncStatus.Ready && f.State.ReadLocal().Count == 0); }); Test("API normalization and account/path/environment namespaces", () => { using var f = New(); var aliases = PlayerKVSyncState.Open(f.Root, new Uri("HTTP://EXAMPLE.TEST:80/api"), "00101"); A(aliases.Error == PlayerKVOpenError.Busy); foreach (var (uri,id) in new[] { ("http://example.test/api/", "102"), ("http://example.test/API/", "101"), ("http://other.test/api/", "101"), ("http://example.test:8080/api/", "101") }) { using var s = PlayerKVSyncState.Open(f.Root, new Uri(uri), id).State; A(s != null); Ok(s.Reconcile(Data())); } }); foreach (var id in new[] { "", "0", "-1", "../101", "1e2", "9223372036854775808", " 101" }) Test("Reject invalid ID " + id, () => A(PlayerKVSyncState.Open(folder, Fixture.Api, id).Error == PlayerKVOpenError.Validation)); foreach (var uri in new[] { "file:///tmp", "http://example.test/?x=1", "http://u:p@example.test/", "http://example.test/#part" }) Test("Reject invalid API form", () => A(PlayerKVSyncState.Open(folder, new Uri(uri), "101").Error == PlayerKVOpenError.Validation)); Test("Reject relative cache root", () => A(PlayerKVSyncState.Open("relative", Fixture.Api, "101").Error == PlayerKVOpenError.Validation)); foreach (var path in new[] { @"C:relative", @"\relative" }) Test("Reject Windows rooted but non-absolute cache path", () => A(PlayerKVSyncState.Open(path, Fixture.Api, "101").Error == PlayerKVOpenError.Validation)); Test("Real file lease excludes another process and is reusable after close", () => { using var f = New(); A(RunChild("--lock", f.Root) == 21); f.State.Dispose(); A(RunChild("--lock", f.Root) == 0); f.Reopen(); A(f.State != null); }); Test("Empty string/delete/localOnly survive disk reload with distinct meaning", () => { using var f = New(); Ready(f); Ok(f.State.Update("empty", "")); Ok(f.State.Update("local", "private", false)); Ok(f.State.Delete("k")); var local = f.State.ReadLocal(); A(local["empty"] == "" && local["local"] == "private" && !local.ContainsKey("k")); f.Reopen(); A(f.State.Status == PlayerKVSyncStatus.NotLoaded); Ready(f); A(f.State.ReadLocal()["local"] == "private" && f.State.Inspect().Pending.Count == 2); }); Test("remoteStore=false retains prior remote intent; remote Set and Delete clear overlay", () => { using var f = New(); Ready(f); Ok(f.State.Update("k", "v1")); Ok(f.State.Update("k", "local", false)); var b = Start(f.State); A(b.Items.Single().Target.Value == "v1"); Ok(f.State.ConfirmBatch(b.Id)); A(f.State.ReadLocal()["k"] == "local" && f.State.Inspect().Pending.Count == 0); Ok(f.State.Update("k", "v2")); A(f.State.ReadLocal()["k"] == "v2" && f.State.Inspect().LocalOnly.Count == 0); Ok(f.State.Update("k", "local", false)); Ok(f.State.Delete("k")); A(!f.State.ReadLocal().ContainsKey("k")); Ok(f.State.ConfirmBatch(Start(f.State).Id)); A(!f.State.ReadLocal().ContainsKey("k")); }); Test("v1 in flight then v2: confirm only v1 and advance v2 base", () => { using var f = New(); Ready(f); var v1 = f.State.Update("k", "v1"); Ok(v1); var batch = Start(f.State); var v2 = f.State.Update("k", "v2"); Ok(v2); A(v2.AttemptSequence > v1.AttemptSequence && batch.Items[0].Target.Value == "v1"); Ok(f.State.ConfirmBatch(batch.Id)); var item = f.State.Inspect().Pending["k"]; A(item.Sequence == v2.AttemptSequence && item.Target.Value == "v2" && item.BaseValue.Value == "v1"); f.Reopen(); Ok(f.State.Reconcile(Data("k", "v1"))); A(f.State.ReadLocal()["k"] == "v2"); }); Test("Set in flight then Delete; delete in flight then new Set", () => { using var f = New(); Ready(f); Ok(f.State.Update("k", "v1")); var set = Start(f.State); Ok(f.State.Delete("k")); Ok(f.State.ConfirmBatch(set.Id)); var del = Start(f.State); A(del.IsDelete && del.Items[0].BaseValue.Value == "v1"); Ok(f.State.Update("k", "v3")); Ok(f.State.ConfirmBatch(del.Id)); A(!f.State.Inspect().Pending["k"].BaseValue.Exists && f.State.ReadLocal()["k"] == "v3"); }); Test("101 unique keys persist and drain in batches of at most 10", () => { using var f = New(); Ok(f.State.Reconcile(Data())); for (var i=0;i<101;i++) Ok(f.State.Update("key"+i, "v"+i)); f.Reopen(); Ok(f.State.Reconcile(Data())); int total=0,batches=0; while(f.State.Inspect().Pending.Count != 0) { var b=Start(f.State); A(b.Items.Count<=10);total+=b.Items.Count;batches++;Ok(f.State.ConfirmBatch(b.Id)); } A(total==101 && batches==11 && f.State.ReadLocal().Count==101); }); Test("Batch respects consecutive operation kind, not only matching kind", () => { using var f=New();Ready(f);Ok(f.State.Delete("a"));Ok(f.State.Update("b","v"));Ok(f.State.Delete("c")); var b=Start(f.State);A(b.IsDelete && b.Items.Count==1 && b.Items[0].Key=="a");Ok(f.State.ConfirmBatch(b.Id)); b=Start(f.State);A(!b.IsDelete && b.Items.Count==1 && b.Items[0].Key=="b"); }); Test("Watermark excludes newer edits and superseded unsent sequence", () => { using var f=New();Ready(f);var old=f.State.Update("k","v1");Ok(old);Ok(f.State.Update("k","v2")); Ok(f.State.BeginBatch(old.AttemptSequence,out var none));A(none==null); A(Start(f.State).Items[0].Sequence>old.AttemptSequence); }); foreach (var observed in new[] { "base", "local", "other" }) Test("Three-way pending merge " + observed, () => { using var f=New();Ready(f);Ok(f.State.Update("k","local"));f.Reopen();Ok(f.State.Reconcile(Data("k",observed))); A(f.State.Status==(observed=="other"?PlayerKVSyncStatus.Conflict:PlayerKVSyncStatus.Ready)); A(f.State.Inspect().Pending.Count==(observed=="local"?0:1)); if(observed=="other") { A(!f.State.Update("x","v").Success);A(!f.State.BeginBatch(long.MaxValue,out _).Success); A(f.State.Inspect().Pending["k"].BaseValue.Value=="base");Ok(f.State.Reconcile(Data("k","base")));A(f.State.Status==PlayerKVSyncStatus.Ready); } }); Test("Missing baseline, empty string and delete use existence, not null substitution", () => { using var f=New();Ok(f.State.Reconcile(Data()));Ok(f.State.Update("new",""));f.Reopen();Ok(f.State.Reconcile(Data())); A(f.State.Inspect().Pending.Count==1);Ok(f.State.Reconcile(Data("new","")));A(f.State.Inspect().Pending.Count==0); Ok(f.State.Delete("new"));f.Reopen();Ok(f.State.Reconcile(Data()));A(f.State.Inspect().Pending.Count==0); }); Test("Mixed merge conflict blocks entire instance while preserving local intention", () => { using var f=New();Ok(f.State.Reconcile(Data("a","base","b","base")));Ok(f.State.Update("a","A"));Ok(f.State.Update("b","B")); f.Reopen();Ok(f.State.Reconcile(Data("a","A","b","other")));A(f.State.Status==PlayerKVSyncStatus.Conflict); A(f.State.Inspect().Pending.Count==1 && f.State.Inspect().ConflictObservations.ContainsKey("b"));A(!f.State.BeginBatch(long.MaxValue,out _).Success); }); Test("JSON value equality is byte-string equality", () => { using var f=New();Ok(f.State.Reconcile(Data("k","{}")));Ok(f.State.Update("k","{\"v\":1}"));f.Reopen(); Ok(f.State.Reconcile(Data("k","{ \"v\": 1 }")));A(f.State.Status==PlayerKVSyncStatus.Conflict); }); foreach(var observation in new[] { "base", "v1", "other" }) Test("Unknown write survives reopen/readback without unlocking " + observation, () => { using var f=New();Ready(f);Ok(f.State.Update("k","v1"));var b=Start(f.State);Ok(f.State.Update("k","v2")); Ok(f.State.FailBatch(b.Id,true,true));A(f.State.Status==PlayerKVSyncStatus.UnknownOutcome && f.State.Inspect().AuthenticationInvalid); f.Reopen();Ok(f.State.Reconcile(Data("k",observation)));A(f.State.Status==PlayerKVSyncStatus.UnknownOutcome); A(f.State.Inspect().UnknownBatch.Items[0].Target.Value=="v1" && f.State.Inspect().Pending["k"].Target.Value=="v2"); A(!f.State.BeginBatch(long.MaxValue,out _).Success && !f.State.Update("k","v3").Success);f.Reopen();A(f.State.Status==PlayerKVSyncStatus.UnknownOutcome); }); Test("Process exit with write-ahead batch recovers as unknown", () => { using var f=New();Ready(f);f.State.Dispose();A(RunChild("--active",f.Root)==0);f.Reopen(); A(f.State.Status==PlayerKVSyncStatus.UnknownOutcome && f.State.Inspect().UnknownBatch!=null); }); Test("Positive no-dispatch evidence retains pending and permits fresh read retry", () => { using var f=New();Ready(f);Ok(f.State.Update("k","v1"));var b=Start(f.State);Ok(f.State.FailBatch(b.Id,false)); A(f.State.Status==PlayerKVSyncStatus.ReadFailed && f.State.Inspect().Pending.Count==1 && f.State.Inspect().UnknownBatch==null); Ok(f.State.Reconcile(Data("k","base")));A(f.State.Status==PlayerKVSyncStatus.Ready); }); Test("Unauthorized stops same instance; new instance requires new successful read", () => { using var f=New();Ready(f);Ok(f.State.RecordReadFailure(true));A(f.State.Status==PlayerKVSyncStatus.Unauthorized); A(!f.State.Reconcile(Data()).Success);f.Reopen();A(f.State.Status==PlayerKVSyncStatus.NotLoaded);Ready(f); }); foreach(var stage in Enum.GetValues()) Test("Edit disk fault " + stage, () => { using var f=New();Ready(f);var before=f.State.Inspect();f.Arm(stage);var r=f.State.Update("k","new"); A(!r.Success && r.Error==PlayerKVChangeError.StorageError && r.AttemptSequence==before.NextSequence && r.Revision==before.Revision+1); A(r.LocalCommitMayHaveSucceeded==(stage==KVCacheWriteStage.AfterReplace)); A(f.State.Inspect().Revision==before.Revision && f.State.Inspect().Pending.Count==0 && f.State.Status==PlayerKVSyncStatus.StorageError); A(!f.State.Update("later","no").Success);f.Disarm();f.Reopen(); A(f.State.Inspect().Pending.Count==(stage==KVCacheWriteStage.AfterReplace?1:0)); }); Test("BeginBatch disk failure never exposes sendable batch", () => { using var f=New();Ready(f);Ok(f.State.Update("k","v1"));f.Arm(KVCacheWriteStage.AfterReplace); var r=f.State.BeginBatch(long.MaxValue,out var batch);A(!r.Success && batch==null && r.LocalCommitMayHaveSucceeded); f.Disarm();f.Reopen();A(f.State.Status==PlayerKVSyncStatus.UnknownOutcome); }); foreach(var stage in new[] { KVCacheWriteStage.BeforeReplace, KVCacheWriteStage.AfterReplace }) Test("HTTP-confirmation disk failure " + stage, () => { using var f=New();Ready(f);Ok(f.State.Update("k","v1"));var batch=Start(f.State);f.Arm(stage); var r=f.State.ConfirmBatch(batch.Id);A(!r.Success && f.State.Inspect().ActiveBatch!=null);f.Disarm();f.Reopen(); A(stage==KVCacheWriteStage.AfterReplace ? f.State.Inspect().Pending.Count==0 && f.State.Status==PlayerKVSyncStatus.NotLoaded : f.State.Status==PlayerKVSyncStatus.UnknownOutcome); }); foreach(var stage in Enum.GetValues().Where(s=>s!=KVCacheWriteStage.BeforeWrite)) Test("Process termination at disk boundary " + stage, () => { using var f=New();Ready(f);f.State.Dispose();A(RunChild("--crash",f.Root,stage.ToString())==17);f.Reopen(); A(f.State.Inspect().Pending.Count==(stage==KVCacheWriteStage.AfterReplace?1:0)); }); Test("Main corruption never falls back to older valid backup", () => { using var f=New();Ready(f);Ok(f.State.Update("k","new"));f.State.Dispose();A(File.Exists(f.Main+".bak")); File.WriteAllText(f.Main,"broken");A(f.TryReopen().Error==PlayerKVOpenError.RecoveryRequired);A(File.ReadAllText(f.Main)=="broken"); }); Test("Valid main wins over broken backup and stale temporary file", () => { using var f=New();Ready(f);Ok(f.State.Update("k","new"));f.State.Dispose();File.WriteAllText(f.Main+".bak","broken");File.WriteAllText(f.Main+".tmp","broken"); f.Reopen();A(f.State.Inspect().Pending["k"].Target.Value=="new"); }); foreach(var suffix in new[] { ".bak", ".tmp" }) Test("Missing main with orphan " + suffix + " requires recovery", () => { using var f=New();f.State.Dispose();File.WriteAllText(f.Main+suffix,"orphan");A(f.TryReopen().Error==PlayerKVOpenError.RecoveryRequired); }); var mutations = new (string,Action)[] { ("future format", j=>j["formatVersion"]=2), ("wrong account",j=>j["playerId"]="102"), ("wrong API",j=>j["apiBaseUri"]="http://other.test/"), ("missing field",j=>j.Remove("pending")), ("bad bool",j=>j["hasBaseline"]="true"), ("bad revision",j=>j["revision"]=-1), ("bad sequence",j=>j["nextSequence"]=0), ("pending null",j=>j["pending"]=null), ("null value",j=>j["baseline"]["k"]=null), ("bad key",j=>j["baseline"][new string('x',129)]="v"), ("duplicate sequence",j=>j["pending"][1]["sequence"]=j["pending"][0]["sequence"]!.DeepClone()), ("sequence beyond counter",j=>j["pending"][0]["sequence"]=j["nextSequence"]!.DeepClone()), ("impossible missing value",j=>j["pending"][0]["target"]["exists"]=false), ("no baseline with pending",j=>j["hasBaseline"]=false), ("invalid conflict",j=>j["conflictObservations"]["absent"]=JObject.Parse("{\"exists\":false,\"value\":null}")) }; foreach(var (name,mutate) in mutations) Test("Reject damaged cache " + name, () => { using var f=New();Ready(f);Ok(f.State.Update("a","1"));Ok(f.State.Update("b","2"));f.State.Dispose();var j=JObject.Parse(File.ReadAllText(f.Main));mutate(j);File.WriteAllText(f.Main,j.ToString()); A(f.TryReopen().Error==PlayerKVOpenError.RecoveryRequired); }); foreach(var text in new[] { "{}", "[]", "{\"formatVersion\":1,\"formatVersion\":1}", "{\"revision\":92233720368547758081234}" }) Test("Reject malformed snapshot shape", () => { using var f=New();f.State.Dispose();File.WriteAllText(f.Main,text);A(f.TryReopen().Error==PlayerKVOpenError.RecoveryRequired); }); var batchMutations = new (string,Action)[] { ("empty batch",j=>j["activeBatch"]["items"]=new JArray()), ("missing pending",j=>j["pending"]=new JArray()), ("target mismatch",j=>j["activeBatch"]["items"][0]["target"]["value"]="different"), ("base mismatch",j=>j["activeBatch"]["items"][0]["base"]["value"]="different"), ("bad batch id",j=>j["activeBatch"]["id"]="not-a-batch"), ("both active and unknown",j=>j["unknownBatch"]=j["activeBatch"]!.DeepClone()), ("duplicate active key",j=>((JArray)j["activeBatch"]["items"]).Add(j["activeBatch"]["items"][0]!.DeepClone())), ("oversized counter",j=>j["nextSequence"]=JToken.Parse("92233720368547758081234")) }; foreach(var (name,mutate) in batchMutations) Test("Reject inconsistent write-ahead record " + name, () => { using var f=New();Ready(f);Ok(f.State.Update("k","new"));Start(f.State);f.State.Dispose(); var j=JObject.Parse(File.ReadAllText(f.Main));mutate(j);File.WriteAllText(f.Main,j.ToString()); A(f.TryReopen().Error==PlayerKVOpenError.RecoveryRequired); }); Test("Oversized disk snapshot is rejected before parsing", () => { using var f=New();f.State.Dispose();using(var stream=File.Create(f.Main))stream.SetLength(64L*1024*1024+1); A(f.TryReopen().Error==PlayerKVOpenError.RecoveryRequired); }); Test("Concurrent edits receive distinct durable sequences without lost keys", () => { using var f=New();Ok(f.State.Reconcile(Data())); var results=new System.Collections.Concurrent.ConcurrentBag(); Parallel.For(0,24,i=>results.Add(f.State.Update("parallel"+i,"v"+i))); A(results.All(r=>r.Success) && results.Select(r=>r.AttemptSequence).Distinct().Count()==24 && f.State.Inspect().Pending.Count==24); f.Reopen();A(f.State.Inspect().Pending.Count==24); }); Test("Sequence overflow explicitly refuses edit without corrupting old state", () => { using var f=New();Ready(f);f.State.Dispose();var j=JObject.Parse(File.ReadAllText(f.Main));j["nextSequence"]=long.MaxValue;File.WriteAllText(f.Main,j.ToString()); f.Reopen();Ready(f);A(f.State.Update("k","new").Error==PlayerKVChangeError.Validation && f.State.Inspect().Pending.Count==0); }); Test("Revision overflow blocks persistence without clearing pending", () => { using var f=New();Ready(f);Ok(f.State.Update("k","new"));f.State.Dispose();var j=JObject.Parse(File.ReadAllText(f.Main));j["revision"]=long.MaxValue;File.WriteAllText(f.Main,j.ToString()); f.Reopen();A(!f.State.Reconcile(Data("k","base")).Success && f.State.Inspect().Pending.Count==1 && f.State.Status==PlayerKVSyncStatus.StorageError); }); Test("Invalid keys/values do not advance revision or touch disk", () => { using var f=New();Ready(f);var revision=f.State.Inspect().Revision; foreach(var key in new[] { "", null, new string('k',129) }) A(f.State.Update(key,"v").Error==PlayerKVChangeError.Validation); A(f.State.Update("k",null).Error==PlayerKVChangeError.Validation && f.State.Inspect().Revision==revision); Ok(f.State.Update(new string('k',128),"")); }); Test("Closed state cannot edit, acknowledge or release another owner's lease", () => { using var f=New();Ready(f);Ok(f.State.Update("k","new"));var batch=Start(f.State);var old=f.State;old.Dispose();f.Reopen();old.Dispose(); A(!old.ConfirmBatch(batch.Id).Success && !old.Update("k","later").Success); A(PlayerKVSyncState.Open(f.Root,Fixture.Api,"101").Error==PlayerKVOpenError.Busy && f.State.Status==PlayerKVSyncStatus.UnknownOutcome); }); Test("Diagnostics/local data and batch items are immutable snapshots", () => { using var f=New();Ready(f);Ok(f.State.Update("k","v1"));var view=f.State.Inspect();var batch=Start(f.State); Ok(f.State.Update("k","v2"));A(view.Pending["k"].Target.Value=="v1" && batch.Items[0].Target.Value=="v1"); try { ((IDictionary)f.State.ReadLocal()).Add("x","y");throw new Exception("Mutable"); } catch(NotSupportedException) { } }); Console.WriteLine($"Passed {passed}; Failed {failed}. Synthetic fixtures only: {Path.GetFileName(folder)}"); return failed==0?0:1; static Dictionary Data(params string[] pairs) { var d=new Dictionary(StringComparer.Ordinal);for(int i=0;i { if(armed && args[0]=="--crash" && stage.ToString()==args[2])Environment.Exit(17); }); if(args[0]=="--lock") { result.State?.Dispose();return result.Error==PlayerKVOpenError.Busy?21:result.Success?0:22; } if(!result.Success)return 23; var state=result.State;if(!state.Reconcile(Data("k","base")).Success)return 24; armed=true;if(!state.Update("k","child").Success)return 25; if(args[0]=="--active"){if(!state.BeginBatch(long.MaxValue,out var batch).Success||batch==null)return 26;Environment.Exit(0);} return 27; } sealed class Fixture : IDisposable { internal static readonly Uri Api=new Uri("http://example.test/api/"); public string Root { get; } public string Main => Path.Combine(new KVIdentity(Root,Api,"101").DirectoryPath,"state.json"); public PlayerKVSyncState State { get; private set; } KVCacheWriteStage? failAt; public Fixture(string root){Root=root;Reopen();} public void Arm(KVCacheWriteStage stage)=>failAt=stage; public void Disarm()=>failAt=null; public PlayerKVOpenResult TryReopen(){State?.Dispose();var r=PlayerKVSyncState.Open(Root,Api,"101",stage=>{if(stage==failAt)throw new IOException("Injected disk failure");});State=r.State;return r;} public void Reopen(){var r=TryReopen();if(!r.Success)throw new InvalidOperationException("Open "+r.Error);} public void Dispose()=>State?.Dispose(); }