#!/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: () => {}, children: [] }), createDocumentFragment: () => ({ appendChild: () => {}, children: [], childNodes: [] }), 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 // Use --full flag to load a full-spec build (if available) const fullBuild = process.argv.includes("--full"); const jsPath = fullBuild ? path.join(__dirname, "..", "..", "shared", "static", "scripts", "sx-full-test.js") : path.join(__dirname, "..", "..", "shared", "static", "scripts", "sx-browser.js"); if (fullBuild && !fs.existsSync(jsPath)) { console.error("Full test build not found. Run: python3 hosts/javascript/cli.py --extensions continuations --spec-modules types --output shared/static/scripts/sx-full-test.js"); process.exit(1); } const Sx = require(jsPath); if (!Sx || !Sx.parse) { console.error("Failed to load Sx evaluator"); process.exit(1); } // Reset render mode — boot process may have set it to true if (Sx.setRenderActive) Sx.setRenderActive(false); // 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-bind!"] = function(e, k, v) { if (e) e[k] = v; return v; }; env["env-set!"] = function(e, k, v) { if (e) e[k] = v; return v; }; env["env-extend"] = function(e) { return Object.create(e); }; env["env-merge"] = function(a, b) { return Object.assign({}, a, b); }; // Missing primitives referenced by tests // primitive? is now in platform.py PRIMITIVES env["upcase"] = function(s) { return s.toUpperCase(); }; env["downcase"] = function(s) { return s.toLowerCase(); }; env["make-keyword"] = function(name) { return new Sx.Keyword(name); }; env["string-length"] = function(s) { return s.length; }; env["dict-get"] = function(d, k) { return d && d[k] !== undefined ? d[k] : null; }; env["apply"] = function(f) { var args = Array.prototype.slice.call(arguments, 1); var lastArg = args.pop(); if (Array.isArray(lastArg)) args = args.concat(lastArg); return f.apply(null, args); }; // 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) { // Continuation must be callable as a function AND have _continuation flag var c = function(v) { return fn(v !== undefined ? v : null); }; c._continuation = true; c.fn = fn; c.call = function(v) { return fn(v !== undefined ? v : null); }; return c; }; env["continuation?"] = function(x) { return x != null && x._continuation === true; }; env["continuation-fn"] = function(c) { return c.fn; }; // Render helpers // render-html: the tests call this with an SX source string, parse it, and render to HTML // IMPORTANT: renderToHtml sets a global _renderMode flag but never resets it. // We must reset it after each call so subsequent eval calls don't go through the render path. env["render-html"] = function(src, e) { var result; if (typeof src === "string") { var parsed = Sx.parse(src); if (!parsed || parsed.length === 0) return ""; var expr = parsed.length === 1 ? parsed[0] : [{ name: "do" }].concat(parsed); if (Sx.renderToHtml) { result = Sx.renderToHtml(expr, e || (Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {})); } else { result = Sx.serialize(expr); } } else { if (Sx.renderToHtml) { result = Sx.renderToHtml(src, e || env); } else { result = Sx.serialize(src); } } // Reset render mode so subsequent eval calls don't go through DOM/HTML render path if (Sx.setRenderActive) Sx.setRenderActive(false); return result; }; // Also register render-to-html directly env["render-to-html"] = env["render-html"]; // Type system helpers — available when types module is included // test-prim-types: dict of primitive return types for type inference env["test-prim-types"] = function() { return { "+": "number", "-": "number", "*": "number", "/": "number", "mod": "number", "inc": "number", "dec": "number", "abs": "number", "min": "number", "max": "number", "floor": "number", "ceil": "number", "round": "number", "str": "string", "upper": "string", "lower": "string", "trim": "string", "join": "string", "replace": "string", "format": "string", "substr": "string", "=": "boolean", "<": "boolean", ">": "boolean", "<=": "boolean", ">=": "boolean", "!=": "boolean", "not": "boolean", "nil?": "boolean", "empty?": "boolean", "number?": "boolean", "string?": "boolean", "boolean?": "boolean", "list?": "boolean", "dict?": "boolean", "symbol?": "boolean", "keyword?": "boolean", "contains?": "boolean", "has-key?": "boolean", "starts-with?": "boolean", "ends-with?": "boolean", "len": "number", "first": "any", "rest": "list", "last": "any", "nth": "any", "cons": "list", "append": "list", "concat": "list", "reverse": "list", "sort": "list", "slice": "list", "range": "list", "flatten": "list", "keys": "list", "vals": "list", "map-dict": "dict", "assoc": "dict", "dissoc": "dict", "merge": "dict", "dict": "dict", "get": "any", "type-of": "string", }; }; // test-prim-param-types: dict of primitive param type specs env["test-prim-param-types"] = function() { return { "+": {"positional": [["a", "number"]], "rest-type": "number"}, "-": {"positional": [["a", "number"]], "rest-type": "number"}, "*": {"positional": [["a", "number"]], "rest-type": "number"}, "/": {"positional": [["a", "number"]], "rest-type": "number"}, "inc": {"positional": [["n", "number"]], "rest-type": null}, "dec": {"positional": [["n", "number"]], "rest-type": null}, "upper": {"positional": [["s", "string"]], "rest-type": null}, "lower": {"positional": [["s", "string"]], "rest-type": null}, "keys": {"positional": [["d", "dict"]], "rest-type": null}, "vals": {"positional": [["d", "dict"]], "rest-type": null}, }; }; // Component type accessors env["component-param-types"] = function(c) { return c && c._paramTypes ? c._paramTypes : null; }; env["component-set-param-types!"] = function(c, t) { if (c) c._paramTypes = t; return null; }; env["component-params"] = function(c) { return c && c.params ? c.params : null; }; env["component-body"] = function(c) { return c && c.body ? c.body : null; }; env["component-has-children"] = function(c) { return c && c.has_children ? c.has_children : false; }; // Aser test helper: parse SX source, evaluate via aser, return wire format string env["render-sx"] = function(source) { const exprs = Sx.parse(source); const parts = []; for (const expr of exprs) { const result = Sx.renderToSx(expr, env); if (result !== null && result !== undefined && result !== Sx.NIL) { parts.push(typeof result === "string" ? result : Sx.serialize(result)); } } return parts.join(""); }; // 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 libTests = path.join(projectDir, "lib", "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); } // Load compiler + VM from lib/ when running full tests if (fullBuild) { const libDir = path.join(projectDir, "lib"); for (const libFile of ["bytecode.sx", "compiler.sx", "vm.sx"]) { const libPath = path.join(libDir, libFile); if (fs.existsSync(libPath)) { const src = fs.readFileSync(libPath, "utf8"); const exprs = Sx.parse(src); for (const expr of exprs) { try { Sx.eval(expr, env); } catch (e) { console.error(`Error loading ${libFile}: ${e.message}`); } } } } } // Determine which tests to run const args = process.argv.slice(2).filter(a => !a.startsWith("--")); let testFiles = []; if (args.length > 0) { // Specific test files — search spec, lib, and web test dirs for (const arg of args) { const name = arg.endsWith(".sx") ? arg : `${arg}.sx`; const specPath = path.join(specTests, name); const libPath = path.join(libTests, name); const webPath = path.join(webTests, name); if (fs.existsSync(specPath)) testFiles.push(specPath); else if (fs.existsSync(libPath)) testFiles.push(libPath); else if (fs.existsSync(webPath)) testFiles.push(webPath); else console.error(`Test file not found: ${name}`); } } else { // All spec tests (core language — always run) 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)); } } // Library tests (only with --full — require compiler, vm, signals, etc.) if (fullBuild) { for (const f of fs.readdirSync(libTests).sort()) { if (f.startsWith("test-") && f.endsWith(".sx")) { testFiles.push(path.join(libTests, 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);