// The harness's own coverage gate (docs/harness-audit.md). // // The shim's hook descriptors only reach a compiler on the MinGW cross build, so the // `static_assert` in Hook<> alone would not stop an unaudited hook from landing on a host-only // CI run. This test instantiates every descriptor's Hook<> on the host -- which fires that // static_assert -- and then checks at run time that what each one declared is actually usable: // // * every hook states its coverage (never "unstated"); // * every unmodelled note carries a `what` and a `why` (an empty admission is not one); // * a note tagged `region:X` / `guard:X` names something, so the mitigation can be checked; // * the guard machinery reports a write outside the declared regions, and stays silent when // there is none. // // Add a `take<>()` line in collect() when you add a hook; the count assertion in // game_hooks_admit_their_boundary() fails until you do. #include #include #include #include #include #include "check.h" #include "shim/hooks/colony_turn.h" #include "shim/hooks/compute_budget.h" #include "shim/hooks/dictionaries.h" #include "shim/hooks/fleet_movement.h" #include "shim/hooks/global_consts.h" #include "shim/hooks/research.h" #include "shim/hooks/tech_effects.h" #include "shim/trace/hook.h" #include "shim/trace/selftest.h" using namespace shim::trace; namespace { struct Row { const char* name; Coverage cov; }; std::vector rows; // Instantiating Hook is what fires the descriptor's compile-time coverage requirement. template void take() { static_assert(sizeof(Hook) > 0, "descriptor must be usable with Hook<>"); Row r; r.name = D::name; D::coverage(r.cov); rows.push_back(std::move(r)); } void collect() { take(); take(); take(); take(); take(); take(); take(); take(); take(); take(); take(); } void every_hook_states_its_coverage() { for (const Row& r : rows) { const std::string state = r.cov.state(); if (state == "unstated") { std::printf("FAIL %s: coverage() is empty -- add notes or call complete()\n", r.name); CHECK(false); } if (state == "complete") CHECK(!r.cov.complete_why().empty()); for (const CoverageNote& n : r.cov.notes()) { if (n.what.empty() || n.why.empty()) { std::printf("FAIL %s: a coverage note has an empty what/why\n", r.name); CHECK(false); } // A mitigation that names a region or guard must actually name one. const bool tagged = n.mitigation.rfind("region:", 0) == 0 || n.mitigation.rfind("guard:", 0) == 0; if (tagged) CHECK(n.mitigation.find(':') + 1 < n.mitigation.size()); } } } // The nine game hooks are all partial by construction: each one models a slice. If one ever // claims completeness, that is a claim a Guard region has to back up -- say so loudly here. void game_hooks_admit_their_boundary() { int partial = 0; for (const Row& r : rows) { if (std::strncmp(r.name, "Shim::SelfTest::", 16) == 0) continue; CHECK(std::strcmp(r.cov.state(), "partial") == 0); CHECK(!r.cov.notes().empty()); ++partial; } CHECK_EQ(partial, 9); // bump this when a hook is added, and audit it first } // The B3 shape: a Result region that ours reproduces exactly, next to a word only the original // writes. The diff is clean; the guard is not. void guard_reports_the_undeclared_write() { using H = Hook; H::original = &shim::selftest::FillCounted; shim::selftest::Blob live{}; std::vector regions; shim::selftest::FillGuardHook::regions(regions, &live, 16, 5); CHECK_EQ(regions.size(), static_cast(2)); CHECK(regions[0].kind == Region::Kind::Result); CHECK(regions[1].kind == Region::Kind::Guard); const Snapshot before = Snapshot::capture(regions[1]); shim::selftest::FillCounted(&live, 16, 5); const Snapshot after = Snapshot::capture(regions[1]); std::vector out; std::size_t total = 0; undeclared_writes(regions[1], before, after, regions, out, total); // buf[0..16) is a declared Result region and is masked out; `calls` at offset 64 is not. CHECK_EQ(total, static_cast(1)); CHECK_EQ(out.size(), static_cast(1)); CHECK_STR(out[0].region, "blob"); CHECK_EQ(out[0].offset, offsetof(shim::selftest::Blob, calls)); CHECK(out[0].length >= 1 && out[0].length <= 4); // No movement at all -> nothing reported (a guard must not cry wolf). std::vector quiet; std::size_t none = 0; undeclared_writes(regions[1], after, after, regions, quiet, none); CHECK_EQ(none, static_cast(0)); CHECK(quiet.empty()); } // Truncation still reports an honest total. void guard_truncates_but_counts() { std::uint8_t a[64] = {}; std::uint8_t b[64] = {}; for (int i = 0; i < 64; i += 2) b[i] = 1; // 32 one-byte runs Region g; g.name = "span"; g.ptr = a; g.size = sizeof a; g.kind = Region::Kind::Guard; Snapshot before = Snapshot::capture(g); Region gb = g; gb.ptr = b; Snapshot after = Snapshot::capture(gb); after.name = before.name; std::vector all{g}; std::vector out; std::size_t total = 0; undeclared_writes(g, before, after, all, out, total, 4); CHECK_EQ(out.size(), static_cast(4)); CHECK_EQ(total, static_cast(32)); } } // namespace int main() { collect(); every_hook_states_its_coverage(); game_hooks_admit_their_boundary(); guard_reports_the_undeclared_write(); guard_truncates_but_counts(); return tracetest::finish("shim_trace_coverage"); }