Files

170 lines
7.0 KiB
TypeScript

// Runs one vendored test262 file verbatim. The harness (assert, Test262Error, compareArray, $DONE,
// $DONOTEVALUATE) is host-provided because programs cannot attach properties to functions, which
// test262's own assert.js relies on.
import path from "node:path"
import { Effect } from "effect"
import { materialize } from "../../src/interpreter/errors.js"
import { executeProgram } from "../../src/interpreter/execute.js"
import type { Interpreter } from "../../src/interpreter/interpreter.js"
import { Throw } from "../../src/interpreter/model.js"
import { createErrorValue } from "../../src/interpreter/intrinsics.js"
import { constructor, fn, methods } from "../../src/interpreter/native.js"
import { Callable, define, get, hidden, Arr, Fn, Obj, type Value } from "../../src/interpreter/objects.js"
import { ToolRuntime } from "../../src/tool-runtime.js"
export const root = import.meta.dir
/** Files that fail on a known gap, mapped to the gap; see skipped.txt. */
export const skipped = new Map(
(await Bun.file(path.join(root, "skipped.txt")).text())
.split("\n")
.filter((line) => line.includes("#"))
.map((line) => [line.slice(0, line.indexOf("#")).trim(), line.slice(line.indexOf("#") + 1).trim()]),
)
export type Outcome = { readonly status: "pass" } | { readonly status: "fail"; readonly reason: string }
type Frontmatter = {
flags?: Array<string>
negative?: { phase: "parse" | "resolution" | "runtime"; type: string }
}
const limits = { timeoutMs: 5000, maxToolCalls: undefined, maxOutputBytes: undefined }
const prepared = ToolRuntime.prepare<never>({})
export const run = async (file: string): Promise<Outcome> => {
const source = await Bun.file(path.join(root, file)).text()
const start = source.indexOf("/*---")
const meta = Bun.YAML.parse(source.slice(start + 5, source.indexOf("---*/", start))) as Frontmatter
let done: { error: unknown } | undefined
// Sloppy-only tests are never vendored, so every file runs as the strict half of test262's two-mode run.
// A host drains the job queue after an async test's script ends; the program end interrupts un-awaited
// work instead, so drain explicitly.
const drain = meta.flags?.includes("async") ? "\nfor (let i = 0; i < 100; i++) await null" : ""
const result = await Effect.runPromise(
executeProgram(`"use strict";\n${source}${drain}`, prepared, limits, {}, (ctx) =>
harness(ctx, (error) => {
done ??= { error }
}),
),
)
if (meta.negative !== undefined) {
// Runtime negatives only check that execution failed: diagnostics do not carry the error name.
if (result.ok) return { status: "fail", reason: `expected ${meta.negative.type} but completed` }
if (meta.negative.phase === "runtime" || result.error.kind === "ParseError") return { status: "pass" }
return {
status: "fail",
reason: `expected ${meta.negative.type} but got ${result.error.kind}: ${result.error.message}`,
}
}
if (!result.ok) return { status: "fail", reason: `${result.error.kind}: ${result.error.message}` }
if (!meta.flags?.includes("async")) return { status: "pass" }
if (done === undefined) return { status: "fail", reason: "$DONE was never called" }
if (done.error !== undefined) return { status: "fail", reason: `$DONE: ${show(done.error)}` }
return { status: "pass" }
}
const harness = <R>(ctx: Interpreter<R>, onDone: (error: Value) => void): ReadonlyArray<readonly [string, Value]> => {
const builtins = ctx.builtins
const test262Prototype = new Obj(builtins.Object)
define(test262Prototype, "name", "Test262Error", hidden)
const test262 = (args: Array<unknown>) =>
createErrorValue(test262Prototype, args[0] === undefined ? "" : String(args[0]))
const fail = (message: string) => {
throw new Throw(test262([message]))
}
const prefix = (message: unknown) => (message === undefined ? "" : `${String(message)} `)
const compare = (a: unknown, b: unknown) =>
a instanceof Arr &&
b instanceof Arr &&
a.items.length === b.items.length &&
a.items.every((value, i) => Object.is(value, b.items[i]))
const test262Error = constructor(builtins, test262Prototype, {
name: "Test262Error",
call: (_, args) => test262(args),
construct: (args) => test262(args),
})
methods(builtins, test262Error, [["thrower", 1, (_, args) => fail(String(args[0]))]])
const compareArray = fn(builtins, "compareArray", 2, (_, args) => compare(args[0], args[1]))
methods(builtins, compareArray, [["format", 1, (_, args) => show(args[0])]])
const assert = fn(builtins, "assert", 2, (_, args) =>
args[0] === true
? undefined
: fail(args[1] === undefined ? `Expected true but got ${show(args[0])}` : String(args[1])),
)
methods(builtins, assert, [
[
"sameValue",
3,
(_, args) =>
Object.is(args[0], args[1])
? undefined
: fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be true`),
],
[
"notSameValue",
3,
(_, args) =>
Object.is(args[0], args[1])
? fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be false`)
: undefined,
],
[
"compareArray",
3,
(_, args) =>
compare(args[0], args[1])
? undefined
: fail(
`Actual ${show(args[0])} and expected ${show(args[1])} should have the same contents. ${prefix(args[2])}`,
),
],
[
"throws",
3,
function* (_, args) {
const expected = args[0] instanceof Callable ? String(get(args[0], "name")) : show(args[0])
try {
yield* ctx.call(args[1], undefined, [])
} catch (error) {
const thrown = materialize(ctx, error)
if (!(thrown instanceof Obj)) return fail(`${prefix(args[2])}Thrown value was not an object!`)
const actual = get(thrown, "constructor")
if (actual === args[0]) return undefined
return fail(`${prefix(args[2])}Expected a ${expected} but got a ${show(actual)}`)
}
return fail(`${prefix(args[2])}Expected a ${expected} to be thrown but no exception was thrown at all`)
},
],
])
return [
["assert", assert],
["compareArray", compareArray],
["Test262Error", test262Error],
[
"$DONE",
fn(builtins, "$DONE", 1, (_, args) => {
onDone(args[0])
return undefined
}),
],
[
"$DONOTEVALUATE",
fn(builtins, "$DONOTEVALUATE", 0, () => {
throw new Throw("Test262: This statement should not be evaluated.")
}),
],
]
}
const show = (value: unknown): string => {
if (typeof value === "string") return JSON.stringify(value)
if (Object.is(value, -0)) return "-0"
if (value instanceof Arr) return `[${value.items.map(show).join(", ")}]`
if (value instanceof Fn) return "program function"
if (value instanceof Callable) return String(get(value, "name"))
if (!(value instanceof Obj)) return String(value)
const message = get(value, "message")
return typeof message === "string" ? `${String(get(value, "name") ?? "object")}: ${message}` : "object"
}