123 lines
5.3 KiB
C++
123 lines
5.3 KiB
C++
|
|
// 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<Value> Final() {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
return final;
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
std::mutex mutex;
|
||
|
|
std::optional<Value> final;
|
||
|
|
};
|
||
|
|
|
||
|
|
Session* session = nullptr;
|
||
|
|
|
||
|
|
std::shared_ptr<FakeExchange> 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<FakeExchange>();
|
||
|
|
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;
|
||
|
|
}
|