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>
192 lines
6.2 KiB
JavaScript
192 lines
6.2 KiB
JavaScript
#!/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);
|