// Drive one strategic turn over every save in $SOTS_SAVES_DIR and assert the properties // the standalone must hold whatever the phase coverage is: // // 1. an untouched load re-serialises byte-identically (the foundation the diff rests on); // 2. a turn never breaks the file -- the post-turn state still round-trips; // 3. the counters that ARE modelled moved, and in the right direction; // 4. no phase committed a write while its inputs were declared missing; // 5. the generator is left alone unless asked for. // // Skips cleanly (exit 0) when the variable is unset. No .sav ever enters this repo. #include #include #include #include #include #include #include "app/report.h" #include "app/turn.h" #include "mars/stream/save.h" static int failures = 0; #define CHECK(c) \ do { \ if (!(c)) { \ std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #c); \ ++failures; \ } \ } while (0) int main() { const char* dir = std::getenv("SOTS_SAVES_DIR"); if (!dir || !*dir) { std::printf("app_test_turn: SOTS_SAVES_DIR unset, skipped\n"); return 0; } DIR* d = opendir(dir); if (!d) { std::fprintf(stderr, "app_test_turn: cannot open %s\n", dir); return 1; } std::vector saves; while (struct dirent* e = readdir(d)) { const std::string n = e->d_name; if (n.size() > 4 && n.compare(n.size() - 4, 4, ".sav") == 0) saves.push_back(std::string(dir) + "/" + n); } closedir(d); if (saves.empty()) { std::printf("app_test_turn: no .sav in %s, skipped\n", dir); return 0; } int ran = 0; for (const std::string& path : saves) { mars::stream::SaveDocument doc; try { doc = mars::stream::read_save_file(path); } catch (const std::exception& ex) { std::printf(" %s: unreadable (%s), skipped\n", path.c_str(), ex.what()); continue; } if (doc.count(mars::stream::Issue::Error)) { std::printf(" %s: parse errors, skipped\n", path.c_str()); continue; } ++ran; // 1. the foundation CHECK(mars::stream::write_save(doc.game) == doc.inflated); const int frame0 = doc.game.sim.frame; const int mod0 = doc.game.sim.modCount; const mars::stream::Node rng0 = doc.game.sim.rng; sots::app::TurnOptions opt; // defaults: commit nothing that is not fully modelled const sots::app::TurnResult r = sots::app::RunStrategicTurn(doc.game, opt); // 2. the turn did not break the file mars::stream::Bytes after = mars::stream::write_save(doc.game); CHECK(!after.empty()); mars::stream::SaveDocument again = mars::stream::read_save_bytes(after.data(), after.size()); CHECK(again.count(mars::stream::Issue::Error) == 0); CHECK(mars::stream::write_save(again.game) == after); // 3. modelled counters moved CHECK(doc.game.sim.modCount > mod0); CHECK(doc.game.summary.turn == doc.game.sim.frame); CHECK(doc.game.sim.frame == frame0 + 1); // 4. blocked phases stayed blocked for (const auto& rec : r.records) { if (rec.desc->status == sots::app::PhaseStatus::Blocked) CHECK(rec.leafWrites == 0); if (rec.desc->status == sots::app::PhaseStatus::Stub) { CHECK(rec.leafWrites == 0); CHECK(rec.rngWords == 0); } } // 5. the generator is untouched by default CHECK(doc.game.sim.rng.children.size() == rng0.children.size()); if (!doc.game.sim.rng.children.empty() && !rng0.children.empty()) CHECK(doc.game.sim.rng.children[0].raw == rng0.children[0].raw); std::printf(" %s: %d leaf write(s), %d blocked, %d rng word(s)\n", path.c_str(), r.leafWrites, r.wouldWrite, r.rngWords); } std::printf("app_test_turn: %d save(s) driven, %d failure(s)\n", ran, failures); return failures ? 1 : 0; }