170 lines
6.2 KiB
C++
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");
|
|
}
|