From 18c58fd11732e96ec02c0223d08592be1090d47c Mon Sep 17 00:00:00 2001 From: iisaev Date: Fri, 13 Nov 2026 17:48:45 +0100 Subject: [PATCH] test: replace fixed waits with observable GUI readiness Advance integration drivers only after their requested controls, game state or file effects are observable. Preserve the behavioral assertions and shared viewport capture pipeline. --- .../src/CollectionEditTestDriverNode.cs | 26 +++++++++++-- .../src/DisplaySettingsTestDriverNode.cs | 39 +++++++++++++++++-- .../hot_reload/src/HotReloadTestDriverNode.cs | 26 +++++++++++-- .../menu_flow/src/MenuFlowTestDriverNode.cs | 26 +++++++++++-- .../new_game/src/NewGameTestDriverNode.cs | 26 +++++++++++-- 5 files changed, 123 insertions(+), 20 deletions(-) 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: