feat(session): serve requests without blocking a thread, pause and step
The app's end of the connection, independent of the transport, which reaches it through Detail::Exchange: one request's way back, safe from any thread and never blocking. - What needs only the session's own state (hello, status, list, log, pause, resume) is answered on the transport thread. What touches the app (calls, get, set, trace, screenshot) queues for the next Pump and runs on the app's thread between frames. Waits and steps reply from the Pump that sees them satisfied, events from the thread that emits them. No request ever holds a thread while it waits. - A request whose timeout passes before Pump is answered Timeout and is guaranteed not to have run, so an agent never wonders whether a command went through; an Async result arrives on the Pump it is ready; requests left when the Session ends are answered ShuttingDown. - Advance() for frame loops and Paused()/TakeSteps() for fixed-step loops hold what the app gates on them; pausing an app that gates nothing is refused instead of silently doing nothing. pause --when stops the frame its condition holds; pause --on-check stops before the next Advance in the frame a check fails, optionally raising SIGTRAP for an attached debugger. - A wait that times out says what its condition's paths were; a log follower gets matching entries in order, from the backlog on, and is dropped when it cannot keep up. - Request parameters go through Codec, so a mistyped request gets the same did-you-mean replies a mistyped command does. The session sets PR_SET_PTRACER_ANY so a debugger the agent starts can attach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3500b73ecf
commit
bd64c44fc5
15 changed files with 1851 additions and 33 deletions
183
tests/ShouldAnswerRequests/main.cpp
Normal file
183
tests/ShouldAnswerRequests/main.cpp
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// The session answers requests the way a client relies on: a wrong token is
|
||||
// Unauthorized, a mistyped method gets a suggestion, hello and status are
|
||||
// answered at once, while calls, gets, sets and screenshots wait for the
|
||||
// app's Pump and run there. A request whose timeout passes before that Pump
|
||||
// is answered Timeout and is never run; a throwing handler is an Exception
|
||||
// reply; an Async result arrives on the Pump it is ready; the agent's
|
||||
// actions appear on the timeline; there is one Session per process; and
|
||||
// requests still queued when it ends are answered ShuttingDown.
|
||||
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& line) override {
|
||||
std::lock_guard lock(mutex);
|
||||
lines.push_back(line);
|
||||
return true;
|
||||
}
|
||||
std::optional<Value> Final() {
|
||||
std::lock_guard lock(mutex);
|
||||
return final;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mutex;
|
||||
std::optional<Value> final;
|
||||
std::vector<Value> lines;
|
||||
};
|
||||
|
||||
Session* session = nullptr;
|
||||
|
||||
std::shared_ptr<FakeExchange> Send(std::string_view method, Value 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", std::move(params));
|
||||
request.Set("timeoutMs", Value(timeoutMs));
|
||||
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();
|
||||
}
|
||||
|
||||
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() {
|
||||
Command("Player_Fire", "Fire.", [] { return std::string("bang"); });
|
||||
Command("Game_Crash", "Throws.", []() -> std::int32_t { throw std::runtime_error("boom"); });
|
||||
std::int32_t polls = 0;
|
||||
Command("Sim_Hash", "Ready on the second poll.", [&]() -> Async<std::int32_t> {
|
||||
return Async<std::int32_t>([&]() -> std::optional<std::int32_t> {
|
||||
if (++polls < 2) return std::nullopt;
|
||||
return 7;
|
||||
});
|
||||
});
|
||||
std::int32_t hp = 100;
|
||||
Watch("fort.hp", hp);
|
||||
const std::int32_t level = 3;
|
||||
Watch("level", level);
|
||||
|
||||
std::shared_ptr<FakeExchange> leftOver;
|
||||
{
|
||||
Session owned({ .name = "test", .listen = false });
|
||||
session = &owned;
|
||||
|
||||
auto wrongToken = std::make_shared<FakeExchange>();
|
||||
owned.Handle(Value::Parse(R"({"token": "nope", "method": "debug.status"})"), wrongToken);
|
||||
Check(CodeOf(*wrongToken) == "Unauthorized", "a wrong token is refused");
|
||||
auto notObject = std::make_shared<FakeExchange>();
|
||||
owned.Handle(Value(3), notObject);
|
||||
Check(CodeOf(*notObject) == "InvalidRequest", "a request that is not an object is refused");
|
||||
|
||||
const Value hello = ResultOf(*Send("debug.hello"));
|
||||
Check(hello.Find("protocol")->AsInteger() == 1 && hello.Find("name")->AsString() == "test", "hello names the protocol and the instance");
|
||||
const Value status = ResultOf(*Send("debug.status"));
|
||||
Check(status.Find("frame")->AsInteger() == 0 && !status.Find("paused")->AsBool() && !status.Find("pauseSupported")->AsBool(), "status is answered at once");
|
||||
const Value list = ResultOf(*Send("debug.list"));
|
||||
Check(list.Find("commands")->Size() == 3 && list.Find("watches")->Size() == 5, "list has the commands and watches, the session's own included");
|
||||
|
||||
auto typo = Send("debug.stauts");
|
||||
Check(CodeOf(*typo) == "UnknownMethod" && MessageOf(*typo).find("did you mean 'debug.status'") != std::string::npos, "a mistyped method gets a suggestion");
|
||||
auto commandTypo = Send("Player_Fier");
|
||||
Check(CodeOf(*commandTypo) == "UnknownMethod" && MessageOf(*commandTypo).find("did you mean 'Player_Fire'") != std::string::npos, "a mistyped command gets a suggestion");
|
||||
|
||||
auto fire = Send("Player_Fire");
|
||||
Check(!fire->Final(), "a call waits for Pump");
|
||||
Pump();
|
||||
Check(ResultOf(*fire).AsString() == "bang", "Pump runs the call and replies");
|
||||
|
||||
auto get = Send("debug.get", Value::Parse(R"({"paths": ["fort.hp", "lev*", "nope"]})"));
|
||||
Pump();
|
||||
const Value got = ResultOf(*get);
|
||||
Check(got.Find("values")->Find("fort.hp")->AsInteger() == 100 && got.Find("values")->Find("level")->AsInteger() == 3 && got.Find("errors")->Find("nope") != nullptr,
|
||||
"get reads paths and globs, and reports the ones it cannot");
|
||||
auto badGet = Send("debug.get", Value::Parse(R"({"pathz": []})"));
|
||||
Pump();
|
||||
Check(CodeOf(*badGet) == "InvalidParams" && MessageOf(*badGet).find("did you mean 'paths'") != std::string::npos, "a mistyped parameter gets a suggestion");
|
||||
|
||||
auto set = Send("debug.set", Value::Parse(R"({"path": "fort.hp", "value": 5})"));
|
||||
Pump();
|
||||
Check(hp == 5 && ResultOf(*set).Find("value")->AsInteger() == 5, "set writes on the Pump thread and replies with the new value");
|
||||
auto readOnly = Send("debug.set", Value::Parse(R"({"path": "level", "value": 5})"));
|
||||
Pump();
|
||||
Check(CodeOf(*readOnly) == "ReadOnly", "setting a read-only watch is refused");
|
||||
|
||||
auto crash = Send("Game_Crash");
|
||||
Pump();
|
||||
Check(CodeOf(*crash) == "Exception" && MessageOf(*crash).find("boom") != std::string::npos, "a throwing handler is an Exception reply");
|
||||
|
||||
std::int32_t fired = 0;
|
||||
Command("Count", "Counts.", [&] { ++fired; });
|
||||
auto late = Send("Count", {}, 1);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
Pump();
|
||||
Check(CodeOf(*late) == "Timeout" && MessageOf(*late).find("not run") != std::string::npos && fired == 0, "a request past its timeout before Pump is never run");
|
||||
|
||||
auto hash = Send("Sim_Hash");
|
||||
Pump();
|
||||
Check(!hash->Final(), "an Async result is not ready on its first poll");
|
||||
Pump();
|
||||
Check(ResultOf(*hash).AsInteger() == 7, "an Async result is replied on the Pump it is ready");
|
||||
|
||||
auto noShot = Send("debug.screenshot");
|
||||
Pump();
|
||||
Check(CodeOf(*noShot) == "NoCapture", "a screenshot without a capture source says so");
|
||||
SetCapture([] {
|
||||
return Async<std::vector<std::byte>>([]() -> std::optional<std::vector<std::byte>> {
|
||||
return std::vector<std::byte>{ std::byte{ 0x89 }, std::byte{ 'P' }, std::byte{ 'N' }, std::byte{ 'G' } };
|
||||
});
|
||||
});
|
||||
auto shot = Send("debug.screenshot");
|
||||
Pump();
|
||||
const auto png = Base64Decode(ResultOf(*shot).Find("png")->AsString());
|
||||
Check(png && png->size() == 4 && (*png)[1] == std::byte{ 'P' }, "a screenshot's PNG comes back as base64");
|
||||
|
||||
auto agent = Send("debug.log", Value::Parse(R"({"kinds": ["agent"], "last": 100})"));
|
||||
const Value entries = *ResultOf(*agent).Find("entries");
|
||||
Check(std::ranges::any_of(entries.Items(), [](const Value& entry) { return entry.Find("message")->AsString().starts_with("Player_Fire"); }),
|
||||
"the agent's calls are on the timeline");
|
||||
|
||||
bool threw = false;
|
||||
try {
|
||||
Session second({ .listen = false });
|
||||
} catch (const std::logic_error&) {
|
||||
threw = true;
|
||||
}
|
||||
Check(threw, "a second Session is refused");
|
||||
|
||||
leftOver = Send("Player_Fire");
|
||||
}
|
||||
Check(CodeOf(*leftOver) == "ShuttingDown", "a request still queued when the Session ends is answered ShuttingDown");
|
||||
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
123
tests/ShouldPauseAndStep/main.cpp
Normal file
123
tests/ShouldPauseAndStep/main.cpp
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// 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;
|
||||
}
|
||||
|
|
@ -57,16 +57,16 @@ int main() {
|
|||
|
||||
Check(!registry().Set("physics.gravity", Value(-3.5)) && gravity == -3.5F, "a set writes the variable");
|
||||
const auto bounded = registry().Set("physics.gravity", Value(10));
|
||||
Check(bounded && bounded->find("outside") != std::string::npos && gravity == -3.5F, "a set outside the bounds is refused");
|
||||
Check(registry().Set("build", Value(8)) == std::optional<std::string>("'build' is read-only") && build == 7, "a const variable's watch is read-only");
|
||||
Check(bounded && bounded->code == "InvalidParams" && bounded->message.find("outside") != std::string::npos && gravity == -3.5F, "a set outside the bounds is refused");
|
||||
Check(registry().Set("build", Value(8))->code == "ReadOnly" && build == 7, "a const variable's watch is read-only");
|
||||
Check(!registry().Set("bodies[0].pos.x", Value(9)) && bodies[0].pos.x == 9 && bodies[0].pos.y == 1, "a set into a member writes back the whole value");
|
||||
const auto wrongType = registry().Set("physics.gravity", Value("down"));
|
||||
Check(wrongType && wrongType->find("expected a number") != std::string::npos, "a set of the wrong type is refused");
|
||||
Check(wrongType && wrongType->message.find("expected a number") != std::string::npos, "a set of the wrong type is refused");
|
||||
|
||||
Check(!registry().Set("lives", Value(5)) && lives == 5, "a computed watch sets through its setter");
|
||||
const auto negative = registry().Set("lives", Value(-1));
|
||||
Check(negative && negative->find("lives cannot be negative") != std::string::npos && lives == 5, "a setter can refuse");
|
||||
Check(registry().Set("score", Value(1)) == std::optional<std::string>("'score' is read-only"), "a getter-only watch is read-only");
|
||||
Check(negative && negative->message.find("lives cannot be negative") != std::string::npos && lives == 5, "a setter can refuse");
|
||||
Check(registry().Set("score", Value(1))->code == "ReadOnly", "a getter-only watch is read-only");
|
||||
|
||||
const auto missing = registry().Resolve("physics.gravty");
|
||||
Check(!missing.Ok() && missing.error.find("did you mean 'physics.gravity'") != std::string::npos, "an unknown path gets a suggestion");
|
||||
|
|
|
|||
157
tests/ShouldWaitTraceAndFollow/main.cpp
Normal file
157
tests/ShouldWaitTraceAndFollow/main.cpp
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// 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<bool> 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<Value> Final() {
|
||||
std::lock_guard lock(mutex);
|
||||
return final;
|
||||
}
|
||||
std::vector<Value> Lines() {
|
||||
std::lock_guard lock(mutex);
|
||||
return lines;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mutex;
|
||||
std::optional<Value> final;
|
||||
std::vector<Value> lines;
|
||||
};
|
||||
|
||||
Session* session = nullptr;
|
||||
|
||||
std::shared_ptr<FakeExchange> 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<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();
|
||||
}
|
||||
|
||||
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<Value> 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;
|
||||
}
|
||||
Loading…
Reference in a new issue