175 lines
6.2 KiB
TypeScript
175 lines
6.2 KiB
TypeScript
import { expect, test } from "vitest";
|
|
import { parseBlock } from "./parse.ts";
|
|
import { luaBuildStandardEnv } from "./stdlib.ts";
|
|
import { LuaEnv, LuaRuntimeError, LuaStackFrame, LuaTable } from "./runtime.ts";
|
|
import { evalStatement } from "./eval.ts";
|
|
import { readFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, resolve } from "node:path";
|
|
import { readdirSync, readFileSync } from "node:fs";
|
|
import { validateObject } from "../plugos/syscalls/jsonschema.ts";
|
|
import { extractSpaceLuaFromPageText } from "../boot_config.ts";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
// Core Lua tests (bare stdlib env)
|
|
const testFiles = readdirSync(__dirname, { recursive: true })
|
|
.filter((f): f is string => typeof f === "string" && f.endsWith("_test.lua"))
|
|
.sort();
|
|
|
|
for (const file of testFiles) {
|
|
test(`[Lua] ${file}`, async () => {
|
|
await runLuaTest(`./${file}`);
|
|
});
|
|
}
|
|
|
|
// Library API Lua tests (stdlib + jsonschema + library code pre-loaded)
|
|
const libraryApisDir = resolve(__dirname, "../../libraries/Library/Std/APIs");
|
|
const libraryTestFiles = readdirSync(libraryApisDir, { recursive: true })
|
|
.filter((f): f is string => typeof f === "string" && f.endsWith("_test.lua"))
|
|
.sort();
|
|
|
|
for (const file of libraryTestFiles) {
|
|
// A test file may declare its companion library .md explicitly via a header
|
|
// comment on (any of) its first lines, e.g. "-- companion: Code Widget.md".
|
|
// This is required for multi-word library pages that the derivation below
|
|
// cannot reach.
|
|
const testSource = readFileSync(resolve(libraryApisDir, file), "utf-8");
|
|
const companionMatch = testSource.match(/^--\s*companion:\s*(.+?)\s*$/m);
|
|
let libraryMdPath: string;
|
|
if (companionMatch) {
|
|
libraryMdPath = resolve(libraryApisDir, companionMatch[1]);
|
|
} else {
|
|
// Derive the companion library .md name from the test file name, e.g.
|
|
// "widget_sandbox_test.lua" → "Widget.md".
|
|
// NOTE: only the first snake_case segment is used, so this scheme cannot
|
|
// reach multi-word library pages (e.g. "Task State.md", "DOM.md"). Such a
|
|
// test should declare its companion path explicitly via the header comment
|
|
// above rather than relying on this derivation.
|
|
const libraryName = file
|
|
.replace(/_test\.lua$/, "")
|
|
// Convert snake_case segment to PascalCase for the .md filename, keeping
|
|
// only the first segment so "widget_sandbox" → "Widget"
|
|
.split("_")[0]
|
|
.replace(/^./, (c) => c.toUpperCase());
|
|
libraryMdPath = resolve(libraryApisDir, `${libraryName}.md`);
|
|
}
|
|
test(`[Lua] ${file}`, async () => {
|
|
await runLuaTest(resolve(libraryApisDir, file), {
|
|
isAbsolutePath: true,
|
|
preloadMdPath: libraryMdPath,
|
|
});
|
|
});
|
|
}
|
|
|
|
async function runLuaTest(
|
|
luaPath: string,
|
|
opts: { isAbsolutePath?: boolean; preloadMdPath?: string } = {},
|
|
) {
|
|
if (
|
|
typeof globalThis.client !== "undefined" &&
|
|
globalThis.client &&
|
|
typeof globalThis.client === "object"
|
|
) {
|
|
if (
|
|
!globalThis.client.config ||
|
|
typeof globalThis.client.config.get !== "function"
|
|
) {
|
|
try {
|
|
Object.defineProperty(globalThis.client, "config", {
|
|
value: {
|
|
get(_key: string, fallback: unknown) {
|
|
return fallback ?? {};
|
|
},
|
|
},
|
|
configurable: true,
|
|
writable: true,
|
|
});
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
} else {
|
|
(globalThis as any).client = {
|
|
config: {
|
|
get(_key: string, fallback: unknown) {
|
|
return fallback ?? {};
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
const resolvedPath = opts.isAbsolutePath
|
|
? luaPath
|
|
: fileURLToPath(new URL(luaPath, import.meta.url));
|
|
const luaFile = await readFile(resolvedPath, "utf-8");
|
|
const chunk = parseBlock(luaFile, {});
|
|
const env = new LuaEnv(luaBuildStandardEnv());
|
|
|
|
// If a companion .md library file is provided, extract its space-lua block(s)
|
|
// and run them in the env first, after injecting any required syscall stubs.
|
|
if (opts.preloadMdPath) {
|
|
// Provide a minimal jsonschema table so widget.new validation works
|
|
const jsonschemaTbl = new LuaTable();
|
|
await jsonschemaTbl.set("validateObject", (schema: any, obj: any) =>
|
|
validateObject(schema, obj),
|
|
);
|
|
env.set("jsonschema", jsonschemaTbl);
|
|
|
|
// Provide a minimal in-memory `config` table (normally syscall-backed) so
|
|
// library APIs that wrap config.get/config.set (e.g. command.define,
|
|
// codeWidget.define) work in the bare test env. Values are stored as their
|
|
// raw Lua values (no JS conversion) so e.g. function fields survive a
|
|
// set/get round trip.
|
|
const configStore = new Map<string, unknown>();
|
|
const pathToKey = (path: unknown): string => {
|
|
if (path instanceof LuaTable) {
|
|
const parts: string[] = [];
|
|
for (let i = 1; i <= path.length; i++) {
|
|
parts.push(String(path.get(i)));
|
|
}
|
|
return parts.join("\0");
|
|
}
|
|
// string path: support dot notation like the real config API
|
|
return String(path).split(".").join("\0");
|
|
};
|
|
const configTbl = new LuaTable();
|
|
await configTbl.set("set", (path: unknown, value: unknown) => {
|
|
configStore.set(pathToKey(path), value);
|
|
});
|
|
await configTbl.set("get", (path: unknown, defaultValue: unknown) => {
|
|
const key = pathToKey(path);
|
|
return configStore.has(key)
|
|
? configStore.get(key)
|
|
: (defaultValue ?? null);
|
|
});
|
|
env.set("config", configTbl);
|
|
|
|
let mdText: string;
|
|
try {
|
|
mdText = await readFile(opts.preloadMdPath, "utf-8");
|
|
} catch {
|
|
throw new Error(`Library preload file not found: ${opts.preloadMdPath}`);
|
|
}
|
|
const libLua = extractSpaceLuaFromPageText(mdText);
|
|
if (libLua) {
|
|
const libChunk = parseBlock(libLua, {});
|
|
const libSf = LuaStackFrame.createWithGlobalEnv(env, libChunk.ctx);
|
|
await evalStatement(libChunk, env, libSf);
|
|
}
|
|
}
|
|
|
|
const sf = LuaStackFrame.createWithGlobalEnv(env, chunk.ctx);
|
|
|
|
try {
|
|
await evalStatement(chunk, env, sf);
|
|
} catch (e: any) {
|
|
if (e instanceof LuaRuntimeError) {
|
|
console.error(`Error evaluating script:`, e.toPrettyString(luaFile));
|
|
} else {
|
|
console.error(`Error evaluating script:`, e);
|
|
}
|
|
expect(false).toBeTruthy();
|
|
}
|
|
}
|