Crafter.Debug/tests/ShouldRecordTimeline/main.cpp
Jorijn van der Graaf 3500b73ecf
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>
2026-11-01 01:40:59 +01:00

105 lines
4.8 KiB
C++

// 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;
}