From d4c53dad4ba2b99e22e41a9739c3e279088a2bac Mon Sep 17 00:00:00 2001 From: patchzyy <64382339+patchzyy@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:17:14 +0200 Subject: [PATCH] Use a deterministic clock for input expression tests --- runtime/src/input_expr.cpp | 25 +++++++++++++++++++------ runtime/tests/CMakeLists.txt | 1 + runtime/tests/test_expr.cpp | 25 ++++++++++++++++--------- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/runtime/src/input_expr.cpp b/runtime/src/input_expr.cpp index d9c73c3..fbf1148 100644 --- a/runtime/src/input_expr.cpp +++ b/runtime/src/input_expr.cpp @@ -9,11 +9,24 @@ #include namespace InputExpr { +#ifdef MKW_INPUT_EXPR_TEST_CLOCK +// Only the standalone test target supplies this clock; runtime builds use the +// steady clock directly, with no mutable override or extra runtime state. +std::chrono::steady_clock::time_point TestClockNow(); +#endif namespace { using Clock = std::chrono::steady_clock; using FSec = std::chrono::duration; +static Clock::time_point Now() { +#ifdef MKW_INPUT_EXPR_TEST_CLOCK + return TestClockNow(); +#else + return Clock::now(); +#endif +} + enum class Kind { Literal, Input, Not, Add, Sub, Mul, Div, And, Or, Xor, Greater, Less, Equal, @@ -56,7 +69,7 @@ struct Node { mutable bool state = false; mutable unsigned taps = 0; mutable double value = 0.0; - mutable Clock::time_point mark = Clock::now(); + mutable Clock::time_point mark = Now(); mutable bool marked = false; }; @@ -400,7 +413,7 @@ double Eval(const Node& node, const InputSource& source) { return std::copysign(std::max(0.0, std::abs(v) - dz) / (1.0 - dz), v); } case Kind::FnTimer: { - const auto now = Clock::now(); + const auto now = Now(); if (!node.marked) { node.mark = now; node.marked = true; @@ -431,7 +444,7 @@ double Eval(const Node& node, const InputSource& source) { return node.state ? 1.0 : 0.0; } case Kind::FnHold: { - const auto now = Clock::now(); + const auto now = Now(); if (!node.marked) { node.mark = now; node.marked = true; @@ -448,7 +461,7 @@ double Eval(const Node& node, const InputSource& source) { return node.state ? 1.0 : 0.0; } case Kind::FnTap: { - const auto now = Clock::now(); + const auto now = Now(); if (!node.marked) { node.mark = now; node.marked = true; @@ -480,7 +493,7 @@ double Eval(const Node& node, const InputSource& source) { return desired == node.taps ? 1.0 : 0.0; } case Kind::FnPulse: { - const auto now = Clock::now(); + const auto now = Now(); const double input = Arg(node, 0, source); if (input < kConditionThreshold) { node.released = true; @@ -502,7 +515,7 @@ double Eval(const Node& node, const InputSource& source) { return node.state ? 1.0 : 0.0; } case Kind::FnSmooth: { - const auto now = Clock::now(); + const auto now = Now(); if (!node.marked) { node.mark = now; node.marked = true; diff --git a/runtime/tests/CMakeLists.txt b/runtime/tests/CMakeLists.txt index 246dd73..6ccbbe2 100644 --- a/runtime/tests/CMakeLists.txt +++ b/runtime/tests/CMakeLists.txt @@ -36,6 +36,7 @@ target_include_directories(mkw_nand_move_tests PRIVATE "${runtime_root}/src/hle/ mkw_add_test(mkw_nand_settings_tests nand_settings_tests.cpp) mkw_add_test(mkw_sc_serial_tests sc_serial_tests.cpp) mkw_add_test(mkw_input_expr_tests test_expr.cpp "${runtime_root}/src/input_expr.cpp") +target_compile_definitions(mkw_input_expr_tests PRIVATE MKW_INPUT_EXPR_TEST_CLOCK) if(WIN32) mkw_add_test(mkw_windows_host_context_tests host_context_tests.cpp diff --git a/runtime/tests/test_expr.cpp b/runtime/tests/test_expr.cpp index c770a3c..d0bc0cb 100644 --- a/runtime/tests/test_expr.cpp +++ b/runtime/tests/test_expr.cpp @@ -8,7 +8,14 @@ #include #include #include -#include +#ifndef MKW_INPUT_EXPR_TEST_CLOCK +#error "Build this test through CMake so the deterministic clock is enabled" +#endif + +static std::chrono::steady_clock::time_point g_now{}; +namespace InputExpr { +std::chrono::steady_clock::time_point TestClockNow() { return g_now; } +} static int g_failures = 0; static std::map g_inputs; @@ -41,7 +48,7 @@ static bool Pressed(const InputExpr::Expression& e) { return e.Evaluate(Source()) > InputExpr::kConditionThreshold; } -static void Sleep(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); } +static void AdvanceTime(int ms) { g_now += std::chrono::milliseconds(ms); } int main() { std::printf("Dolphin expression engine\n"); @@ -82,7 +89,7 @@ int main() { auto hold = Compile("hold(`H`, 0.05)"); g_inputs["H"] = 1.0; Check(!Pressed(hold), "hold not satisfied immediately"); - Sleep(70); + AdvanceTime(70); Check(Pressed(hold), "hold satisfied after the interval"); g_inputs["H"] = 0.0; Check(!Pressed(hold), "hold clears on release"); @@ -93,7 +100,7 @@ int main() { pulse.Evaluate(Source()); g_inputs["P"] = 1.0; Check(Pressed(pulse), "pulse fires on rising edge"); - Sleep(80); + AdvanceTime(80); Check(!Pressed(pulse), "pulse expires"); // The timing-window idiom seen in shared Dolphin configs. @@ -102,9 +109,9 @@ int main() { window.Evaluate(Source()); g_inputs["W"] = 1.0; Check(!Pressed(window), "window closed before its start"); - Sleep(90); + AdvanceTime(90); Check(Pressed(window), "window open between the two pulses"); - Sleep(90); + AdvanceTime(90); Check(!Pressed(window), "window closed after its end"); // timer ramps 0..1 and wraps, so a threshold turns it into a square wave. @@ -114,7 +121,7 @@ int main() { int low = 0; for (int i = 0; i < 40; ++i) { (Pressed(timer) ? high : low)++; - Sleep(5); + AdvanceTime(5); } Check(high > 5 && low > 5, "timer alternates high and low"); @@ -130,7 +137,7 @@ int main() { high = low = 0; for (int i = 0; i < 60; ++i) { (Pressed(dolphinLine) ? high : low)++; - Sleep(2); + AdvanceTime(2); } Check(high > 5 && low > 5, "LB alternates via timer(0.01)"); @@ -192,7 +199,7 @@ int main() { auto sm = Compile("smooth(`A`, 0)"); g_inputs["A"] = 1.0; sm.Evaluate(Source()); - Sleep(5); + AdvanceTime(5); Check(std::isfinite(sm.Evaluate(Source())), "smooth with a zero rate stays finite"); }