feat(registry): commands, watches, samples, the timeline and conditions
The app side of Crafter.Debug, minus the session:
- Command(name, description, handler): the handler takes nothing or
one argument struct and returns void, a value, Result<T> or
Async<T>. Arguments are decoded before it runs, so a mistyped call
is answered with every mistake and never reaches it; a throwing
handler becomes an Exception error instead of unwinding into the
frame loop. This replaces 3DForts' three-file pattern per verb.
- Watch(path, variable | getter [, setter]) with bounds, help and
read-only, and Sample(path, value) from any thread with a
frame-stamped history. Paths resolve into values ("bodies[1].pos.y")
and set the same way, writing a member writes the whole value back.
Unknown paths and commands get a suggestion.
- Group: registrations under a prefix that end with their owner. Not
movable, so neither is its owner: its watches refer to the owner's
members, and a move would leave them pointing at the old object.
- Log, LogWarn, LogError, Check and Emit on one frame-stamped
timeline, with source locations captured without macros. A check
failing every frame is counted every time but stored sparsely, and
the session's listener hears every failure.
- Condition: what `wait` and `pause --when` evaluate each frame. A
condition that cannot be decided reports why instead of being false,
which would make a wait hang.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
89a12bcd61
commit
3500b73ecf
12 changed files with 1829 additions and 7 deletions
105
tests/ShouldRecordTimeline/main.cpp
Normal file
105
tests/ShouldRecordTimeline/main.cpp
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// The timeline records Log, LogWarn, LogError, Check and Emit with frame,
|
||||
// category and source location, and answers the queries `crafter-debug log`
|
||||
// makes: by level, category glob, substring, frame and sequence, newest N.
|
||||
// A check that fails every frame is counted every time but stored only
|
||||
// sparsely, so it cannot push everything else out, while the listener the
|
||||
// session pauses from still hears every failure. Appends from many threads
|
||||
// keep one increasing sequence.
|
||||
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;
|
||||
}
|
||||
|
||||
struct MatchEnded {
|
||||
std::int32_t winnerSlot = 0;
|
||||
};
|
||||
|
||||
Detail::Timeline& timeline() {
|
||||
return Detail::Timeline::Instance();
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::atomic<std::int32_t> heard = 0;
|
||||
timeline().SetListener([&](const TimelineEntry&) { heard.fetch_add(1); });
|
||||
|
||||
Detail::SetFrame(10);
|
||||
Log("net", "peer {} joined slot {}", "b1", 2);
|
||||
Detail::SetFrame(11);
|
||||
LogWarn("physics", "brace {} strain {:.1f}", 17, 4.2);
|
||||
LogError("render", "lost device");
|
||||
Emit("MatchEnded", MatchEnded{ .winnerSlot = 1 });
|
||||
|
||||
const auto all = timeline().Query({});
|
||||
Check(all.size() == 4, std::format("four entries ({})", all.size()));
|
||||
Check(all[0].message == "peer b1 joined slot 2" && all[0].category == "net" && all[0].frame == 10 && all[0].kind == EntryKind::Log,
|
||||
"a log entry has its message, category and frame");
|
||||
Check(all[0].file.ends_with("main.cpp") && all[0].line > 0, "an entry carries the caller's source location");
|
||||
Check(all[1].message == "brace 17 strain 4.2" && all[1].kind == EntryKind::Warn && all[1].frame == 11, "a warning is formatted and stamped");
|
||||
Check(all[3].kind == EntryKind::Event && all[3].category == "MatchEnded" && all[3].payload.Find("winnerSlot")->AsInteger() == 1, "an event carries its payload");
|
||||
Check(all[1].sequence > all[0].sequence, "sequences increase");
|
||||
|
||||
Detail::TimelineQuery warnings;
|
||||
warnings.minLevel = 1;
|
||||
Check(timeline().Query(warnings).size() == 2, "--level warn keeps warnings and errors");
|
||||
Detail::TimelineQuery physics;
|
||||
physics.category = "phys*";
|
||||
Check(timeline().Query(physics).size() == 1, "a category glob filters");
|
||||
Detail::TimelineQuery grep;
|
||||
grep.grep = "device";
|
||||
Check(timeline().Query(grep).size() == 1, "a substring filters");
|
||||
Detail::TimelineQuery since;
|
||||
since.sinceFrame = 11;
|
||||
Check(timeline().Query(since).size() == 3, "--since-frame filters");
|
||||
Detail::TimelineQuery after;
|
||||
after.afterSequence = all[1].sequence;
|
||||
Check(timeline().Query(after).size() == 2, "afterSequence returns only newer entries");
|
||||
Detail::TimelineQuery last;
|
||||
last.last = 2;
|
||||
const auto newest = timeline().Query(last);
|
||||
Check(newest.size() == 2 && newest.back().kind == EntryKind::Event, "last returns the newest entries, in order");
|
||||
|
||||
heard = 0;
|
||||
const std::uint64_t before = timeline().LastSequence();
|
||||
std::int32_t passed = 0;
|
||||
for (std::int32_t i = 0; i < 100; ++i) {
|
||||
if (Crafter::Debug::Check(i % 2 == 1, "physics", "body {} has mass {}", i, -1)) ++passed;
|
||||
}
|
||||
Check(passed == 50, "Check returns its condition");
|
||||
Detail::TimelineQuery checks;
|
||||
checks.afterSequence = before;
|
||||
const auto stored = timeline().Query(checks);
|
||||
Check(stored.size() == 12, std::format("50 failures at one site store 12 entries ({})", stored.size()));
|
||||
Check(stored.back().count == 32 && stored.back().kind == EntryKind::Check, "a stored check carries its site's count");
|
||||
Check(heard == 50, std::format("the listener hears every failure ({})", heard.load()));
|
||||
const auto sites = timeline().CheckSites();
|
||||
Check(sites.size() == 1 && sites[0].failures == 50 && sites[0].category == "physics", "check sites are counted");
|
||||
|
||||
const std::uint64_t start = timeline().LastSequence();
|
||||
std::vector<std::thread> threads;
|
||||
for (std::int32_t t = 0; t < 8; ++t) {
|
||||
threads.emplace_back([t] {
|
||||
for (std::int32_t i = 0; i < 200; ++i) Log("threads", "thread {} line {}", t, i);
|
||||
});
|
||||
}
|
||||
for (std::thread& thread : threads) thread.join();
|
||||
Detail::TimelineQuery fromThreads;
|
||||
fromThreads.afterSequence = start;
|
||||
const auto lines = timeline().Query(fromThreads);
|
||||
bool increasing = lines.size() == 1600;
|
||||
for (std::size_t i = 1; i < lines.size(); ++i) increasing = increasing && lines[i].sequence == lines[i - 1].sequence + 1;
|
||||
Check(increasing, "appends from eight threads keep one increasing sequence");
|
||||
|
||||
timeline().SetListener(nullptr);
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Loading…
Reference in a new issue