Files
ft/CodexOutput/upserver/ReviewLifecycleTests/Program.cs

44 lines
12 KiB
C#

using System.Text.Json;
var mode=args.Contains("--normal")?"normal":args.Contains("--regression")?"regression":args.Contains("--stage")?"stage":"extended";
var results=new List<object>();var failures=0;
bool NoLeaks()=>Addressables.All.All(o=>!o.Valid&&o.Releases==1);
async Task Case(string name,Func<Task<bool>> body){Probe.Reset();bool ok=false;string error=null;try{ok=await body();}catch(Exception ex){error=ex.ToString();}if(!ok)failures++;var observation=new{Probe.RankCalls,Probe.HomeCalls,Probe.CameraWrites,Probe.UiCalls,Probe.Published,Probe.PoolCreates,Probe.Subscriptions,Probe.VolumeAfterRelease,refs=Addressables.All.Select(o=>new{o.Key,o.Valid,o.Releases}).ToArray(),error};results.Add(new{name,passed=ok,observation});Console.WriteLine($"{(ok?"PASS":"FAIL")} {name}: {JsonSerializer.Serialize(observation)}");}
void Invalidate(GetUiSubject s,string how){if(how=="destroy")s.Destroyed=true;else if(how=="cancel")GContext.container.Resolve<IUserService>().Lifetime.Cancel();else GContext.container=new();}
void ClipsReady(){foreach(var op in Addressables.All.OfType<Operation<AudioClip>>().Where(o=>o.Valid&&!o.Completion.Task.IsCompleted))op.Completion.SetResult(new());}
async Task ServiceReady(ServiceSubject s){var task=s.InitAsync();Addressables.Complete("AudioMixer",new AudioMixer());Addressables.Complete("sound",new GameObject());await task;}
async Task<bool> Fails(Task t){try{await t;return false;}catch{return true;}}
if(mode=="normal"){
await Case("GetUI normal",async()=>{var s=new GetUiSubject();UIManager.Instance.Ready.SetResult(new());var t=s.GetUI();Probe.Delay.SetResult(true);await t;return Probe.RankCalls==1&&Probe.HomeCalls==1&&Probe.CameraWrites==1;});
await Case("SoundMgr normal Init/Release",async()=>{var s=new ManagerSubject();var t=s.Init(_=>{});ClipsReady();Probe.Frame.SetResult(true);await t;var count=Probe.Subscriptions;s.Release();return count==10&&Probe.Subscriptions==0&&Probe.VolumeAfterRelease==0&&NoLeaks();});
foreach(var name in new[]{"mixer","prefab"})await Case("SoundService normal "+name,async()=>{var s=new ServiceSubject();await ServiceReady(s);var count=Probe.PoolCreates;s.Release();s.Release();return count==1&&Probe.Subscriptions==0&&NoLeaks();});
}else if(mode=="regression"){
await Case("GetUI exits during delay",async()=>{var s=new GetUiSubject();UIManager.Instance.Ready.SetResult(new());var t=s.GetUI();s.Destroyed=true;GContext.container=new();Probe.Delay.SetResult(true);await t;return Probe.RankCalls==0&&Probe.HomeCalls==0&&Probe.CameraWrites==0;});
await Case("SoundMgr pending Init then Release",async()=>{var s=new ManagerSubject();var t=s.Init(_=>{});s.Release();GContext.container=new();ClipsReady();Probe.Frame.SetResult(true);await t;return Probe.Subscriptions==0&&Probe.VolumeAfterRelease==0&&(s.residentClip==null||s.residentClip.Count==0)&&NoLeaks();});
foreach(var boundary in new[]{"mixer","prefab"})await Case("SoundService Release during "+boundary,async()=>{var s=new ServiceSubject();var t=s.InitAsync();if(boundary=="prefab")Addressables.Complete("AudioMixer",new AudioMixer());s.Release();if(boundary=="mixer")Addressables.Complete("AudioMixer",new AudioMixer());else Addressables.Complete("sound",new GameObject());await t;return Probe.PoolCreates==0&&Probe.Subscriptions==0&&NoLeaks();});
}else if(mode=="stage"){
await Case("Stage normal prefix",async()=>{var s=new StageSubject();var t=s.RunPrefix();Probe.Frame.SetResult(true);GContext.container.Resolve<SoundMgr>().Ready.SetResult(true);await t;return s.Applied;});
await Case("Stage stopped before Start, alive uncancelled",async()=>{var s=new StageSubject();await s.Stop();await s.RunPrefix();return !s.Applied&&!s.Destroyed&&!s.UserService.LoginLifetime.IsCancellationRequested;});
await Case("Stage Stop first statement blocks pending NextFrame",async()=>{var s=new StageSubject{StopReady=new()};var t=s.RunPrefix();var stop=s.Stop();Probe.Frame.SetResult(true);await t;var blocked=!s.Applied&&!s.Destroyed&&!s.UserService.LoginLifetime.IsCancellationRequested&&!stop.IsCompleted;s.StopReady.SetResult(true);await stop;return blocked;});
await Case("Stage Stop during SoundMgr wait and stale progress",async()=>{var s=new StageSubject();var t=s.RunPrefix();Probe.Frame.SetResult(true);var sound=GContext.container.Resolve<SoundMgr>();await s.Stop();var writes=LoadingScreen.Writes;sound.Progress(.5f);sound.Ready.SetResult(true);await t;await s.Stop();return !s.Applied&&!s.Destroyed&&!s.UserService.LoginLifetime.IsCancellationRequested&&LoadingScreen.Writes==writes;});
await Case("Stage OnDestroy invalidates pending initialization",async()=>{var s=new StageSubject();var t=s.RunPrefix();s.DestroyStage();Probe.Frame.SetResult(true);await t;return !s.Applied;});
foreach(var stopped in new[]{false,true})await Case("Stage base startup failure, stopped="+stopped,async()=>{var s=new StageSubject();var t=s.RunCatch();if(stopped)await s.Stop();var warnings=Debug.Warnings;s.StartReady.SetException(new Exception("base startup failure"));var failed=await Fails(t);return stopped?!failed&&!s.CatchCompleted&&Debug.Warnings==warnings+1:failed&&!s.CatchCompleted&&Debug.Warnings==warnings;});
foreach(var stopped in new[]{false,true})await Case("Stage base startup completion, stopped="+stopped,async()=>{var s=new StageSubject();var t=s.RunCatch();if(stopped)await s.Stop();s.StartReady.SetResult(true);await t;return s.CatchCompleted==!stopped;});
}else{
foreach(var boundary in new[]{"ui","replenishment","delay"})foreach(var how in new[]{"destroy","cancel","container"})await Case("GetUI "+boundary+"/"+how,async()=>{var s=new GetUiSubject();var shop=GContext.container.Resolve<PlayerShopData>();shop.Pending=new();if(boundary!="ui")UIManager.Instance.Ready.SetResult(new());if(boundary=="delay")shop.Pending.SetResult(true);var t=s.GetUI();Invalidate(s,how);if(boundary=="ui")UIManager.Instance.Ready.SetResult(new());else if(boundary=="replenishment")shop.Pending.SetResult(true);else Probe.Delay.SetResult(true);await t;return Probe.RankCalls==0&&Probe.HomeCalls==0&&Probe.CameraWrites==0;});
foreach(var boundary in new[]{"timeline","rod","ui"})foreach(var how in new[]{"destroy","cancel","container"})await Case("NewEnterMap "+boundary+"/"+how,async()=>{var s=new GetUiSubject();var t=s.NewEnterMap(new());if(boundary!="timeline")Probe.Seconds[0].SetResult(true);if(boundary=="ui")Probe.Seconds[1].SetResult(true);var camera=Probe.CameraWrites;var published=Probe.Published;var active=s.fishingBehavior.RodBehaviour.gameObject.ActiveWrites;Invalidate(s,how);if(boundary=="timeline")Probe.Seconds[0].SetResult(true);else if(boundary=="rod")Probe.Seconds[1].SetResult(true);else UIManager.Instance.Ready.SetResult(new());await t;return Probe.HomeCalls==0&&Probe.CameraWrites==camera&&Probe.Published==published&&s.fishingBehavior.RodBehaviour.gameObject.ActiveWrites==active;});
foreach(var how in new[]{"destroy","cancel","container"})await Case("OnCameraSpeedEvent "+how,async()=>{var s=new GetUiSubject();var t=s.OnCameraSpeedEvent();Invalidate(s,how);Probe.Seconds[0].SetResult(true);await t;return Probe.Published==0&&s.fishingBehavior.RodBehaviour.gameObject.ActiveWrites==0;});
await Case("NewEnterMap normal",async()=>{var s=new GetUiSubject();var t=s.NewEnterMap(new());Probe.Seconds[0].SetResult(true);Probe.Seconds[1].SetResult(true);UIManager.Instance.Ready.SetResult(new());await t;return Probe.HomeCalls==1&&Probe.Published==2&&s.fishingBehavior.RodBehaviour.gameObject.ActiveWrites==1;});
await Case("GetUI entry already destroyed",async()=>{var s=new GetUiSubject{Destroyed=true};await s.GetUI();return Probe.UiCalls==0;});
foreach(var how in new[]{"release","container"})await Case("SoundMgr bottom service wait/"+how,async()=>{var s=new ManagerSubject();s.soundService.Pending=new();var t=s.Init(_=>{});if(how=="release")s.Release();else GContext.container=new();s.soundService.Pending.SetResult(true);await t;return Probe.Subscriptions==0&&Addressables.All.Count==0&&s.soundService.Released;});
await Case("SoundMgr frame/container",async()=>{var s=new ManagerSubject();var t=s.Init(_=>{});GContext.container=new();Probe.Frame.SetResult(true);await t;return Probe.Subscriptions==0&&NoLeaks();});
foreach(var failure in new[]{"clip","subscription","volume"})await Case("SoundMgr failure rollback/"+failure,async()=>{var s=new ManagerSubject();var t=s.Init(_=>{});if(failure=="clip")Addressables.Get<AudioClip>("clip-a").Completion.SetException(new Exception("clip failure"));else if(failure=="subscription")Probe.ThrowSubscriptionAt=3;else Probe.ThrowVolume=true;ClipsReady();Probe.Frame.SetResult(true);var failed=await Fails(t);return failed&&Probe.Subscriptions==0&&s.soundService.Released&&NoLeaks();});
await Case("SoundMgr duplicate address + repeated Release",async()=>{var s=new ManagerSubject{residentFish=new[]{"one","one","two"}};var t=s.Init(_=>{});ClipsReady();Probe.Frame.SetResult(true);await t;var n=Addressables.All.Count;s.Release();s.Release();return n==2&&Probe.Subscriptions==0&&NoLeaks();});
await Case("SoundMgr repeat successful Init",async()=>{var s=new ManagerSubject();var a=s.Init(_=>{});ClipsReady();Probe.Frame.SetResult(true);await a;Probe.Frame=new();var b=s.Init(_=>{});ClipsReady();Probe.Frame.SetResult(true);await b;var subs=Probe.Subscriptions;s.Release();return subs==10&&Probe.Subscriptions==0&&NoLeaks();});
await Case("SoundMgr superseded audio wait",async()=>{var s=new ManagerSubject();var a=s.Init(_=>{});var old=Addressables.All.ToArray();var b=s.Init(_=>{});ClipsReady();Probe.Frame.SetResult(true);await Task.WhenAll(a,b);var oldReleased=old.All(x=>!x.Valid&&x.Releases==1);var subs=Probe.Subscriptions;var current=Addressables.All.Except(old).All(x=>x.Valid&&x.Releases==0);s.Release();return oldReleased&&current&&subs==10&&Probe.Subscriptions==0&&NoLeaks();});
foreach(var boundary in new[]{"mixer","prefab"})await Case("SoundService failure/"+boundary,async()=>{var s=new ServiceSubject();var t=s.InitAsync();if(boundary=="mixer")Addressables.Get<AudioMixer>("AudioMixer").Completion.SetException(new Exception("mixer fail"));else{Addressables.Complete("AudioMixer",new AudioMixer());Addressables.Get<GameObject>("sound").Completion.SetException(new Exception("prefab fail"));}var failed=await Fails(t);return failed&&Probe.Subscriptions==0&&s.poolService.Active==0&&NoLeaks();});
foreach(var failure in new[]{"pool","subscription"})await Case("SoundService failure rollback/"+failure,async()=>{var s=new ServiceSubject();var t=s.InitAsync();Addressables.Complete("AudioMixer",new AudioMixer());if(failure=="pool")Probe.ThrowPool=true;else Probe.ThrowSubscriptionAt=1;Addressables.Complete("sound",new GameObject());var failed=await Fails(t);return failed&&Probe.Subscriptions==0&&s.poolService.Active==0&&NoLeaks();});
await Case("SoundService repeated successful Init/Release",async()=>{var s=new ServiceSubject();await ServiceReady(s);await ServiceReady(s);var sub=Probe.Subscriptions;s.Release();s.Release();return sub==1&&Probe.Subscriptions==0&&s.poolService.Active==0&&NoLeaks();});
foreach(var boundary in new[]{"mixer","prefab"})foreach(var oldFails in new[]{false,true})await Case("SoundService old "+boundary+" completes after new/fault="+oldFails,async()=>{var s=new ServiceSubject();var a=s.InitAsync();var oldMixer=Addressables.Get<AudioMixer>("AudioMixer");Operation<GameObject> oldPrefab=null;if(boundary=="prefab"){oldMixer.Completion.SetResult(new());oldPrefab=Addressables.Get<GameObject>("sound");}await ServiceReady(s);var fresh=Addressables.All.Where(o=>!ReferenceEquals(o,oldMixer)&&!ReferenceEquals(o,oldPrefab)).ToArray();if(boundary=="mixer"){if(oldFails)oldMixer.Completion.SetException(new Exception("old fail"));else oldMixer.Completion.SetResult(new());}else{if(oldFails)oldPrefab.Completion.SetException(new Exception("old fail"));else oldPrefab.Completion.SetResult(new());}if(oldFails){if(!await Fails(a))return false;}else await a;var isolated=fresh.All(o=>o.Valid&&o.Releases==0)&&Probe.Subscriptions==1&&s.poolService.Active==1;s.Release();return isolated&&Probe.Subscriptions==0&&NoLeaks();});
}
File.WriteAllText(Path.Combine(AppContext.BaseDirectory,"../../../"+$"fixed-{mode}-result.json"),JsonSerializer.Serialize(new{mode,failures,results},new JsonSerializerOptions{WriteIndented=true}));return failures==0?0:1;