// Run SX test specs against sx-browser.js. // // sx-browser.js parses and evaluates test specs — SX tests itself. // This script provides only platform functions (error catching, reporting). // // Usage: // node shared/sx/tests/run.js # run all available specs // node shared/sx/tests/run.js eval # run only test-eval.sx // node shared/sx/tests/run.js eval parser router # run specific specs // node shared/sx/tests/run.js --legacy # run monolithic test.sx Object.defineProperty(globalThis, "document", { value: undefined, writable: true }); var path = require("path"); var fs = require("fs"); var Sx = require(path.resolve(__dirname, "../../static/scripts/sx-browser.js")); // --- Test state --- var suiteStack = []; var passed = 0, failed = 0, testNum = 0; // --- Helpers --- function isNil(x) { return x === Sx.NIL || x === null || x === undefined; } function deepEqual(a, b) { if (a === b) return true; if (isNil(a) && isNil(b)) return true; if (typeof a !== typeof b) return false; if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; for (var i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false; return true; } if (a && typeof a === "object" && b && typeof b === "object") { var ka = Object.keys(a), kb = Object.keys(b); if (ka.length !== kb.length) return false; for (var j = 0; j < ka.length; j++) if (!deepEqual(a[ka[j]], b[ka[j]])) return false; return true; } return false; } // --- Platform functions injected into the SX env --- var env = { // Error catching — calls an SX thunk, returns result dict "try-call": function(thunk) { try { Sx.eval([thunk], env); return { ok: true }; } catch(e) { return { ok: false, error: e.message || String(e) }; } }, // Test reporting "report-pass": function(name) { testNum++; passed++; var fullName = suiteStack.concat([name]).join(" > "); console.log("ok " + testNum + " - " + fullName); }, "report-fail": function(name, error) { testNum++; failed++; var fullName = suiteStack.concat([name]).join(" > "); console.log("not ok " + testNum + " - " + fullName); console.log(" # " + error); }, // Suite context "push-suite": function(name) { suiteStack.push(name); }, "pop-suite": function() { suiteStack.pop(); }, // Primitives that sx-browser.js has internally but doesn't expose through env "equal?": function(a, b) { return deepEqual(a, b); }, "eq?": function(a, b) { return a === b; }, "boolean?": function(x) { return typeof x === "boolean"; }, "string-length": function(s) { return String(s).length; }, "substring": function(s, start, end) { return String(s).slice(start, end); }, "string-contains?": function(s, needle) { return String(s).indexOf(needle) !== -1; }, "upcase": function(s) { return String(s).toUpperCase(); }, "downcase": function(s) { return String(s).toLowerCase(); }, "reverse": function(c) { return c ? c.slice().reverse() : []; }, "flatten": function(c) { var r = []; for (var i = 0; i < (c||[]).length; i++) { if (Array.isArray(c[i])) for (var j = 0; j < c[i].length; j++) r.push(c[i][j]); else r.push(c[i]); } return r; }, "has-key?": function(d, k) { return d && typeof d === "object" && k in d; }, "append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); }, "for-each-indexed": function(f, coll) { for (var i = 0; i < (coll||[]).length; i++) { Sx.eval([f, i, coll[i]], env); } }, "dict-set!": function(d, k, v) { if (d) d[k] = v; }, "dict-has?": function(d, k) { return d && typeof d === "object" && k in d; }, "dict-get": function(d, k) { return d ? d[k] : undefined; }, "starts-with?": function(s, prefix) { return String(s).indexOf(prefix) === 0; }, "ends-with?": function(s, suffix) { var str = String(s); return str.indexOf(suffix) === str.length - suffix.length; }, "slice": function(s, start, end) { return end !== undefined ? s.slice(start, end) : s.slice(start); }, "inc": function(n) { return n + 1; }, "append!": function(arr, item) { if (Array.isArray(arr)) arr.push(item); }, "dict": function() { return {}; }, "for-each": function(f, coll) { for (var i = 0; i < (coll||[]).length; i++) { Sx.eval([f, coll[i]], env); } }, // --- Parser platform functions (for test-parser.sx) --- "sx-parse": function(source) { return Sx.parseAll(source); }, "sx-serialize": function(val) { // Basic serializer for test roundtrips if (val === Sx.NIL || val === null || val === undefined) return "nil"; if (typeof val === "boolean") return val ? "true" : "false"; if (typeof val === "number") return String(val); if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; // Check Symbol/Keyword BEFORE generic object — they are objects too if (val && (val._sym || val._sx_symbol)) return val.name; if (val && (val._kw || val._sx_keyword)) return ":" + val.name; if (Array.isArray(val)) return "(" + val.map(function(x) { return env["sx-serialize"](x); }).join(" ") + ")"; if (val && typeof val === "object") { var parts = []; Object.keys(val).forEach(function(k) { parts.push(":" + k); parts.push(env["sx-serialize"](val[k])); }); return "{" + parts.join(" ") + "}"; } return String(val); }, "make-symbol": function(name) { return Sx.sym ? Sx.sym(name) : { _sx_symbol: true, name: name, toString: function() { return name; } }; }, "make-keyword": function(name) { return Sx.kw ? Sx.kw(name) : { _sx_keyword: true, name: name, toString: function() { return name; } }; }, "symbol-name": function(s) { return s && s.name ? s.name : String(s); }, "keyword-name": function(k) { return k && k.name ? k.name : String(k); }, // --- Render platform function (for test-render.sx) --- "render-html": function(sxSource) { if (!Sx.renderToHtml) throw new Error("render-to-html not available — html adapter not bootstrapped"); var exprs = Sx.parseAll(sxSource); var result = ""; for (var i = 0; i < exprs.length; i++) { result += Sx.renderToHtml(exprs[i], env); } return result; }, }; // --- Resolve which test specs to run --- var refDir = path.resolve(__dirname, "../ref"); var args = process.argv.slice(2); // Available spec modules and their platform requirements var SPECS = { "eval": { file: "test-eval.sx", needs: [] }, "parser": { file: "test-parser.sx", needs: ["sx-parse"] }, "router": { file: "test-router.sx", needs: [] }, "render": { file: "test-render.sx", needs: ["render-html"] }, "deps": { file: "test-deps.sx", needs: [] }, "engine": { file: "test-engine.sx", needs: [] }, }; function evalFile(filename) { var filepath = path.resolve(refDir, filename); if (!fs.existsSync(filepath)) { console.log("# SKIP " + filename + " (file not found)"); return; } var src = fs.readFileSync(filepath, "utf8"); var exprs = Sx.parseAll(src); for (var i = 0; i < exprs.length; i++) { Sx.eval(exprs[i], env); } } // Legacy mode — run monolithic test.sx if (args[0] === "--legacy") { console.log("TAP version 13"); evalFile("test.sx"); } else { // Determine which specs to run var specsToRun; if (args.length > 0) { specsToRun = args; } else { // Auto-discover: run all specs whose platform functions are available specsToRun = Object.keys(SPECS); } console.log("TAP version 13"); // Always load framework first evalFile("test-framework.sx"); // Load router.sx if testing router (it defines the functions being tested) for (var si = 0; si < specsToRun.length; si++) { var specName = specsToRun[si]; var spec = SPECS[specName]; if (!spec) { console.log("# SKIP unknown spec: " + specName); continue; } // Check platform requirements var canRun = true; for (var ni = 0; ni < spec.needs.length; ni++) { if (!(spec.needs[ni] in env)) { console.log("# SKIP " + specName + " (missing: " + spec.needs[ni] + ")"); canRun = false; break; } } if (!canRun) continue; // Load prerequisite spec modules if (specName === "router") { if (Sx.splitPathSegments) { env["split-path-segments"] = Sx.splitPathSegments; env["parse-route-pattern"] = Sx.parseRoutePattern; env["match-route-segments"] = Sx.matchRouteSegments; env["match-route"] = Sx.matchRoute; env["find-matching-route"] = Sx.findMatchingRoute; env["make-route-segment"] = Sx.makeRouteSegment; } else { evalFile("router.sx"); } } if (specName === "deps") { if (Sx.scanRefs) { env["scan-refs"] = Sx.scanRefs; env["scan-components-from-source"] = Sx.scanComponentsFromSource; env["transitive-deps"] = Sx.transitiveDeps; env["compute-all-deps"] = Sx.computeAllDeps; env["components-needed"] = Sx.componentsNeeded; env["page-component-bundle"] = Sx.pageComponentBundle; env["page-css-classes"] = Sx.pageCssClasses; env["scan-io-refs"] = Sx.scanIoRefs; env["transitive-io-refs"] = Sx.transitiveIoRefs; env["compute-all-io-refs"] = Sx.computeAllIoRefs; env["component-pure?"] = Sx.componentPure_p; env["test-env"] = function() { return env; }; } } if (specName === "engine") { if (Sx.parseTime) { env["parse-time"] = Sx.parseTime; env["parse-trigger-spec"] = Sx.parseTriggerSpec; env["default-trigger"] = Sx.defaultTrigger; env["parse-swap-spec"] = Sx.parseSwapSpec; env["parse-retry-spec"] = Sx.parseRetrySpec; env["next-retry-ms"] = function(cur, cap) { return Math.min(cur * 2, cap); }; env["filter-params"] = Sx.filterParams; } } console.log("# --- " + specName + " ---"); evalFile(spec.file); } } // --- Summary --- console.log(""); console.log("1.." + testNum); console.log("# tests " + (passed + failed)); console.log("# pass " + passed); if (failed > 0) { console.log("# fail " + failed); process.exit(1); }