Add Node.js test harness for spec tests
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 10s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 10s
hosts/javascript/run_tests.js — loads sx-browser.js in Node, provides test platform functions, runs spec/tests/*.sx. 40/43 CEK tests pass (3 continuation tests need extension). 178/328 total spec tests pass — remaining failures are missing env bindings (equal?, continuation helpers, etc). Usage: node hosts/javascript/run_tests.js [test-name] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
191
hosts/javascript/run_tests.js
Normal file
191
hosts/javascript/run_tests.js
Normal file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Run SX spec tests in Node.js using the bootstrapped evaluator.
|
||||
*
|
||||
* Usage:
|
||||
* node hosts/javascript/run_tests.js # all spec tests
|
||||
* node hosts/javascript/run_tests.js test-primitives # specific test
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Provide globals that sx-browser.js expects
|
||||
global.window = global;
|
||||
global.addEventListener = () => {};
|
||||
global.self = global;
|
||||
global.document = {
|
||||
createElement: () => ({ style: {}, setAttribute: () => {}, appendChild: () => {} }),
|
||||
head: { appendChild: () => {} },
|
||||
body: { appendChild: () => {} },
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
createTextNode: (s) => ({ textContent: s }),
|
||||
addEventListener: () => {},
|
||||
};
|
||||
global.localStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
|
||||
global.CustomEvent = class CustomEvent { constructor(n, o) { this.type = n; this.detail = (o||{}).detail||{}; } };
|
||||
global.MutationObserver = class { observe() {} disconnect() {} };
|
||||
global.requestIdleCallback = (fn) => setTimeout(fn, 0);
|
||||
global.matchMedia = () => ({ matches: false });
|
||||
global.navigator = { serviceWorker: { register: () => Promise.resolve() } };
|
||||
global.location = { href: "", pathname: "/", hostname: "localhost" };
|
||||
global.history = { pushState: () => {}, replaceState: () => {} };
|
||||
global.fetch = () => Promise.resolve({ ok: true, text: () => Promise.resolve("") });
|
||||
global.setTimeout = setTimeout;
|
||||
global.clearTimeout = clearTimeout;
|
||||
global.console = console;
|
||||
|
||||
// Load the bootstrapped evaluator
|
||||
const jsPath = path.join(__dirname, "..", "..", "shared", "static", "scripts", "sx-browser.js");
|
||||
const Sx = require(jsPath);
|
||||
if (!Sx || !Sx.parse) {
|
||||
console.error("Failed to load Sx evaluator");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Test infrastructure
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
const suiteStack = [];
|
||||
|
||||
// Build env with all primitives + spec functions
|
||||
const env = Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {};
|
||||
|
||||
// Additional test helpers needed by spec tests
|
||||
env["sx-parse"] = function(s) { return Sx.parse(s); };
|
||||
env["sx-parse-one"] = function(s) { const r = Sx.parse(s); return r && r.length > 0 ? r[0] : null; };
|
||||
env["test-env"] = function() { return Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {}; };
|
||||
env["cek-eval"] = function(s) {
|
||||
const parsed = Sx.parse(s);
|
||||
if (!parsed || parsed.length === 0) return null;
|
||||
return Sx.eval(parsed[0], Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {});
|
||||
};
|
||||
env["eval-expr-cek"] = function(expr, e) { return Sx.eval(expr, e || env); };
|
||||
env["env-get"] = function(e, k) { return e && e[k] !== undefined ? e[k] : null; };
|
||||
env["env-has?"] = function(e, k) { return e && k in e; };
|
||||
env["env-set!"] = function(e, k, v) { if (e) e[k] = v; return v; };
|
||||
env["env-extend"] = function(e) { return Object.assign({}, e); };
|
||||
env["env-merge"] = function(a, b) { return Object.assign({}, a, b); };
|
||||
|
||||
// Deep equality
|
||||
function deepEqual(a, b) {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return a == b;
|
||||
if (typeof a !== typeof b) return false;
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((v, i) => deepEqual(v, b[i]));
|
||||
}
|
||||
if (typeof a === "object") {
|
||||
const ka = Object.keys(a).filter(k => k !== "_nil");
|
||||
const kb = Object.keys(b).filter(k => k !== "_nil");
|
||||
if (ka.length !== kb.length) return false;
|
||||
return ka.every(k => deepEqual(a[k], b[k]));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
env["equal?"] = deepEqual;
|
||||
env["identical?"] = function(a, b) { return a === b; };
|
||||
|
||||
// Continuation support
|
||||
env["make-continuation"] = function(fn) {
|
||||
const c = { fn: fn, _continuation: true, call: function(v) { return fn(v); } };
|
||||
return c;
|
||||
};
|
||||
env["continuation?"] = function(x) { return x != null && x._continuation === true; };
|
||||
env["continuation-fn"] = function(c) { return c.fn; };
|
||||
|
||||
// Platform test functions
|
||||
env["try-call"] = function(thunk) {
|
||||
try {
|
||||
Sx.eval([thunk], env);
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message || String(e) };
|
||||
}
|
||||
};
|
||||
|
||||
env["report-pass"] = function(name) {
|
||||
passCount++;
|
||||
const ctx = suiteStack.join(" > ");
|
||||
console.log(` PASS: ${ctx} > ${name}`);
|
||||
return null;
|
||||
};
|
||||
|
||||
env["report-fail"] = function(name, error) {
|
||||
failCount++;
|
||||
const ctx = suiteStack.join(" > ");
|
||||
console.log(` FAIL: ${ctx} > ${name}: ${error}`);
|
||||
return null;
|
||||
};
|
||||
|
||||
env["push-suite"] = function(name) {
|
||||
suiteStack.push(name);
|
||||
console.log(`${" ".repeat(suiteStack.length - 1)}Suite: ${name}`);
|
||||
return null;
|
||||
};
|
||||
|
||||
env["pop-suite"] = function() {
|
||||
suiteStack.pop();
|
||||
return null;
|
||||
};
|
||||
|
||||
// Load test framework
|
||||
const projectDir = path.join(__dirname, "..", "..");
|
||||
const specTests = path.join(projectDir, "spec", "tests");
|
||||
const webTests = path.join(projectDir, "web", "tests");
|
||||
|
||||
const frameworkSrc = fs.readFileSync(path.join(specTests, "test-framework.sx"), "utf8");
|
||||
const frameworkExprs = Sx.parse(frameworkSrc);
|
||||
for (const expr of frameworkExprs) {
|
||||
Sx.eval(expr, env);
|
||||
}
|
||||
|
||||
// Determine which tests to run
|
||||
const args = process.argv.slice(2);
|
||||
let testFiles = [];
|
||||
|
||||
if (args.length > 0) {
|
||||
// Specific test files
|
||||
for (const arg of args) {
|
||||
const name = arg.endsWith(".sx") ? arg : `${arg}.sx`;
|
||||
const specPath = path.join(specTests, name);
|
||||
const webPath = path.join(webTests, name);
|
||||
if (fs.existsSync(specPath)) testFiles.push(specPath);
|
||||
else if (fs.existsSync(webPath)) testFiles.push(webPath);
|
||||
else console.error(`Test file not found: ${name}`);
|
||||
}
|
||||
} else {
|
||||
// All spec tests
|
||||
for (const f of fs.readdirSync(specTests).sort()) {
|
||||
if (f.startsWith("test-") && f.endsWith(".sx") && f !== "test-framework.sx") {
|
||||
testFiles.push(path.join(specTests, f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for (const testFile of testFiles) {
|
||||
const name = path.basename(testFile);
|
||||
console.log("=" .repeat(60));
|
||||
console.log(`Running ${name}`);
|
||||
console.log("=" .repeat(60));
|
||||
|
||||
try {
|
||||
const src = fs.readFileSync(testFile, "utf8");
|
||||
const exprs = Sx.parse(src);
|
||||
for (const expr of exprs) {
|
||||
Sx.eval(expr, env);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`ERROR in ${name}: ${e.message}`);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log("=" .repeat(60));
|
||||
console.log(`Results: ${passCount} passed, ${failCount} failed`);
|
||||
console.log("=" .repeat(60));
|
||||
|
||||
process.exit(failCount > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user