216 lines
8 KiB
C++
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;
|
|
}
|