using game.PlayerDataStorage; static partial class Tests { static PlayerKVSchemaResult Aggregate(Dictionary raw=null,bool fresh=false,bool current=true,string version="synthetic-v1") { var prepared=new PlayerKVLoginPrepareResult(PlayerKVLoginPrepareError.None,"101",7,data:raw ?? ModernRaw()); var account=new PlayerKVAccountDescriptor("101",7,17,fresh,SchemaSnapshot().NowUtc,SchemaSnapshot().NowUtc); return PlayerKVSaveSchema.Decode(prepared,account,"101",7,current,SchemaSnapshot(),version,Identity()); } static async Task RunAggregateTests() { await Run("transaction-every-reset-replace-revocation-boundary",()=> { foreach(int boundary in Enumerable.Range(1,31)) foreach(int mode in new[]{0,1,2,3}) { bool current=true;int notified=0;var values=new int[31]; var modules=Enumerable.Range(1,31).Select(id=>(IPlayerKVLoadModule)new PlayerKVLoadModule(id, ()=>{values[id-1]=0;if(id==boundary && mode==0)throw new Exception();if(id==boundary && mode==1)current=false;}, _=>{values[id-1]=id;if(id==boundary && mode==2)current=false;if(id==boundary && mode==3)throw new Exception();})); var transaction=new PlayerKVGameLoadTransaction(modules,Environment.CurrentManagedThreadId,()=>{}); var result=transaction.Apply(Aggregate().Data,()=>current,()=>true,()=>notified++); Check(result==(mode==0?PlayerKVApplyError.Poisoned:mode==3?PlayerKVApplyError.Apply:PlayerKVApplyError.Stale),"failure classification at every module"); Check(notified==0 && values.All(v=>v==0) && transaction.AppliedModules==0,"no publication, all reset"); if(mode==0)Check(transaction.Apply(Aggregate().Data,()=>true,()=>true,()=>{})==PlayerKVApplyError.Poisoned,"failed reset cannot reuse container"); } return Task.CompletedTask; }); await Run("aggregate-31-source-priority-no-partial-state",()=> { var raw=ModernRaw(); raw["extension"]="uninterpreted"; var a=Aggregate(raw); Check(a.Verdict==PlayerKVSchemaVerdict.Valid && a.Data.ModuleCount==31,"all31 decoded"); Check(a.Data.UnconsumedCount==1 && a.Data.UnconsumedTopLevelCount==1,"unknown top-level retained and counted"); var extended=ModernRaw(); var inner=Newtonsoft.Json.JsonConvert.DeserializeObject>(extended["NewPlayerDataDic"]); inner["unknown"]="opaque"; extended["NewPlayerDataDic"]=Newtonsoft.Json.JsonConvert.SerializeObject(inner);extended["Map_not_a_number"]="opaque"; var counted=Aggregate(extended).Data;Check(counted.UnconsumedModernCount==1 && counted.UnconsumedTopLevelCount==1,"unknown modern and nonmatching map prefixes counted separately"); raw.Clear(); Check(a.Data.Raw.Count==2 && a.Data.Module(1).Core==a.Data.Module(2),"source copied, consistent shared immutable core"); Check(Aggregate(new(),true).Verdict==PlayerKVSchemaVerdict.EmptyNew && Aggregate(new()).Verdict==PlayerKVSchemaVerdict.EmptyExisting,"S101 empty classification"); Check(Aggregate(new(),true,false).Verdict==PlayerKVSchemaVerdict.Stale,"stale before empty"); Check(Aggregate(version:"other").Verdict==PlayerKVSchemaVerdict.UnsupportedSchema,"version locked"); Check(Aggregate(new(){["FootPrint"]="1"}).Verdict==PlayerKVSchemaVerdict.MissingCore,"optional data not core"); raw=ModernRaw(); raw["EventPartnerGatherData"]="null"; raw["TapJoy_Diamond"]="bad"; var bad=Aggregate(raw); Check(bad.Data==null && bad.Diagnostics.Count(18,PlayerKVSchemaError.Malformed)==1 && bad.Diagnostics.Count(31,PlayerKVSchemaError.Malformed)==1,"last module visited despite earlier optional failure"); raw["NewPlayerDataDic"]="null"; Check(Aggregate(raw).Verdict==PlayerKVSchemaVerdict.Malformed,"outer parse failure"); return Task.CompletedTask; }); await Run("transaction-all-reset-before-replace-failure-clears",()=> { var order=new List(); var values=new int[31]; int failReplace=0,failReset=0,discard=0,notify=0; var modules=Enumerable.Range(1,31).Select(id=>(IPlayerKVLoadModule)new PlayerKVLoadModule(id, ()=>{order.Add("R"+id); values[id-1]=0; if(failReset==id) throw new Exception();}, _=>{order.Add("A"+id); values[id-1]=id; if(failReplace==id) throw new Exception();})).ToArray(); var transaction=new PlayerKVGameLoadTransaction(modules,Environment.CurrentManagedThreadId,()=>discard++); Check(transaction.Apply(Aggregate().Data,()=>true,()=>true,()=>notify++)==PlayerKVApplyError.None,"apply succeeded"); Check(order.Take(31).SequenceEqual(Enumerable.Range(1,31).Select(id=>"R"+id)) && order.Skip(31).SequenceEqual(Enumerable.Range(1,31).Select(id=>"A"+id)),"exact reset-then-replace sequence"); Check(transaction.AppliedModules==31 && notify==1 && discard==0,"notification only complete"); foreach(var failed in Enumerable.Range(1,31)) { failReplace=failed; order.Clear(); Check(transaction.Apply(Aggregate().Data,()=>true,()=>true,()=>notify++)==PlayerKVApplyError.Apply && values.All(v=>v==0),"partial materialization cleared"); Check(order.TakeLast(31).SequenceEqual(Enumerable.Range(1,31).Select(id=>"R"+id)),"all resets attempted after failure"); } failReplace=0; failReset=8; Check(transaction.Apply(Aggregate().Data,()=>true,()=>true,()=>notify++)==PlayerKVApplyError.Poisoned && transaction.Poisoned,"reset failure poisons"); failReset=0; Check(transaction.Apply(Aggregate().Data,()=>true,()=>true,()=>notify++)==PlayerKVApplyError.Poisoned,"poison cannot retry"); Check(notify==1,"no failure notifications"); return Task.CompletedTask; }); await Run("transaction-notification-stale-sideeffect-and-main-thread",async()=> { bool current=true,clean=true; int resets=0; var modules=Enumerable.Range(1,31).Select(id=>(IPlayerKVLoadModule)new PlayerKVLoadModule(id,()=>resets++,_=>{})).ToArray(); var transaction=new PlayerKVGameLoadTransaction(modules,Environment.CurrentManagedThreadId,()=>{}); Check(transaction.Apply(Aggregate().Data,()=>current,()=>clean,()=>current=false)==PlayerKVApplyError.Stale,"notification revocation"); current=true; Check(transaction.Apply(Aggregate().Data,()=>current,()=>clean,()=>clean=false)==PlayerKVApplyError.SideEffect,"notification sideeffect"); clean=true; Check(transaction.Apply(Aggregate().Data,()=>current,()=>clean,()=>throw new Exception())==PlayerKVApplyError.Apply,"receiver failure"); int before=resets; var other=new PlayerKVGameLoadTransaction(modules,-1,()=>{}); Check(other.Apply(Aggregate().Data,()=>true,()=>true,()=>{})==PlayerKVApplyError.Input && resets==before,"wrong thread never mutates"); await Throws(()=>{new PlayerKVGameLoadTransaction(modules.Take(30),Environment.CurrentManagedThreadId,()=>{});return Task.CompletedTask;}); }); } }