#!/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);