diff --git a/test_suites/collection_edit/src/CollectionEditTestDriverNode.cs b/test_suites/collection_edit/src/CollectionEditTestDriverNode.cs index 4b5cda7..3d28e60 100644 --- a/test_suites/collection_edit/src/CollectionEditTestDriverNode.cs +++ b/test_suites/collection_edit/src/CollectionEditTestDriverNode.cs @@ -34,6 +34,9 @@ namespace Mods.CollectionEditTest /// public partial class CollectionEditTestDriverNode : Node { + private int readinessToken = int.MinValue; + private ulong readinessStarted; + private ulong readinessFrame; public const string Flag = "--itest=collection_edit"; private const string GameId = "novel_template"; @@ -54,7 +57,6 @@ namespace Mods.CollectionEditTest private int passed; private int step; private double elapsed; - private double stepAt; private bool done; public override void _Ready() @@ -69,6 +71,7 @@ namespace Mods.CollectionEditTest ModEventBus.Unsubscribe(OnSceneEntered); } + /// Advances when observable scene/UI readiness has held for two real frames; the watchdog remains active. public override void _Process(double delta) { if (done) @@ -82,9 +85,6 @@ namespace Mods.CollectionEditTest return; } - if (elapsed - stepAt < 0.25) - return; - stepAt = elapsed; try { @@ -110,8 +110,26 @@ namespace Mods.CollectionEditTest enteredScenes.Add(e.SceneName); } + // Deferred UI/scene work must finish before the original assertions run. + private bool WaitForReadyFrames(int token, bool ready, double timeoutSeconds = 30) + { + var now = global::Godot.Time.GetTicksMsec(); + if (readinessToken != token) + { + readinessToken = token; + readinessStarted = now; + readinessFrame = 0; + } + if (now - readinessStarted > timeoutSeconds * 1000) + throw new global::System.TimeoutException($"readiness at step {token}: scene/UI did not become ready"); + if (!ready) { readinessFrame = 0; return false; } + if (readinessFrame == 0) readinessFrame = global::Godot.Engine.GetProcessFrames(); + return global::Godot.Engine.GetProcessFrames() - readinessFrame >= 2; + } + private void RunStep() { + if (!WaitForReadyFrames(step, Boot.IsReady && Menu != null && !Busy && !GodotSceneService.IsTransitioning, 120)) return; switch (step) { case 0: diff --git a/test_suites/display_settings/src/DisplaySettingsTestDriverNode.cs b/test_suites/display_settings/src/DisplaySettingsTestDriverNode.cs index 576e58c..cff4210 100644 --- a/test_suites/display_settings/src/DisplaySettingsTestDriverNode.cs +++ b/test_suites/display_settings/src/DisplaySettingsTestDriverNode.cs @@ -24,6 +24,9 @@ namespace Mods.DisplaySettingsTest /// public partial class DisplaySettingsTestDriverNode : Node { + private int readinessToken = int.MinValue; + private ulong readinessStarted; + private ulong readinessFrame; private const string Display = "Window/Column/Body/Pages/Display/"; private const float TimeoutSeconds = 60f; private const string ResetButton = "Window/Column/Footer/Row/ResetButton"; @@ -38,7 +41,6 @@ namespace Mods.DisplaySettingsTest private int step; private int shotTab; private double elapsed; - private double stepAt; private bool done; public DisplaySettingsTestDriverNode(string shotsDir) @@ -47,6 +49,7 @@ namespace Mods.DisplaySettingsTest backup = File.Exists(settingsPath) ? File.ReadAllText(settingsPath) : null; } + /// Advances when observable scene/UI readiness has held for two real frames; the watchdog remains active. public override void _Process(double delta) { if (done) @@ -60,9 +63,6 @@ namespace Mods.DisplaySettingsTest return; } - if (elapsed - stepAt < 0.5) - return; - stepAt = elapsed; try { @@ -75,8 +75,39 @@ namespace Mods.DisplaySettingsTest } } + // Deferred UI/scene work must finish before the original assertions run. + private bool WaitForReadyFrames(int token, bool ready, double timeoutSeconds = 30) + { + var now = global::Godot.Time.GetTicksMsec(); + if (readinessToken != token) + { + readinessToken = token; + readinessStarted = now; + readinessFrame = 0; + } + if (now - readinessStarted > timeoutSeconds * 1000) + throw new global::System.TimeoutException($"readiness at step {token}: scene/UI did not become ready"); + if (!ready) { readinessFrame = 0; return false; } + if (readinessFrame == 0) readinessFrame = global::Godot.Engine.GetProcessFrames(); + return global::Godot.Engine.GetProcessFrames() - readinessFrame >= 2; + } + + private bool DisplayReady() + { + if (step == 0) return true; + if (menu == null || !menu.Visible) return false; + return step switch + { + 2 => DisplayServer.WindowGetMode() == DisplayServer.WindowMode.Windowed && DisplayServer.WindowGetSize() == WindowSize, + 3 or 6 => DisplayServer.WindowGetMode() == DisplayServer.WindowMode.Fullscreen, + 5 => DisplayServer.WindowGetMode() == DisplayServer.WindowMode.Windowed, + _ => true + }; + } + private void RunStep() { + if (!WaitForReadyFrames(step * 10 + shotTab, Boot.IsReady && GetTree().CurrentScene != null && DisplayReady(), 30)) return; switch (step) { case 0: diff --git a/test_suites/hot_reload/src/HotReloadTestDriverNode.cs b/test_suites/hot_reload/src/HotReloadTestDriverNode.cs index 5a922d7..853ea56 100644 --- a/test_suites/hot_reload/src/HotReloadTestDriverNode.cs +++ b/test_suites/hot_reload/src/HotReloadTestDriverNode.cs @@ -34,6 +34,9 @@ namespace Mods.HotReloadTest /// public partial class HotReloadTestDriverNode : Node { + private int readinessToken = int.MinValue; + private ulong readinessStarted; + private ulong readinessFrame; public const string Flag = "--itest=hot_reload"; private const string LibId = "hot_reload_probe_lib"; @@ -48,7 +51,6 @@ namespace Mods.HotReloadTest private int passed; private int step; private double elapsed; - private double stepAt; private bool done; private GameLaunchArgs startSelection; private string modsRoot; @@ -67,6 +69,7 @@ namespace Mods.HotReloadTest private void OnReloaded(ModSetReloadedEvent e) => reloads.Add(e); + /// Advances when observable scene/UI readiness has held for two real frames; the watchdog remains active. public override void _Process(double delta) { if (done) @@ -80,9 +83,6 @@ namespace Mods.HotReloadTest return; } - if (elapsed - stepAt < 0.25) - return; - stepAt = elapsed; try { @@ -97,8 +97,26 @@ namespace Mods.HotReloadTest private static bool Busy => GameSession.IsSwitching || ModManager.Instance.IsReloading || ModHotReloadService.IsReloadRunning; + // Deferred UI/scene work must finish before the original assertions run. + private bool WaitForReadyFrames(int token, bool ready, double timeoutSeconds = 30) + { + var now = global::Godot.Time.GetTicksMsec(); + if (readinessToken != token) + { + readinessToken = token; + readinessStarted = now; + readinessFrame = 0; + } + if (now - readinessStarted > timeoutSeconds * 1000) + throw new global::System.TimeoutException($"readiness at step {token}: scene/UI did not become ready"); + if (!ready) { readinessFrame = 0; return false; } + if (readinessFrame == 0) readinessFrame = global::Godot.Engine.GetProcessFrames(); + return global::Godot.Engine.GetProcessFrames() - readinessFrame >= 2; + } + private void RunStep() { + if (!WaitForReadyFrames(step, Boot.IsReady && GetTree().CurrentScene != null && !Busy, 120)) return; switch (step) { case 0: diff --git a/test_suites/menu_flow/src/MenuFlowTestDriverNode.cs b/test_suites/menu_flow/src/MenuFlowTestDriverNode.cs index 9523eeb..a54b458 100644 --- a/test_suites/menu_flow/src/MenuFlowTestDriverNode.cs +++ b/test_suites/menu_flow/src/MenuFlowTestDriverNode.cs @@ -26,6 +26,9 @@ namespace Mods.MenuFlowTest /// public partial class MenuFlowTestDriverNode : Node { + private int readinessToken = int.MinValue; + private ulong readinessStarted; + private ulong readinessFrame; private const string GameId = "novel_template"; private const string Slot = "slot_942"; private const string DeletableId = "menu_flow_deletable"; @@ -35,13 +38,13 @@ namespace Mods.MenuFlowTest private int passed; private int step; private double elapsed; - private double stepAt; private bool done; private Node hub; private string initialLocale; private int languageIndex; private bool languageReady; + /// Advances when observable scene/UI readiness has held for two real frames; the watchdog remains active. public override void _Process(double delta) { if (done) @@ -55,9 +58,6 @@ namespace Mods.MenuFlowTest return; } - if (elapsed - stepAt < 0.5) - return; - stepAt = elapsed; try { @@ -72,8 +72,26 @@ namespace Mods.MenuFlowTest private Control Menu => GetTree().CurrentScene as Control; + // Deferred UI/scene work must finish before the original assertions run. + private bool WaitForReadyFrames(int token, bool ready, double timeoutSeconds = 30) + { + var now = global::Godot.Time.GetTicksMsec(); + if (readinessToken != token) + { + readinessToken = token; + readinessStarted = now; + readinessFrame = 0; + } + if (now - readinessStarted > timeoutSeconds * 1000) + throw new global::System.TimeoutException($"readiness at step {token}: scene/UI did not become ready"); + if (!ready) { readinessFrame = 0; return false; } + if (readinessFrame == 0) readinessFrame = global::Godot.Engine.GetProcessFrames(); + return global::Godot.Engine.GetProcessFrames() - readinessFrame >= 2; + } + private void RunStep() { + if (!WaitForReadyFrames(step * 100 + languageIndex * 2 + (languageReady ? 1 : 0), Boot.IsReady && Menu != null && !Cthangover.Core.Scenes.GodotSceneService.IsTransitioning, 30)) return; switch (step) { case 0: diff --git a/test_suites/new_game/src/NewGameTestDriverNode.cs b/test_suites/new_game/src/NewGameTestDriverNode.cs index 6f491cc..8ec4d3e 100644 --- a/test_suites/new_game/src/NewGameTestDriverNode.cs +++ b/test_suites/new_game/src/NewGameTestDriverNode.cs @@ -36,6 +36,9 @@ namespace Mods.NewGameTest /// public partial class NewGameTestDriverNode : Node { + private int readinessToken = int.MinValue; + private ulong readinessStarted; + private ulong readinessFrame; public const string Flag = "--itest=new_game"; private const string MainGame = "cthangover"; @@ -49,7 +52,6 @@ namespace Mods.NewGameTest private int passed; private int step; private double elapsed; - private double stepAt; private bool done; private bool sawMarao; private int clicks; @@ -67,6 +69,7 @@ namespace Mods.NewGameTest ModEventBus.Unsubscribe(OnSceneEntered); } + /// Advances when observable scene/UI readiness has held for two real frames; the watchdog remains active. public override void _Process(double delta) { if (done) @@ -80,9 +83,6 @@ namespace Mods.NewGameTest return; } - if (elapsed - stepAt < 0.25) - return; - stepAt = elapsed; try { @@ -101,8 +101,26 @@ namespace Mods.NewGameTest private void OnSceneEntered(SceneEnteredEvent e) => enteredScenes.Add(e.SceneName); + // Deferred UI/scene work must finish before the original assertions run. + private bool WaitForReadyFrames(int token, bool ready, double timeoutSeconds = 30) + { + var now = global::Godot.Time.GetTicksMsec(); + if (readinessToken != token) + { + readinessToken = token; + readinessStarted = now; + readinessFrame = 0; + } + if (now - readinessStarted > timeoutSeconds * 1000) + throw new global::System.TimeoutException($"readiness at step {token}: scene/UI did not become ready"); + if (!ready) { readinessFrame = 0; return false; } + if (readinessFrame == 0) readinessFrame = global::Godot.Engine.GetProcessFrames(); + return global::Godot.Engine.GetProcessFrames() - readinessFrame >= 2; + } + private void RunStep() { + if (!WaitForReadyFrames(step, Boot.IsReady && Menu != null && !Busy && (step != 0 || Menu is not Cthangover.BaseMenu.MainMenu || !GameSession.IsWarmingUp), TimeoutSeconds)) return; switch (step) { case 0: