// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Pause and step hold exactly what the app gates on them. Pausing an app // that never calls Advance is refused rather than silently doing nothing; // while paused Advance is false and a step lets it through that many times, // replying once they are taken; `pause --when` stops the app the frame its // condition holds; `pause --on-check` stops it within the frame a check // fails, before the next Advance; TakeSteps hands a fixed-step loop every // granted step at once; and quit calls onQuit once, on the Pump thread. import std; import Crafter.Debug; using namespace Crafter::Debug; namespace { std::int32_t failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } class FakeExchange : public Detail::Exchange { public: void Reply(const Value& reply) override { std::lock_guard lock(mutex); if (!final) final = reply; } bool Stream(const Value&) override { return true; } std::optional Final() { std::lock_guard lock(mutex); return final; } private: std::mutex mutex; std::optional final; }; Session* session = nullptr; std::shared_ptr Send(std::string_view method, std::string_view params = "{}") { Value request = Value::MakeObject(); request.Set("token", Value(session->Token())); request.Set("method", Value(method)); request.Set("params", Value::Parse(params)); auto exchange = std::make_shared(); session->Handle(request, exchange); return exchange; } Value ResultOf(FakeExchange& exchange) { const auto reply = exchange.Final(); return reply && reply->Find("result") ? *reply->Find("result") : Value(); } std::string CodeOf(FakeExchange& exchange) { const auto reply = exchange.Final(); return reply && reply->Find("error") ? reply->Find("error")->Find("code")->AsString() : std::string(); } } int main() { std::int32_t counter = 0; Watch("counter", counter); std::int32_t quits = 0; Session owned({ .name = "pause", .onQuit = [&] { ++quits; }, .listen = false }); session = &owned; Check(CodeOf(*Send("debug.pause")) == "PauseUnsupported", "pausing an app that never calls Advance is refused"); Check(Advance(), "Advance is true while running"); Check(ResultOf(*Send("debug.pause")).Find("paused")->AsBool(), "pause replies at once"); Check(!Advance() && Paused(), "Advance is false while paused"); auto step = Send("debug.step", R"({"count": 2})"); Check(!step->Final(), "a step replies once its steps are taken, not before"); Check(Advance() && Advance() && !Advance(), "a step of two lets Advance through twice"); Pump(); Check(ResultOf(*step).Find("paused")->AsBool(), "the step replies on the next Pump"); Send("debug.resume"); Check(Advance() && !Paused(), "resume lets the app run"); Check(CodeOf(*Send("debug.step")) == "NotPaused", "a step while running is refused"); Check(ResultOf(*Send("debug.pause", R"({"when": "counter >= 3"})")).Find("armed")->AsString() == "when", "pause --when arms"); auto waitPaused = Send("debug.wait", R"({"paused": true})"); for (std::int32_t frame = 0; frame < 10 && Advance(); ++frame) { ++counter; Pump(); } Check(counter == 3 && Paused(), "pause --when stops the app the frame its condition holds"); Pump(); const Value pausedReply = ResultOf(*waitPaused); const Value* reason = pausedReply.Find("reason"); Check(reason != nullptr && reason->AsString().find("counter >= 3") != std::string::npos, "wait --paused replies with the reason"); Send("debug.resume"); Check(ResultOf(*Send("debug.pause", R"({"onCheck": true})")).Find("armed")->AsString() == "onCheck", "pause --on-check arms"); Check(Advance(), "running before the check fails"); Crafter::Debug::Check(false, "physics", "body {} has mass {}", 9, -1); Check(!Advance(), "a failed check pauses before the next Advance in the same frame"); const Value status = ResultOf(*Send("debug.status")); Check(status.Find("pauseReason")->AsString().find("check failed at main.cpp:") != std::string::npos, "the reason names the check's site"); Send("debug.resume"); Send("debug.pause"); auto steps = Send("debug.step", R"({"count": 3})"); Check(Paused() && TakeSteps() == 3 && TakeSteps() == 0, "TakeSteps hands out every granted step at once"); Pump(); Check(steps->Final().has_value() && CodeOf(*steps).empty(), "a fixed-step loop's steps reply too"); Send("debug.resume"); Check(CodeOf(*Send("debug.pause", R"({"trap": true})")) == "InvalidParams", "trap without when or onCheck is refused"); Check(CodeOf(*Send("debug.pause", R"({"when": "counter = 3"})")) == "ConditionError", "a malformed condition is refused when armed"); auto quit = Send("debug.quit"); Check(QuitRequested() && ResultOf(*quit).Find("quitting")->AsBool(), "quit replies and sets QuitRequested"); Check(quits == 0, "onQuit waits for the Pump thread"); Pump(); Pump(); Check(quits == 1, "onQuit runs once, on Pump"); return failures == 0 ? 0 : 1; }