// Conformance: every typed shape in shapes.h vs the wire schema recovered from // the game's own Mars::IStreamable serializers (include/generated/sots_stream_schema.h). // // Why a check and not a generator. The recovered table is a *specification*, not // a program: the recovery is a linear pass over the game's Write and cannot see // Write's branches, so a conditional field (StarShip's BQ2, gated by hbq) is // listed unconditionally and the sequence is a superset of any single record. // Container loops are flattened the same way. A codec driven straight off the // table would desynchronise on the first branch. The hand-written io() shapes // stay the codec — they can express the conditionals and the nesting that the // binary facts cannot supply — and this test is what proves they agree with the // binary, item for item, in order. // // SchemaProbe runs each io() with every branch taken, which is the same "all // branches" view the recovery has, so the two sequences are comparable. They // are aligned with an LCS and three numbers come out: // // matched the shape and the binary agree on tag and on-disk primitive // MISMATCH same tag, different primitive — a real bug, and a hard failure // wire-only the binary writes an item the shape does not name: either a // conditional the shape models with opt_*/when, or genuine // coverage debt // shape-only the shape names an item the recovery did not resolve // // Opaque items (ar.any / ar.raw_frame — a body carried as a Node) are counted // separately: they round-trip byte-for-byte but are not understood, and the // count is this codebase's honest coverage number. // // No game data, no saves: the whole test is the generated table plus shapes.h. #include #include #include #include #include "generated/sots_stream_schema.h" #include "mars/stream/probe.h" #include "mars/stream/shapes.h" using namespace mars::stream; namespace sh = mars::stream::shapes; static int fails = 0; #define CHECK(cond) \ do { \ if (!(cond)) { \ std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ ++fails; \ } \ } while (0) // --- primitive compatibility ------------------------------------------------- // The generated Prim is the DISK type: a member the original holds as int16 or // int8 is written by Stream::WriteInt and is I32 on the wire, so there is no // narrow integer to reconcile here. static bool prim_ok(SchemaProbe::P got, sots::wire::Prim want) { using P = SchemaProbe::P; using W = sots::wire::Prim; if (want == W::Unknown) return true; // recovery could not type it switch (got) { case P::Unknown: return true; // opaque on our side: any disk type fits case P::I32: return want == W::I32; case P::I64: return want == W::I64; case P::F32: return want == W::F32; case P::Bool: return want == W::Bool; case P::Str: return want == W::Str; case P::Frame: return want == W::Frame; case P::Raw: return want == W::Raw || want == W::I64; } return false; } struct Result { int matched = 0, mismatch = 0, wire_only = 0, shape_only = 0; int opaque = 0; }; // LCS alignment on (tag, compatible primitive). Sequences are short (<= 120). static Result align(const std::vector& a, const sots::wire::Class& c, bool verbose) { const size_t n = a.size(), m = c.count; std::vector> dp(n + 1, std::vector(m + 1, 0)); auto eq = [&](size_t i, size_t j) { return a[i].tag == c.fields[j].tag && prim_ok(a[i].prim, c.fields[j].prim); }; for (size_t i = n; i-- > 0;) for (size_t j = m; j-- > 0;) dp[i][j] = eq(i, j) ? dp[i + 1][j + 1] + 1 : std::max(dp[i + 1][j], dp[i][j + 1]); Result r; for (const SchemaProbe::Item& it : a) r.opaque += it.opaque; size_t i = 0, j = 0; while (i < n && j < m) { if (eq(i, j)) { ++r.matched; ++i; ++j; continue; } // Same tag, incompatible primitive: not a gap, a disagreement. if (a[i].tag == c.fields[j].tag) { ++r.mismatch; std::printf(" MISMATCH %-20s shape %-6s vs wire %-6s\n", a[i].tag.c_str(), probe_prim_name(a[i].prim), sots::wire::prim_name(c.fields[j].prim)); ++i; ++j; continue; } if (dp[i + 1][j] >= dp[i][j + 1]) { ++r.shape_only; if (verbose) std::printf(" shape-only %-20s %s\n", a[i].tag.c_str(), probe_prim_name(a[i].prim)); ++i; } else { ++r.wire_only; if (verbose) std::printf(" wire-only %-20s %s %s\n", c.fields[j].tag, sots::wire::prim_name(c.fields[j].prim), sots::wire::shape_name(c.fields[j].shape)); ++j; } } for (; i < n; ++i) { ++r.shape_only; if (verbose) std::printf(" shape-only %-20s %s\n", a[i].tag.c_str(), probe_prim_name(a[i].prim)); } for (; j < m; ++j) { ++r.wire_only; if (verbose) std::printf(" wire-only %-20s %s %s\n", c.fields[j].tag, sots::wire::prim_name(c.fields[j].prim), sots::wire::shape_name(c.fields[j].shape)); } return r; } static int total_matched = 0, total_mismatch = 0, total_wire_only = 0, total_opaque = 0; static int bound = 0, unresolved_classes = 0; template static void check(const char* shape_name, const char* cls) { const sots::wire::Class* c = sots::wire::find(cls); if (!c) { std::printf(" %-18s -> %-34s NOT IN TABLE\n", shape_name, cls); ++fails; return; } SchemaProbe p = SchemaProbe::of(); bool verbose = std::getenv("SOTS_WIRE_VERBOSE") != nullptr; Result r = align(p.items, *c, verbose); ++bound; total_matched += r.matched; total_mismatch += r.mismatch; total_wire_only += r.wire_only; total_opaque += r.opaque; std::printf(" %-18s -> %-34s [%-8s %2u/%-2u] shape %2zu wire %2u match %2d" " wire-only %2d shape-only %2d opaque %d\n", shape_name, cls, c->grade, c->read_agree, c->read_comparable, p.items.size(), c->count, r.matched, r.wire_only, r.shape_only, r.opaque); // A tag both sides name, typed differently, is a real disagreement between // our codec and the binary. Nothing else here is allowed to fail the build: // wire-only is conditional-field or coverage debt and is reported, not fatal. CHECK(r.mismatch == 0); } int main() { std::printf("wire schema: %zu classes, generated from the game's serializers\n\n", sots::wire::kClassCount); // --- the table's own integrity ------------------------------------------------- for (const sots::wire::Class& c : sots::wire::kClasses) { if (c.count == 0) continue; for (uint16_t k = 0; k < c.count; ++k) unresolved_classes += c.fields[k].unresolved; } std::printf("shape -> class (recovery tier, Read/Write cross-check)\n"); // Bindings taken from the campaign's own shape->Write table // (sots-re tools/serializers_golden.py DISK_ORDER, resolved through RTTI), // extended with unambiguous name matches. check("Summary", "Game::StrategyGameInfo"); check("Slot", "Game::SlotDef"); check("PlayerSettings", "Game::StrategyPlayerGameSettings"); check("PlayerColor", "Game::PlayerColorID"); check("Session", "Game::StrategySessionParams"); check("CreateParams", "Game::StrategyGameCreateParams"); check("Scrp", "Game::StrategyScriptParams"); check("MapP", "Game::StarMapParams"); check("Planet", "Game::SystemParams"); check("Sim", "Game::StrategyServer"); check("Sys", "Game::ServerSystem"); check("Player", "Game::ServerPlayer"); check("Fleet", "Game::StarFleet"); check("Ship", "Game::StarShip"); check("FlightPlan", "Game::FlightPlan"); check("Waypoint", "Game::FlightPlan::Waypoint"); check("PrisonerHold", "Game::PrisonerHold"); check("Otch", "Game::ObservedTech"); check("Owep", "Game::ObservedWeapon"); check("Odes", "Game::ObservedDesign"); check("Rts", "Game::StarSystem::OutputRates"); check("PlayerView", "Game::StarSystem::PlayerView"); check("IndependenceInfo", "Game::IndependenceInfo"); check("BuildOrder", "Game::ShipBuildOrder"); check("BuildQueue", "Game::BuildQueue"); check("Prep", "Game::PlayerReport"); check("DipStat", "Game::DiplomacyStats"); check("Alliances", "Game::PlayerAlliances"); check("NodeRoute", "Game::NodeRoute"); check("SystemEvent", "Game::SystemEvent"); check("PlayerTurnStats", "Game::PlayerTurnStats"); check("PlayerTurnHistory", "Game::PlayerTurnHistory"); check("TurnStats", "Game::GameTurnHistory"); check("PopG", "Game::PopulationGroup"); check("Population", "Game::Population"); check("Morale", "Game::Morale"); check("MoraleEvent", "Game::MoraleEvent"); // Game::SimpleNodePath is a different, 2-item type — it is what MapP.nodePaths // holds (VectorHelper), and it is empty in every real save. check("NodePath", "Game::NodePath"); // Bodies typed this round from the recovered schema (previously opaque Nodes). check("EventStorage", "Game::EventStorage"); check("TurnEvents", "Game::EventStorage::TurnEvents"); check("EventRec", "Game::EventStorage::Event"); check("FleetNameGen", "Game::FleetNameGenerator"); check("SpeciesRatios", "Game::SpeciesRatios"); check("CivilianRatios", "Game::CivilianRatios"); check("TechTree", "Game::TechTree"); check("ShipRecords", "Game::ShipRecords"); check("AIEncounterFlags", "Game::AIEncounterFlags"); check("PlayerAid", "Game::PlayerAid"); check("CommMessages", "Game::CommMessageContainer"); check("SpyReport", "Game::SpyReport"); check("SpyManager", "Game::ServerSpyManager"); check("ProjectNames", "Game::SpecialProjectNameGen"); check("TradeManager", "Game::ServerTradeManagerImpl"); check("TradeSector", "Game::ServerTradeSector"); check("ShipSectionID", "Game::ShipSectionID"); check("DesignSection", "Game::ShipDesignDef::Section"); std::printf( "\ntotals: %d shapes bound, %d items matched, %d MISMATCH, %d wire-only, " "%d opaque item(s) in bound shapes\n", bound, total_matched, total_mismatch, total_wire_only, total_opaque); std::printf("table: %d item(s) the recovery itself could not type\n", unresolved_classes); std::printf("test_wire_schema: %s (%d failures)\n", fails ? "FAILED" : "ok", fails); return fails ? 1 : 0; }