sots-engine/tests/shim_trace/test_coverage.cpp

170 lines
6.2 KiB
C++

// 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 <cstddef>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#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<Row> rows;
// Instantiating Hook<D> is what fires the descriptor's compile-time coverage requirement.
template <class D>
void take() {
static_assert(sizeof(Hook<D>) > 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<shim::hooks::GlobalConstsLoadFileHook>();
take<shim::hooks::WeaponDictionaryInitHook>();
take<shim::hooks::SectionDictionaryCtorHook>();
take<shim::hooks::ComputeBudgetHook>();
take<shim::hooks::TechTreeProcessResearchHook>();
take<shim::hooks::ServerPlayerOnTechResearchedHook>();
take<shim::hooks::ServerSystemProcessTurnHook>();
take<shim::hooks::StrategyServerMoveFleetHook>();
take<shim::hooks::StrategyServerProcessFleetMovementHook>();
take<shim::selftest::FillHook>();
take<shim::selftest::FillGuardHook>();
}
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<shim::selftest::FillGuardHook>;
H::original = &shim::selftest::FillCounted;
shim::selftest::Blob live{};
std::vector<Region> regions;
shim::selftest::FillGuardHook::regions(regions, &live, 16, 5);
CHECK_EQ(regions.size(), static_cast<std::size_t>(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<UndeclaredWrite> 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<std::size_t>(1));
CHECK_EQ(out.size(), static_cast<std::size_t>(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<UndeclaredWrite> quiet;
std::size_t none = 0;
undeclared_writes(regions[1], after, after, regions, quiet, none);
CHECK_EQ(none, static_cast<std::size_t>(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<Region> all{g};
std::vector<UndeclaredWrite> out;
std::size_t total = 0;
undeclared_writes(g, before, after, all, out, total, 4);
CHECK_EQ(out.size(), static_cast<std::size_t>(4));
CHECK_EQ(total, static_cast<std::size_t>(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");
}