mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
feat(editor): add --bulk-validate to run every format's validator across a tree
Recursively walks a directory, peeks each file's 4-byte magic to identify the format, derives the per-format --validate-X flag from the format table's --info-X entry, and invokes that validator as a subprocess for each file. Reports total / passed / failed / skipped counts and lists the failure paths with their exit codes; returns exit 1 if any failure is found. For each failure it prints the exact follow-up command needed to reproduce the detailed error message, so the user doesn't have to remember which validator goes with which extension. Asset- style formats with no validator (.wom / .wob / .whm world geometry) are counted in the "skipped" bucket but don't fail the run. Composes with the existing audit/fix tooling: --audit-tree dir # find header-level breakage --magic-fix dir --apply # auto-fix ext/magic mismatches --bulk-validate dir # run every per-format validator # then re-run --validate-X on individual failures for detail This is the 12th cross-format utility — depth-checking that catches per-format semantic errors (duplicate ids, invalid enums, contradictory flag combos, dangling cross-refs) that --audit-tree's header-only scan can't see. CLI flag count 950 -> 951.
This commit is contained in:
parent
9318b6c006
commit
f606edc4c9
6 changed files with 208 additions and 1 deletions
191
tools/editor/cli_bulk_validate.cpp
Normal file
191
tools/editor/cli_bulk_validate.cpp
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
#include "cli_bulk_validate.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_format_table.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <sys/wait.h>
|
||||
#endif
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct PerFile {
|
||||
fs::path path;
|
||||
const FormatMagicEntry* fmt = nullptr;
|
||||
int exitCode = 0; // 0 = OK, anything else = validator complained
|
||||
bool skipped = false; // file is a known asset format with no validator
|
||||
};
|
||||
|
||||
bool peekMagic(const fs::path& path, char magic[4]) {
|
||||
std::ifstream is(path, std::ios::binary);
|
||||
if (!is) return false;
|
||||
if (!is.read(magic, 4) || is.gcount() != 4) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wrap a single argument in single quotes for /bin/sh,
|
||||
// escaping any embedded single quotes via the standard
|
||||
// '"'"' incantation. Used so paths with spaces /
|
||||
// apostrophes still work when handed to std::system().
|
||||
std::string shellQuote(const std::string& s) {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 2);
|
||||
out.push_back('\'');
|
||||
for (char c : s) {
|
||||
if (c == '\'') out += "'\"'\"'";
|
||||
else out.push_back(c);
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
|
||||
// Derive the per-format --validate-X flag from the
|
||||
// --info-X flag in the format table. Both share the same
|
||||
// magic suffix (e.g. --info-wsrg -> --validate-wsrg).
|
||||
std::string deriveValidateFlag(const char* infoFlag) {
|
||||
if (!infoFlag) return {};
|
||||
std::string s = infoFlag;
|
||||
const std::string prefix = "--info-";
|
||||
if (s.size() < prefix.size() ||
|
||||
s.compare(0, prefix.size(), prefix) != 0) {
|
||||
return {};
|
||||
}
|
||||
return "--validate-" + s.substr(prefix.size());
|
||||
}
|
||||
|
||||
int handleBulk(int& i, int argc, char** argv) {
|
||||
std::string dir = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
if (!fs::exists(dir) || !fs::is_directory(dir)) {
|
||||
std::fprintf(stderr,
|
||||
"bulk-validate: not a directory: %s\n", dir.c_str());
|
||||
return 1;
|
||||
}
|
||||
// argv[0] is this binary's invocation path — needed
|
||||
// so each file can be validated via a fresh subprocess
|
||||
// call, isolating one file's failures from another's.
|
||||
std::string self = argv[0];
|
||||
std::vector<PerFile> rows;
|
||||
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
char magic[4];
|
||||
if (!peekMagic(entry.path(), magic)) continue;
|
||||
const FormatMagicEntry* fmt = findFormatByMagic(magic);
|
||||
if (!fmt) continue; // non-Wowee file
|
||||
PerFile pf;
|
||||
pf.path = entry.path();
|
||||
pf.fmt = fmt;
|
||||
std::string validateFlag = deriveValidateFlag(fmt->infoFlag);
|
||||
if (validateFlag.empty()) {
|
||||
// Asset-style format with no validator hooked
|
||||
// up — count it but don't try to invoke.
|
||||
pf.skipped = true;
|
||||
rows.push_back(std::move(pf));
|
||||
continue;
|
||||
}
|
||||
std::string cmd = shellQuote(self) + " " +
|
||||
validateFlag + " " +
|
||||
shellQuote(entry.path().string()) +
|
||||
" >/dev/null 2>&1";
|
||||
int rc = std::system(cmd.c_str());
|
||||
// std::system returns the wait-status; on POSIX
|
||||
// WEXITSTATUS extracts the actual program exit code.
|
||||
#ifdef _WIN32
|
||||
pf.exitCode = rc;
|
||||
#else
|
||||
if (rc == -1) pf.exitCode = -1;
|
||||
else if (WIFEXITED(rc)) pf.exitCode = WEXITSTATUS(rc);
|
||||
else pf.exitCode = 1;
|
||||
#endif
|
||||
rows.push_back(std::move(pf));
|
||||
}
|
||||
size_t total = rows.size();
|
||||
size_t okCount = 0;
|
||||
size_t failCount = 0;
|
||||
size_t skipCount = 0;
|
||||
for (const auto& r : rows) {
|
||||
if (r.skipped) ++skipCount;
|
||||
else if (r.exitCode == 0) ++okCount;
|
||||
else ++failCount;
|
||||
}
|
||||
bool ok = (failCount == 0);
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["dir"] = dir;
|
||||
j["total"] = total;
|
||||
j["ok"] = okCount;
|
||||
j["failed"] = failCount;
|
||||
j["skipped"] = skipCount;
|
||||
j["allOk"] = ok;
|
||||
nlohmann::json failArr = nlohmann::json::array();
|
||||
for (const auto& r : rows) {
|
||||
if (r.skipped || r.exitCode == 0) continue;
|
||||
failArr.push_back({
|
||||
{"path", fs::relative(r.path, dir).string()},
|
||||
{"format", std::string(r.fmt->extension)},
|
||||
{"exitCode", r.exitCode},
|
||||
});
|
||||
}
|
||||
j["failures"] = failArr;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
std::printf("bulk-validate: %s\n", dir.c_str());
|
||||
std::printf(" total recognized : %zu\n", total);
|
||||
std::printf(" passed : %zu\n", okCount);
|
||||
std::printf(" failed : %zu\n", failCount);
|
||||
std::printf(" skipped (no val) : %zu\n", skipCount);
|
||||
if (ok) {
|
||||
std::printf(" OK — every catalog with a validator passed\n");
|
||||
return 0;
|
||||
}
|
||||
std::printf("\n failures:\n");
|
||||
for (const auto& r : rows) {
|
||||
if (r.skipped || r.exitCode == 0) continue;
|
||||
std::printf(" %s [%s, exit %d]\n",
|
||||
fs::relative(r.path, dir).string().c_str(),
|
||||
r.fmt->extension, r.exitCode);
|
||||
}
|
||||
std::printf("\n re-run the per-format validator on a failure for "
|
||||
"details, e.g.:\n");
|
||||
// Show one example so the user knows the followup
|
||||
// command. Pick the first failure.
|
||||
for (const auto& r : rows) {
|
||||
if (r.skipped || r.exitCode == 0) continue;
|
||||
std::string flag = deriveValidateFlag(r.fmt->infoFlag);
|
||||
std::printf(" %s %s %s\n",
|
||||
self.c_str(), flag.c_str(),
|
||||
r.path.string().c_str());
|
||||
break;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool handleBulkValidate(int& i, int argc, char** argv, int& outRc) {
|
||||
if (std::strcmp(argv[i], "--bulk-validate") == 0 && i + 1 < argc) {
|
||||
outRc = handleBulk(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue