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
|
fi
|
||||||
|
|
||||||
.PHONY: check
|
.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
|
.PHONY: clean
|
||||||
clean: ## Remove build output
|
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.
|
examples: export-fixtures ## Regenerate fixtures from scenarios and run them through plan-runner against their oracles.
|
||||||
@cargo test -p plan-runner --test examples
|
@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
|
VIEWER_PORT ?= 8000
|
||||||
|
|
||||||
.PHONY: viewer
|
.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