char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somgenerate test typedefs from typescript impleae10ac

main
5.2 KiB126 linesraw
1import {
2  ArrayPrototypePush,
3  RangeError,
4  TypeError,
5} from "ext:core/primordials";
6import { flag, optionsOf, registry, type Definition } from "./shared.js";
7import { required, usv } from "ext:web/idl.js";
8// The runner captures intrinsics before guest evaluation can poison globals.
9import "./runner.js";
10
11const DEFAULT_TIMEOUT_MS = 5000;
12
13/** Context passed to each test function; also the type of nested step contexts. */
14export interface TestContext {
15  readonly name: string;
16  /**
17   * Runs a substep sequentially and reports it indented under its parent.
18   * Resolves true when the step passed, false when it was ignored or
19   * failed; a failing step never throws, but any failed step still fails
20   * its ancestors up to the test. Steps share their test's timeout;
21   * `only` and `timeout` options are rejected. Only top-level tests count
22   * toward file totals.
23   */
24  step(name: string, fn: (t: TestContext) => void | Promise<void>): Promise<boolean>;
25  step(options: {
26    name: string;
27    fn?: (t: TestContext) => void | Promise<void>;
28    ignore?: boolean;
29  }): Promise<boolean>;
30}
31export interface TestOptions {
32  name: string;
33  /** Required unless `ignore` is true. */
34  fn?: (t: TestContext) => void | Promise<void>;
35  ignore?: boolean;
36  /** When any test in a file sets this, the rest of the file is filtered out. */
37  only?: boolean;
38  /**
39   * Milliseconds before the test fails; defaults to 5000, 0 requires the
40   * body to settle synchronously, past 2147483647 is rejected, and
41   * Infinity disables the per-test timeout (the file-level 60s progress
42   * budget still applies). Covers the test body and all of its steps
43   * together, including steps never awaited.
44   */
45  timeout?: number;
46}
47
48function checkTimeout(value: unknown): number {
49  if (typeof value !== "number" || value !== value) {
50    throw new TypeError("Test timeout must be a number of milliseconds");
51  }
52  if (value < 0) {
53    throw new RangeError("Test timeout must be zero or more milliseconds");
54  }
55  // Timers truncate to 32 bits, so larger finite values would fire immediately.
56  if (value !== Infinity && value > 2147483647) {
57    throw new RangeError(
58      "Test timeout must be at most 2147483647ms; use Infinity for no timeout",
59    );
60  }
61  return value;
62}
63
64function parseTest(nameOrOptions: unknown, fn: unknown, only: boolean, ignore: boolean, count: number): Definition {
65  required(count, 1);
66  let name, func = fn, timeout = DEFAULT_TIMEOUT_MS;
67  if (typeof nameOrOptions === "string") {
68    // An undefined function falls through to the missing-function check so
69    // that test.ignore('name') reads naturally.
70    if (count > 2 || (count === 2 && fn !== undefined && typeof fn !== "function")) {
71      throw new TypeError("Expected test(name, fn)");
72    }
73    name = usv(nameOrOptions);
74  } else {
75    if (count > 1) throw new TypeError("Expected test(options)");
76    const options = optionsOf(nameOrOptions, ["name", "fn", "ignore", "only", "timeout"], "test");
77    if (options.name === undefined) throw new TypeError("Test requires a name");
78    name = usv(options.name);
79    func = options.fn;
80    ignore = flag(options, "ignore", "Test") || ignore;
81    only = flag(options, "only", "Test") || only;
82    if (options.timeout !== undefined) timeout = checkTimeout(options.timeout);
83  }
84  if (func === undefined) {
85    if (!ignore) throw new TypeError(`Test "${name}" requires a function`);
86  } else if (typeof func !== "function") {
87    throw new TypeError(`Test "${name}" requires a function`);
88  }
89  return { name, fn: func, ignore, only, timeout } as Definition;
90}
91
92function push(nameOrOptions: unknown, fn: unknown, only: boolean, ignore: boolean, count: number): void {
93  // Registrations after collection would silently never run.
94  if (registry.sealed) throw new TypeError("test() must be called while the test file evaluates");
95  ArrayPrototypePush(registry.definitions, parseTest(nameOrOptions, fn, only, ignore, count));
96}
97
98const registration = function test(nameOrOptions: unknown, fn?: unknown): void {
99  push(nameOrOptions, fn, false, false, arguments.length);
100};
101registration.only = function only(nameOrOptions: unknown, fn?: unknown): void {
102  push(nameOrOptions, fn, true, false, arguments.length);
103};
104registration.ignore = function ignore(nameOrOptions: unknown, fn?: unknown): void {
105  push(nameOrOptions, fn, false, true, arguments.length);
106};
107
108
109/**
110 * Registers a test in the current file. Only `avril test` runs registered
111 * tests; under `run` and `serve` registration is inert. Each file runs in
112 * its own fresh isolate, tests within a file run sequentially in
113 * registration order, and a `pledge()` inside one test narrows every test
114 * after it in the same file. There are no resource or timer sanitizers.
115 */
116export const test: (
117  ((name: string, fn: (t: TestContext) => void | Promise<void>) => void) &
118  ((options: TestOptions) => void)
119) & {
120  /** Registers a test that runs only when some test in its file uses `only`. */
121  only: ((name: string, fn: (t: TestContext) => void | Promise<void>) => void) &
122    ((options: TestOptions) => void);
123  /** Registers a test that is collected but never runs. */
124  ignore: ((name: string, fn?: (t: TestContext) => void | Promise<void>) => void) &
125    ((options: TestOptions) => void);
126} = registration;