// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Waits, traces and log follows: `wait --frames` and a condition reply on the // Pump that satisfies them, a condition that cannot be decided replies with // why, an event wakes its waiter from the emitting thread without waiting // for Pump, and a timeout says what the condition's paths were. A trace // collects a value over frames with its stats, or a sample's history. The // log filters by level, and a follower receives new matching entries in // order, is dropped when it cannot keep up, and ends when its time is up. 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: std::atomic refuse = false; void Reply(const Value& reply) override { std::lock_guard lock(mutex); if (!final) final = reply; } bool Stream(const Value& line) override { if (refuse) return false; std::lock_guard lock(mutex); lines.push_back(line); return true; } std::optional Final() { std::lock_guard lock(mutex); return final; } std::vector Lines() { std::lock_guard lock(mutex); return lines; } private: std::mutex mutex; std::optional final; std::vector lines; }; Session* session = nullptr; std::shared_ptr Send(std::string_view method, std::string_view params = "{}", std::int64_t timeoutMs = 30'000) { Value request = Value::MakeObject(); request.Set("token", Value(session->Token())); request.Set("method", Value(method)); request.Set("params", Value::Parse(params)); request.Set("timeoutMs", Value(timeoutMs)); 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(); } std::string MessageOf(FakeExchange& exchange) { const auto reply = exchange.Final(); return reply && reply->Find("error") ? reply->Find("error")->Find("message")->AsString() : std::string(); } } int main() { std::int32_t score = 0; Watch("score", score); Session owned({ .name = "wait", .listen = false }); session = &owned; auto frames = Send("debug.wait", R"({"frames": 3})"); Pump(); Pump(); Check(!frames->Final(), "wait --frames 3 has not replied after two frames"); Pump(); Check(ResultOf(*frames).Find("frame")->AsInteger() == 3, "wait --frames 3 replies on the third"); auto condition = Send("debug.wait", R"({"condition": "score >= 2"})"); for (std::int32_t i = 0; i < 5 && !condition->Final(); ++i) { ++score; Pump(); } Check(score == 2 && CodeOf(*condition).empty(), "a condition replies on the Pump it holds"); auto broken = Send("debug.wait", R"({"condition": "missing > 1"})"); Pump(); Check(CodeOf(*broken) == "ConditionError" && MessageOf(*broken).find("no watch or sample 'missing'") != std::string::npos, "an undecidable condition says why"); auto event = Send("debug.wait", R"({"event": "Boom"})"); std::thread([] { Emit("Boom", Value::Parse(R"({"power": 9})")); }).join(); Check(event->Final().has_value() && ResultOf(*event).Find("payload")->Find("power")->AsInteger() == 9, "an event wakes its waiter without waiting for Pump"); auto slow = Send("debug.wait", R"({"condition": "score > 100"})", 1); std::this_thread::sleep_for(std::chrono::milliseconds(10)); Pump(); Check(CodeOf(*slow) == "Timeout" && MessageOf(*slow).find("score is 2") != std::string::npos, "a timeout reports the condition's paths"); Check(CodeOf(*Send("debug.wait", R"({"frames": 1, "paused": true})")) == "InvalidParams", "waiting for two things at once is refused"); auto trace = Send("debug.trace", R"({"paths": ["score"], "frames": 4})"); for (std::int32_t i = 0; i < 4; ++i) { score += 10; Pump(); } const Value traced = ResultOf(*trace); const Value* series = traced.Find("series") ? traced.Find("series")->Find("score") : nullptr; Check(series != nullptr && series->Find("values")->Size() == 4 && series->Find("stats")->Find("min")->AsNumber() == 12 && series->Find("stats")->Find("max")->AsNumber() == 42 && series->Find("stats")->Find("changes")->AsInteger() == 3, "a trace collects a value per frame with its stats"); for (std::int32_t i = 1; i <= 3; ++i) { Sample("residual", 0.5 * i); Pump(); } auto history = Send("debug.trace", R"({"paths": ["residual"], "history": true})"); Pump(); const Value historyReply = ResultOf(*history); const Value* samples = historyReply.Find("series") ? historyReply.Find("series")->Find("residual") : nullptr; Check(samples != nullptr && samples->Find("values")->Size() == 3 && samples->Find("stats")->Find("last")->AsNumber() == 1.5, "a trace can read a sample's history"); Log("net", "hello {}", 1); LogWarn("net", "careful"); const Value warnings = ResultOf(*Send("debug.log", R"({"level": "warn"})")); Check(warnings.Find("entries")->Size() == 1 && warnings.Find("entries")->Items()[0].Find("message")->AsString() == "careful", "the log filters by level"); Check(CodeOf(*Send("debug.log", R"({"level": "loud"})")) == "InvalidParams", "an unknown level is refused"); auto follow = Send("debug.log", R"({"follow": true, "category": "game"})"); Log("game", "one"); Log("other", "skip"); Log("game", "two"); const std::vector lines = follow->Lines(); Check(lines.size() == 2 && lines[1].Find("entry")->Find("message")->AsString() == "two", "a follower receives new matching entries, in order"); follow->refuse = true; Log("game", "three"); Check(ResultOf(*Send("debug.status")).Find("following")->AsInteger() == 0, "a follower that cannot keep up is dropped"); auto brief = Send("debug.log", R"({"follow": true, "forMs": 1})"); std::this_thread::sleep_for(std::chrono::milliseconds(10)); Pump(); Check(brief->Final().has_value() && ResultOf(*brief).Find("ended") != nullptr, "a follow ends when its time is up"); return failures == 0 ? 0 : 1; }