105 lines
4.8 KiB
C++
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;
|
||
|
|
}
|