using game; void Assert(bool ok){if(!ok)throw new Exception("Exit assertion failed");} foreach(var logout in new[]{true,false}) foreach(var failure in new[]{true,false}) { GameCore.Timer.Reset(); var user=new User();var hub=new Hub();var ads=new Ads(); var act=new QuitAct{userService=user,rtService=hub,config=new Config(),adsService=ads,soundMgr=new SoundMgr()}; QuitStage.Logout=logout;var pending=act.StartAsync(); Assert(!pending.IsCompleted && user.Logouts==(logout?1:0) && user.Valid==!logout && !QuitStage.Logout); user.DeliverOldHttpAndLogin();Assert(user.Applied==(logout?0:2)); if(failure)hub.Gate.SetException(new Exception("Stop failed"));else hub.Gate.SetResult(true); await pending;Assert(ads.Released && GameCore.Timer.Destroyed==1 && GameCore.Timer.Cancelled==1); await act.StartAsync();Assert(user.Logouts==(logout?1:0)); Console.WriteLine($"PASS actual QuitAct: logout={logout}, stopFailure={failure}, identity invalidated before stop wait and no duplicate logout"); } class User:IUserService {public bool Valid=true;public int Logouts,Applied;public void Logout(){Valid=false;Logouts++;} public void DeliverOldHttpAndLogin(){if(Valid)Applied+=2;} } class Hub:IRTService {public TaskCompletionSource Gate=new();public Task ReleaseAsync()=>Gate.Task;} class Ads:IAdsService {public bool Released;public void Release(){Released=true;}} class Config:asap.core.IConfig{public void Rest(){}} public interface IRTService{Task ReleaseAsync();}public interface IAdsService{void Release();} namespace asap.core { public class InjectAttribute:Attribute{} public interface IConfig{void Rest();} } namespace game {public interface IUserService{void Logout();}public static class QuitStage{public static bool Logout;} } namespace Game {public class AGameAct {protected virtual void OnDestroy(){} protected virtual void OnStarted(){}public virtual Task StartAsync()=>Task.FromResult(true);public void Invoke(string s,float n){} } } namespace GameCore {public static class Timer{public static int Cancelled,Destroyed;public static void Reset(){Cancelled=Destroyed=0;}public static void CancelAllRegisteredTimers(){Cancelled++;}public static void Destroy(){if(Cancelled!=Destroyed+1)throw new Exception("Timer destroyed before cancellation");Destroyed++;}} } namespace UnityEngine {public class GameObject {public static void Destroy(object x){}public static void DestroyImmediate(object x){}public static GameObject Find(string x)=>null;}public static class Debug{public static void LogException(Exception e){}}} public class SoundMgr {public void Release(){}} public static class LoadingScreen{public static void Hide(){}} public class UIManager{public static UIManager Instance=new();public UnityEngine.GameObject gameObject=new();} public class GameEventMgr{public static GameEventMgr Instance=new();public void Clear(){}} public class RedPointManager{public static RedPointManager Instance=new();public void Init(){}} public static class RootCtx{public static void DestoryRootCtx(){}}