89 lines
6.5 KiB
C#
89 lines
6.5 KiB
C#
using System.Net;
|
|
using System.Reflection;
|
|
using System.Runtime.ExceptionServices;
|
|
using Newtonsoft.Json.Linq;
|
|
using game.Account;
|
|
using game.PlayerDataStorage;
|
|
|
|
partial class RouteTests
|
|
{
|
|
internal static readonly string Root=Path.GetFullPath(Path.Combine("CodexOutput/upserver/KVSaveRouteTests/runs",DateTime.UtcNow.ToString("yyyyMMddHHmmss")+"_"+Guid.NewGuid().ToString("N")));
|
|
static readonly Uri Api=new("http://kv-route.invalid/");
|
|
static int assertions,failed; static readonly List<object> Results=new();
|
|
static async Task<int> Main(string[] args) {
|
|
Directory.CreateDirectory(Root);
|
|
if(args.Contains("--diagnostic")) AppDomain.CurrentDomain.FirstChanceException += (_,e)=>{
|
|
if(e.Exception is IOException || e.Exception is UnauthorizedAccessException)
|
|
Console.WriteLine("IO DIAGNOSTIC "+e.Exception);
|
|
};
|
|
await Run("C1-ReadKV-contract",ReadContract);
|
|
if(args.Contains("--all")) await More();
|
|
File.WriteAllText(Path.Combine(Root,"result.json"),System.Text.Json.JsonSerializer.Serialize(new{groups=Results.Count,assertions,failed,results=Results},new System.Text.Json.JsonSerializerOptions{WriteIndented=true}));
|
|
Console.WriteLine($"RESULT groups={Results.Count} assertions={assertions} failed={failed} run={Root}");return failed==0?0:1;
|
|
}
|
|
static async Task Run(string name,Func<Task> action) { int before=assertions;try{await action();Results.Add(new{name,passed=true,assertions=assertions-before});Console.WriteLine("PASS "+name);}catch(Exception e){failed++;Results.Add(new{name,passed=false,error=e.ToString()});Console.WriteLine("FAIL "+name+" "+e);} }
|
|
static void Check(bool ok,string message){Interlocked.Increment(ref assertions);if(!ok)throw new InvalidOperationException(message);}
|
|
static object Call(string name,params object[] args) {
|
|
var method=typeof(PlayFabMgr).GetMethod(name,BindingFlags.Static|BindingFlags.NonPublic);
|
|
Check(method!=null,"Required fixed access missing: "+name);
|
|
try{return method.Invoke(null,args);}catch(TargetInvocationException e){ExceptionDispatchInfo.Capture(e.InnerException).Throw();throw;}
|
|
}
|
|
static dynamic Read(PlayerKVGameSessionHandle h,string key)=>Call("ReadKV",h,key);
|
|
static void ReadIs(dynamic r,string error,bool? found,string value){Check(r.Error.ToString()==error,"read error "+error);Check((bool?)r.Found==found,"found tri-state");Check((string)r.Value==value,"read value");}
|
|
static async Task ReadContract(){
|
|
// Missing method fails this behavioral test before implementation, without a compiler error.
|
|
ReadIs(Read(null,"k"),"InvalidInput",null,null);
|
|
await using var f=new Fixture(); var h=await f.Open();
|
|
ReadIs(Read(h,"k"),"None",true,"base");ReadIs(Read(h,"missing"),"None",false,null);ReadIs(Read(h,"empty"),"None",true,"");
|
|
ReadIs(Read(h,"K"),"None",false,null);
|
|
foreach(var key in new[]{" ","姹夊瓧",new string('x',128)})ReadIs(Read(h,key),"None",false,null);
|
|
foreach(var key in new[]{null,"",new string('x',129)})ReadIs(Read(h,key),"InvalidInput",null,null);
|
|
Check(f.Fake.Gets==1,"reads do not get network");
|
|
Check(PlayerKVGameLoadGuard.TryEnter(out var guard),"own guard");
|
|
try{ReadIs(Read(null,null),"LoadGuardActive",null,null);Check(guard.Count(PlayerKVGuardPoint.ReadLocal)==1,"guard precedes input");}finally{guard.Dispose();}
|
|
await f.Bridge.CloseAsync();ReadIs(Read(h,"k"),"StaleOrClosing",null,null);
|
|
Check(UnityEngine.PlayerPrefs.Calls==0&&PlayFab.PlayFabClientAPI.Calls==0,"no PF/Prefs");
|
|
}
|
|
sealed class Handler:HttpMessageHandler {
|
|
readonly Func<HttpRequestMessage,CancellationToken,Task<HttpResponseMessage>> send;
|
|
internal Handler(Func<HttpRequestMessage,CancellationToken,Task<HttpResponseMessage>> send){this.send=send;}
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage r,CancellationToken t)=>send(r,t);
|
|
}
|
|
static HttpResponseMessage Json(HttpStatusCode status,object o)=>new(status){Content=new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(o))};
|
|
sealed class Fake {
|
|
internal readonly Dictionary<string,Dictionary<string,string>> Remote=new(){["101"]=new(){{"k","base"},{"empty",""}},["102"]=new(){{"k","B"}}};
|
|
internal int Gets,Writes; internal bool DropResponse,Unauthorized;
|
|
internal Func<HttpRequestMessage,CancellationToken,Task> Before;
|
|
internal HttpMessageHandler Create(CookieContainer jar){var id=jar.GetCookies(Api)["sid"].Value;return new Handler(async(r,t)=>{
|
|
Check(r.RequestUri.Host=="kv-route.invalid"&&r.RequestUri.Port==80,"synthetic host only");
|
|
var path=r.RequestUri.AbsolutePath;Check(path is "/playerdata/Get" or "/playerdata/Set" or "/playerdata/Delete","fixed KV path");
|
|
if(path.EndsWith("Get"))Interlocked.Increment(ref Gets);else Interlocked.Increment(ref Writes);
|
|
if(Before!=null)await Before(r,t);
|
|
if(Unauthorized)return Json(HttpStatusCode.Unauthorized,new{});
|
|
var body=JObject.Parse(await r.Content.ReadAsStringAsync());
|
|
lock(Remote){
|
|
if(path.EndsWith("Get"))return Json(HttpStatusCode.OK,new{data=new Dictionary<string,string>(Remote[id])});
|
|
if(path.EndsWith("Set"))foreach(var p in ((JObject)body["data"]).Properties())Remote[id][p.Name]=(string)p.Value;
|
|
else foreach(var k in body["keys"].Values<string>())Remote[id].Remove(k);
|
|
}
|
|
if(DropResponse)throw new HttpRequestException("Synthetic response lost after apply");
|
|
return Json(HttpStatusCode.OK,new{});
|
|
});}
|
|
}
|
|
sealed class Fixture:IAsyncDisposable {
|
|
internal readonly string Cache=Path.Combine(Root,Guid.NewGuid().ToString("N"));
|
|
internal readonly Fake Fake=new(); internal readonly PlayerKVGameSessionBridge Bridge;
|
|
readonly List<S101AccountSession> sessions=new(); internal long Current=1; internal bool ThrowCurrent;
|
|
internal Fixture(){Directory.CreateDirectory(Cache);Bridge=new PlayerKVGameSessionBridge(Fake.Create);}
|
|
internal async Task<PlayerKVGameSessionHandle> Open(string id="101"){
|
|
var s=new S101AccountSession(Api,jar=>new Handler((r,t)=>{
|
|
Check(r.RequestUri.Host=="kv-route.invalid"&&r.RequestUri.AbsolutePath=="/account/Login","synthetic auth only");jar.Add(Api,new Cookie("sid",id));
|
|
return Task.FromResult(Json(HttpStatusCode.OK,new{state=0,internalId=int.Parse(id),newCreate=false,createTime="2026-01-01T00:00:00Z",svrTime="2026-01-01T00:00:00Z"}));}));
|
|
sessions.Add(s);Check((await s.LoginAsync(id)).Success,"login");
|
|
var result=await Bridge.OpenAsync(s,Cache,id,Api,Current,e=>ThrowCurrent?throw new InvalidOperationException("current-predicate-fault"):e==Current);
|
|
Check(result.Success,"open "+result.Error);return result.Handle;
|
|
}
|
|
public async ValueTask DisposeAsync(){await Bridge.CloseAsync();foreach(var s in sessions)s.Dispose();}
|
|
}
|
|
}
|