sots-engine/tests/mars_parse/oracle_check.cpp

216 lines
8 KiB
C++

// 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 <data-dir> [<oracle-dir>]
//
// <oracle-dir> 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
// <rel>.cpp.json so the two can be diffed.
//
// Also reports which files need the lenient recoveries (strict mode fails).
#include <cctype>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#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<char>(in), std::istreambuf_iterator<char>());
return true;
}
std::string ext_lower(const fs::path& p) {
std::string e = p.extension().string();
for (char& c : e) c = static_cast<char>(std::tolower(static_cast<unsigned char>(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 <data-dir> [<oracle-dir>]\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<Entry> 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<std::string, Stats> by_kind;
std::vector<std::string> 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<int>(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;
}