Revise and polish the query plan viewer
This commit is contained in:
parent
362d5d1917
commit
4b94b83cef
10
Makefile
10
Makefile
@ -66,7 +66,7 @@ bench-check: ## Type-check benchmark code without running it
|
||||
fi
|
||||
|
||||
.PHONY: check
|
||||
check: format-check lint test bench-check ## Run all checks (format-check, lint, test, bench-check)
|
||||
check: format-check lint test bench-check viewer-test ## Run all checks (format-check, lint, test, bench-check, viewer-test)
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## Remove build output
|
||||
@ -99,6 +99,14 @@ export-fixtures: ## Regenerate plan JSON for every tools/exporter/examples/*.sce
|
||||
examples: export-fixtures ## Regenerate fixtures from scenarios and run them through plan-runner against their oracles.
|
||||
@cargo test -p plan-runner --test examples
|
||||
|
||||
.PHONY: viewer-test
|
||||
viewer-test: ## Check the plan viewer's JS engine against every fixture oracle (needs Node)
|
||||
@if ! command -v node >/dev/null 2>&1; then \
|
||||
echo "node not found. Skipping plan viewer engine check."; \
|
||||
else \
|
||||
node tools/plan-viewer/test.js; \
|
||||
fi
|
||||
|
||||
VIEWER_PORT ?= 8000
|
||||
|
||||
.PHONY: viewer
|
||||
|
||||
Binary file not shown.
87
tools/plan-viewer/test.js
Normal file
87
tools/plan-viewer/test.js
Normal file
@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
// Parity check for the plan viewer's JavaScript engine.
|
||||
//
|
||||
// Extracts the engine script block from index.html and runs every fixture
|
||||
// in crates/plan-runner/fixtures/ through it, requiring the result to match
|
||||
// the fixture's expected_bindings oracle, exactly as the Rust runner does.
|
||||
// Also checks the provenance helpers: every output row of every node must
|
||||
// have at least one contributing row in each of its inputs.
|
||||
//
|
||||
// The Rust crates remain the correctness oracle; this test only catches the
|
||||
// viewer drifting away from them.
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const vm = require("vm");
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, "index.html"), "utf8");
|
||||
const match = html.match(/<script id="engine">([\s\S]*?)<\/script>/);
|
||||
if (!match) {
|
||||
console.error("engine script block not found in index.html");
|
||||
process.exit(1);
|
||||
}
|
||||
const sandbox = { module: { exports: {} }, console };
|
||||
vm.runInNewContext(match[1], sandbox, { filename: "index.html#engine" });
|
||||
const engine = sandbox.module.exports;
|
||||
|
||||
const fixturesDir = path.join(__dirname, "..", "..", "crates", "plan-runner", "fixtures");
|
||||
const files = fs.readdirSync(fixturesDir).filter((f) => f.endsWith(".json")).sort();
|
||||
if (files.length === 0) {
|
||||
console.error("no fixtures found in " + fixturesDir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Every output row must have at least one contributing row in each input.
|
||||
function checkProvenance(plan, tables, results) {
|
||||
const byId = new Map(plan.query.nodes.map((n) => [n.id, n]));
|
||||
for (const node of plan.query.nodes) {
|
||||
const rel = results.get(node.id);
|
||||
for (const row of rel.rows) {
|
||||
const bindings = {};
|
||||
rel.columns.forEach((c, i) => { bindings[c] = row[i]; });
|
||||
if (node.action.scan) {
|
||||
const scan = node.action.scan;
|
||||
const matched = engine.scanMatches(tables.get(scan.table), scan.columns, bindings);
|
||||
if (matched.length === 0) {
|
||||
throw new Error("node " + node.id + ": output row has no matching fact row");
|
||||
}
|
||||
} else {
|
||||
for (const id of [node.action.join.left, node.action.join.right]) {
|
||||
const matched = engine.filterByBindings(results.get(id), bindings);
|
||||
if (matched.length === 0) {
|
||||
throw new Error(
|
||||
"node " + node.id + ": output row has no contributing row in node " + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let failures = 0;
|
||||
for (const file of files) {
|
||||
const plan = JSON.parse(fs.readFileSync(path.join(fixturesDir, file), "utf8"));
|
||||
try {
|
||||
const tables = engine.buildTables(plan);
|
||||
const results = engine.executePlan(plan, tables);
|
||||
const verdict = engine.verifyBindings(plan, results.get(plan.query.root));
|
||||
if (verdict.status !== "match") {
|
||||
throw new Error("bindings " + verdict.status +
|
||||
(verdict.message ? ": " + verdict.message : ""));
|
||||
}
|
||||
checkProvenance(plan, tables, results);
|
||||
engine.layoutQuery(plan.query);
|
||||
console.log("ok " + file);
|
||||
} catch (err) {
|
||||
failures += 1;
|
||||
console.log("FAIL " + file + " (" + err.message + ")");
|
||||
}
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
console.error(failures + " of " + files.length + " fixtures failed");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("all " + files.length + " fixtures match their oracles");
|
||||
Loading…
x
Reference in New Issue
Block a user