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