SvSctOb is a StreamableHelper<SVScriptObject> -- a polymorphic pointer holding a Game::SVSOSots, which writes two variant lists each dispatched by the key item before it (xscn -> xsc, EncID -> EncObj). Neither map is on the wire; both were read out of the game's factories (see the notes repo). The shapes apply the key in both directions, so a body goes back out as whatever it came in as, and an unmodelled key still round-trips as a Node. 18 new shapes: SVSOSots, the four scenario bodies (traps / crowdefs / indsys / gmtrigger -- indsys really does serialize nothing, its Read and Write are both the shared `ret 4` stub) and the eight encounter bodies the saves exercise. The four factory ids no save carries (7 SystemKiller, 8 PuppetMaster, 14 Locust, 21 Ortgay) are deliberately NOT typed: their serializers are recovered but nothing could check a shape for them. DOpts and SVSOVonNeumann::trev are VectorHelper<Mars::String>, so read_elem / write_elem / SchemaBuilder::carr grew the std::string branch lane G listed as missing. spies2 is VectorHelper<int>: the TYPE is certain from the helper's own decorated name, but the count is 0 in all 28 systems of all four saves, so no element value has ever been observed -- the shape is a hypothesis about behaviour even though it is a fact about type. Same for SysMem and mts. Conformance 56 shapes / 657 items -> 74 / 769, still 0 MISMATCH, and every new binding is 0 wire-only and 0 shape-only. Coverage 97.1/97.2/97.2/97.6 -> 98.0/98.0/98.0/98.4 with the byte-identical round trip preserved; ratchet 95.0 -> 97.5. CD is now the only remaining region of size, and it stays opaque: the recovered 44-item Game::TurnCommands sequence cannot be aligned to the save's 35 items even as a subsequence (item 4 is 8 bytes, so a bool where the recovery says i32; and the 27 trailing ints have only 22 i32 slots to come from), which proves the no-orders diagnosis rather than assuming it. Two unit tests added that need no saves: the string-array element branch (including the empty string, which is four zero bytes and so looks like int 0) and the SvSctOb variant dispatch round trip. ctest 34/34, clean_room_check OK, test_save skips cleanly with SOTS_SAVES_DIR unset. sots_stream_schema.h unchanged: streams.py and gen_stream_schema.py were re-run and the output is byte-identical apart from the provenance line.
178 lines
8.2 KiB
C++
178 lines
8.2 KiB
C++
// Real-save test: reads every *.sav in $SOTS_SAVES_DIR (owner's data, never
|
|
// in the repo) and checks that the walker parses clean, the typed shapes
|
|
// load, both round trips are byte-identical and the RNG blob is a valid
|
|
// MT19937 state. Skips (exit 0) when the variable is unset.
|
|
//
|
|
// With SOTS_DUMP_DIR set, writes <name>.cpp.dump / <name>.cpp.summary there
|
|
// for tests/mars_stream/oracle/compare.py.
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <dirent.h>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "mars/rng/mt19937.h"
|
|
#include "mars/stream/dump.h"
|
|
#include "mars/stream/probe.h"
|
|
#include "mars/stream/save.h"
|
|
|
|
using namespace mars::stream;
|
|
|
|
static int fails = 0;
|
|
#define CHECK(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
|
++fails; \
|
|
} \
|
|
} while (0)
|
|
|
|
static std::vector<std::string> list_saves(const std::string& dir) {
|
|
std::vector<std::string> out;
|
|
DIR* d = opendir(dir.c_str());
|
|
if (!d) return out;
|
|
while (dirent* e = readdir(d)) {
|
|
std::string n = e->d_name;
|
|
if (n.size() > 4 && n.compare(n.size() - 4, 4, ".sav") == 0) out.push_back(dir + "/" + n);
|
|
}
|
|
closedir(d);
|
|
std::sort(out.begin(), out.end());
|
|
return out;
|
|
}
|
|
|
|
static void check_save(const std::string& path, const char* dump_dir) {
|
|
std::printf("== %s\n", path.c_str());
|
|
SaveDocument doc = read_save_file(path);
|
|
std::printf(" inflated %zu bytes, items %u, frames %u, resyncs %u, hint-failures %u, raw-bytes %u\n",
|
|
doc.inflated.size(), doc.stats.items, doc.stats.frames, doc.stats.resyncs, doc.stats.hint_failures,
|
|
doc.stats.raw_bytes);
|
|
std::printf(" issues: %zu error, %zu warn, %zu info\n", doc.count(Issue::Error), doc.count(Issue::Warn),
|
|
doc.count(Issue::Info));
|
|
for (const Issue& i : doc.issues)
|
|
if (i.level != Issue::Info) std::printf(" %s\n", format_issue(i).c_str());
|
|
|
|
// --- the confirmed format parses clean --------------------------------------
|
|
CHECK(doc.stats.resyncs == 0);
|
|
CHECK(doc.stats.hint_failures == 0);
|
|
CHECK(doc.count(Issue::Error) == 0);
|
|
CHECK(doc.count(Issue::Warn) == 0);
|
|
CHECK(doc.stats.raw_bytes == 2503); // only the opaque RNG blob
|
|
|
|
// --- typed shapes -------------------------------------------------------------
|
|
const auto& s = doc.game.summary;
|
|
const auto& sim = doc.game.sim;
|
|
std::printf(" summary: game='%s' turn=%d numSys=%d players=%zu\n", s.gameName.c_str(), s.turn, s.numSys,
|
|
s.players.size());
|
|
CHECK(!s.gameName.empty());
|
|
CHECK(s.turn >= 1);
|
|
CHECK(s.numSys > 0);
|
|
CHECK(!s.players.empty());
|
|
CHECK(sim.gameName == s.gameName);
|
|
CHECK(int(sim.systems.size()) == s.numSys);
|
|
CHECK(sim.systems.size() == sim.systemIds.size());
|
|
CHECK(sim.players.size() == sim.playerIds.size());
|
|
CHECK(sim.fleets.size() == sim.fleetIds.size());
|
|
CHECK(sim.species.size() == 7);
|
|
CHECK(!sim.players.empty() && !sim.players[0].player.plryName.empty());
|
|
CHECK(doc.game.createParams.name == s.gameName);
|
|
CHECK(doc.game.createParams.nSys == s.numSys);
|
|
CHECK(doc.game.createParams.mapP.planets.size() == size_t(s.numSys));
|
|
for (const auto& se : sim.systems) CHECK(!se.sys.name.empty());
|
|
for (const auto& fe : sim.fleets) CHECK(fe.flt.ships.size() >= 1);
|
|
CHECK(!doc.game.cdTable.ids.empty());
|
|
|
|
// --- RNG blob: mt[624] + left, produced by our MT19937 from RSeed --------------
|
|
CHECK(sim.rng.is_complex() && sim.rng.children.size() == 1);
|
|
const Node& blob = sim.rng.children[0];
|
|
CHECK(blob.kind == Kind::Raw && blob.raw.size() == 2503);
|
|
mars::rng::MT19937 saved(1u);
|
|
CHECK(saved.load_state(blob.raw.data(), blob.raw.size()));
|
|
std::printf(" rng: left=%d (index %d), RSeed=%d\n", saved.left(), saved.index(), doc.game.createParams.rseed);
|
|
CHECK(saved.left() >= 0 && saved.left() <= mars::rng::MT19937::N);
|
|
{
|
|
// the saved block must be reachable from seed(RSeed) by whole twists
|
|
mars::rng::MT19937 gen(uint32_t(doc.game.createParams.rseed));
|
|
int twists = -1;
|
|
for (int k = 0; k < 16 && twists < 0; ++k) {
|
|
if (std::memcmp(gen.state(), saved.state(), sizeof(uint32_t) * mars::rng::MT19937::N) == 0) twists = k;
|
|
else
|
|
for (int i = 0; i < mars::rng::MT19937::N; ++i) gen.next_u32(); // consume a block -> next twist
|
|
}
|
|
std::printf(" rng: state == seed(RSeed) after %d twist(s)\n", twists);
|
|
CHECK(twists >= 0);
|
|
}
|
|
|
|
// --- coverage: what the shapes understand vs what a Node merely carries ---------
|
|
// The round trip below is byte-identical either way, because an opaque Node is
|
|
// copied verbatim. This is the number that actually moves when a body gets typed.
|
|
{
|
|
CoverageArchive cov;
|
|
doc.game.io(cov);
|
|
size_t total = cov.typed + cov.opaque;
|
|
double pct = total ? 100.0 * double(cov.typed) / double(total) : 0.0;
|
|
std::printf(" coverage: %zu typed, %zu opaque (%.1f%% of %u stream items typed)\n", cov.typed,
|
|
cov.opaque, pct, doc.stats.items);
|
|
std::vector<std::pair<size_t, std::string>> worst;
|
|
for (const auto& kv : cov.opaque_by_tag) worst.emplace_back(kv.second, kv.first);
|
|
std::sort(worst.rbegin(), worst.rend());
|
|
std::printf(" still opaque:");
|
|
for (size_t i = 0; i < worst.size() && i < 8; ++i)
|
|
std::printf(" %s=%zu", worst[i].second.c_str(), worst[i].first);
|
|
std::printf("\n");
|
|
// Ratchet, not a target: typing a body must never silently regress.
|
|
CHECK(pct >= 97.5);
|
|
}
|
|
|
|
// --- round trips ------------------------------------------------------------------
|
|
Bytes tree_bytes = write_tree(doc.tree);
|
|
CHECK(tree_bytes == doc.inflated);
|
|
Bytes typed_bytes = write_save(doc.game);
|
|
if (typed_bytes != doc.inflated) {
|
|
size_t i = 0, n = std::min(typed_bytes.size(), doc.inflated.size());
|
|
while (i < n && typed_bytes[i] == doc.inflated[i]) ++i;
|
|
std::printf(" typed round trip differs at 0x%zx (sizes %zu vs %zu)\n", i, typed_bytes.size(),
|
|
doc.inflated.size());
|
|
}
|
|
CHECK(typed_bytes == doc.inflated);
|
|
std::printf(" round trip: tree %s, typed %s\n", tree_bytes == doc.inflated ? "identical" : "DIFFERS",
|
|
typed_bytes == doc.inflated ? "identical" : "DIFFERS");
|
|
|
|
// --- optional dump for the oracle comparison -------------------------------------
|
|
if (dump_dir) {
|
|
std::string base = path.substr(path.find_last_of('/') + 1);
|
|
std::ofstream d(std::string(dump_dir) + "/" + base + ".cpp.dump");
|
|
for (const std::string& l : dump_tree(doc.tree)) d << l << '\n';
|
|
std::ofstream sm(std::string(dump_dir) + "/" + base + ".cpp.summary");
|
|
sm << "summary: game=" << json_quote_cp1252(s.gameName) << " turn=" << s.turn << " numSys=" << s.numSys
|
|
<< " players=" << s.players.size() << '\n';
|
|
sm << "sim: players=" << sim.players.size() << " systems=" << sim.systems.size()
|
|
<< " fleets=" << sim.fleets.size() << '\n';
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
const char* dir = std::getenv("SOTS_SAVES_DIR");
|
|
if (!dir || !*dir) {
|
|
std::printf("test_save: SKIPPED (SOTS_SAVES_DIR not set)\n");
|
|
return 0;
|
|
}
|
|
std::vector<std::string> saves = list_saves(dir);
|
|
if (saves.empty()) {
|
|
std::printf("test_save: SKIPPED (no *.sav in %s)\n", dir);
|
|
return 0;
|
|
}
|
|
const char* dump_dir = std::getenv("SOTS_DUMP_DIR");
|
|
for (const std::string& p : saves) {
|
|
try {
|
|
check_save(p, dump_dir && *dump_dir ? dump_dir : nullptr);
|
|
} catch (const std::exception& e) {
|
|
std::printf("FAIL %s: %s\n", p.c_str(), e.what());
|
|
++fails;
|
|
}
|
|
}
|
|
std::printf("test_save: %s (%zu save(s), %d failures)\n", fails ? "FAILED" : "ok", saves.size(), fails);
|
|
return fails ? 1 : 0;
|
|
}
|