From 1800b80316ada451ede2d311562904f0ea16c267 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 15 Mar 2026 10:22:00 +0000 Subject: [PATCH] Add Node.js test harness for spec tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- hosts/javascript/run_tests.js | 191 ++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 hosts/javascript/run_tests.js diff --git a/hosts/javascript/run_tests.js b/hosts/javascript/run_tests.js new file mode 100644 index 0000000..fadbf89 --- /dev/null +++ b/hosts/javascript/run_tests.js @@ -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);