test(upserver): 归档存档迁移与玩法闭环回归工具
This commit is contained in:
277
CodexOutput/upserver/KVSyncTests/Program.cs
Normal file
277
CodexOutput/upserver/KVSyncTests/Program.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
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<KVCacheWriteStage>())
|
||||
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<KVCacheWriteStage>().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<JObject>)[] {
|
||||
("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<JObject>)[] {
|
||||
("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<PlayerKVStateResult>();
|
||||
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<string,string>)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<string,string> Data(params string[] pairs)
|
||||
{ var d=new Dictionary<string,string>(StringComparer.Ordinal);for(int i=0;i<pairs.Length;i+=2)d.Add(pairs[i],pairs[i+1]);return d; }
|
||||
static int RunChild(params string[] args)
|
||||
{
|
||||
var start=new ProcessStartInfo("dotnet") { UseShellExecute=false,CreateNoWindow=true,RedirectStandardOutput=true,RedirectStandardError=true };
|
||||
start.ArgumentList.Add(Assembly.GetExecutingAssembly().Location);foreach(var arg in args)start.ArgumentList.Add(arg);
|
||||
using var process=Process.Start(start);
|
||||
if(!process.WaitForExit(10000)){process.Kill();throw new TimeoutException("Child process");}
|
||||
return process.ExitCode;
|
||||
}
|
||||
static int Child(string[] args)
|
||||
{
|
||||
bool armed=false;
|
||||
var result=PlayerKVSyncState.Open(args[1],Fixture.Api,"101",stage=> { 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();
|
||||
}
|
||||
Reference in New Issue
Block a user