// Real-data test: parse every brace-block / .effect file under a data tree and // compare the canonical rendering with the reference (Python) parse. // // oracle_check [] // // is what tests/mars_parse/oracle/dump.py produced (index.txt + // one .json per file). Without it the tool only parses and counts. On a // mismatch the C++ rendering is written next to the oracle file as // .cpp.json so the two can be diffed. // // Also reports which files need the lenient recoveries (strict mode fails). #include #include #include #include #include #include #include #include #include #include "canon.h" #include "mars/parse/blocks.h" #include "mars/parse/effect.h" namespace fs = std::filesystem; namespace { bool read_file(const fs::path& p, std::string& out) { std::ifstream in(p, std::ios::binary); if (!in) return false; out.assign(std::istreambuf_iterator(in), std::istreambuf_iterator()); return true; } std::string ext_lower(const fs::path& p) { std::string e = p.extension().string(); for (char& c : e) c = static_cast(std::tolower(static_cast(c))); return e; } // Which .txt files are brace-form (mirrors verify.kind_of in the RE repo). bool is_brace_txt(const std::string& rel) { static const char* const kNamed[] = { "Data/tutorial.txt", "Data/credits.txt", "Data/Strategy/systemnames.txt", "Data/Combat/ctechvars.txt", "Data/Combat/shipai.txt", "Models/Skysphere/skydefs.txt", "Models/Skysphere/NodeSpace-skydefs.txt"}; if (rel.rfind("Scenarios/", 0) == 0) return true; for (const char* n : kNamed) if (rel == n) return true; return false; } // "" when the file is not one of ours. std::string kind_of(const std::string& rel) { std::string e = ext_lower(rel); if (e == ".weapon" || e == ".shipsection" || e == ".tech" || e == ".combat" || e == ".def" || e == ".script") return "brace:" + e.substr(1); if (e == ".effect") return "effect"; if (e == ".txt" && is_brace_txt(rel)) return "brace:txt"; return ""; } struct Entry { std::string kind; int oracle_warnings = -1; // -1: no oracle std::string rel; }; struct Stats { int files = 0, parsed = 0, failed = 0, compared = 0, matched = 0, lenient = 0; }; std::string first_diff(const std::string& a, const std::string& b) { std::size_t i = 0; while (i < a.size() && i < b.size() && a[i] == b[i]) ++i; std::size_t lo = i > 60 ? i - 60 : 0; std::ostringstream os; os << "first difference at byte " << i << "\n cpp: ..." << a.substr(lo, 140) << "\n oracle: ..." << b.substr(lo, 140); return os.str(); } } // namespace int main(int argc, char** argv) { if (argc < 2 || argc > 3) { std::fprintf(stderr, "usage: oracle_check []\n"); return 2; } const fs::path data_dir = argv[1]; const fs::path oracle_dir = argc == 3 ? fs::path(argv[2]) : fs::path(); const bool have_oracle = !oracle_dir.empty(); // Build the work list: from the oracle index when present, else by walking. std::vector work; if (have_oracle) { std::ifstream idx(oracle_dir / "index.txt"); if (!idx) { std::fprintf(stderr, "cannot read %s\n", (oracle_dir / "index.txt").c_str()); return 2; } std::string line; while (std::getline(idx, line)) { if (line.empty()) continue; std::size_t t1 = line.find('\t'), t2 = line.find('\t', t1 + 1); if (t1 == std::string::npos || t2 == std::string::npos) continue; Entry e; e.kind = line.substr(0, t1); e.oracle_warnings = std::stoi(line.substr(t1 + 1, t2 - t1 - 1)); e.rel = line.substr(t2 + 1); work.push_back(e); } } else { for (const auto& de : fs::recursive_directory_iterator(data_dir)) { if (!de.is_regular_file()) continue; std::string rel = fs::relative(de.path(), data_dir).generic_string(); std::string k = kind_of(rel); if (k.empty()) continue; work.push_back({k, -1, rel}); } } std::map by_kind; std::vector failures, divergences, lenient_files; for (const Entry& e : work) { Stats& st = by_kind[e.kind]; ++st.files; std::string src; if (!read_file(data_dir / e.rel, src)) { ++st.failed; failures.push_back(e.rel + ": cannot read"); continue; } std::string rendered; int warnings = 0; if (e.kind == "effect") { auto r = mars::parse::parse_effect(src); if (!r.ok()) { ++st.failed; failures.push_back(e.rel + ": line " + std::to_string(r.error().line) + ": " + r.error().message); continue; } rendered = canon::effect_json(r->root); } else { auto r = mars::parse::parse_blocks(src); if (!r.ok()) { ++st.failed; failures.push_back(e.rel + ": line " + std::to_string(r.error().line) + ": " + r.error().message); continue; } warnings = static_cast(r->warnings.size()); rendered = canon::blocks_json(r->root); mars::parse::Options strict; strict.strict = true; if (!mars::parse::parse_blocks(src, strict).ok()) { ++st.lenient; lenient_files.push_back(e.rel + " (" + r->warnings[0].message + ")"); } } ++st.parsed; if (!have_oracle) continue; std::string expected; if (!read_file(oracle_dir / (e.rel + ".json"), expected)) { failures.push_back(e.rel + ": oracle json missing"); continue; } ++st.compared; bool same = rendered == expected && warnings == e.oracle_warnings; if (same) { ++st.matched; } else { std::string why = warnings != e.oracle_warnings ? "warning count cpp=" + std::to_string(warnings) + " oracle=" + std::to_string(e.oracle_warnings) : first_diff(rendered, expected); divergences.push_back(e.rel + ": " + why); std::ofstream out(oracle_dir / (e.rel + ".cpp.json"), std::ios::binary); out << rendered; } } std::printf("%-18s %6s %6s %6s %8s %7s %8s\n", "kind", "files", "parsed", "failed", "compared", "match", "lenient"); Stats total; for (const auto& [k, s] : by_kind) { std::printf("%-18s %6d %6d %6d %8d %7d %8d\n", k.c_str(), s.files, s.parsed, s.failed, s.compared, s.matched, s.lenient); total.files += s.files; total.parsed += s.parsed; total.failed += s.failed; total.compared += s.compared; total.matched += s.matched; total.lenient += s.lenient; } std::printf("%-18s %6d %6d %6d %8d %7d %8d\n", "TOTAL", total.files, total.parsed, total.failed, total.compared, total.matched, total.lenient); if (!lenient_files.empty()) { std::printf("\nfiles needing lenient recovery (strict mode rejects): %zu\n", lenient_files.size()); for (const auto& f : lenient_files) std::printf(" %s\n", f.c_str()); } if (!failures.empty()) { std::printf("\nPARSE FAILURES: %zu\n", failures.size()); for (const auto& f : failures) std::printf(" %s\n", f.c_str()); } if (!divergences.empty()) { std::printf("\nDIVERGENCES FROM ORACLE: %zu\n", divergences.size()); for (const auto& d : divergences) std::printf(" %s\n", d.c_str()); } if (have_oracle) std::printf("\noracle agreement: %d / %d\n", total.matched, total.compared); if (total.files == 0) { std::printf("no data files found under %s\n", data_dir.c_str()); return 1; } return (failures.empty() && divergences.empty()) ? 0 : 1; }