/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/node/polyfills/testing.ts
3 589 строк
106 KB
Bartek Iwańczuk
fix(ext/node): implement node:test tags (#36292)
26 июл 2026, 12:52
Не верифицирован
26 июл 2026, 12:52
bec84a0
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. (function () { "use strict"; const { core, primordials } = __bootstrap; const { ArrayIsArray, ArrayPrototypeForEach, ArrayPrototypeIncludes, ArrayPrototypeIndexOf, ArrayPrototypeJoin, ArrayPrototypeLastIndexOf, ArrayPrototypePush, ArrayPrototypeSlice, ArrayPrototypeSplice, DatePrototypeGetTime, DatePrototypeToString, Error, ErrorPrototype, JSONStringify, MapPrototypeClear, MapPrototypeDelete, MapPrototypeGet, MapPrototypeHas, MapPrototypeSet, NumberIsFinite, NumberIsInteger, ObjectDefineProperty, ObjectKeys, ObjectPrototypeHasOwnProperty, ObjectGetOwnPropertyDescriptor, ObjectGetPrototypeOf, ObjectPrototypeIsPrototypeOf, Promise, PromisePrototypeThen, PromiseResolve, PromiseWithResolvers, Proxy, ReflectApply, ReflectConstruct, ReflectGet, RegExpPrototypeExec, RegExpPrototypeTest, SafeArrayIterator, SafeMap, SafeMapIterator, SafeRegExp, String, StringPrototypeEndsWith, StringPrototypeIncludes, StringPrototypeIndexOf, StringPrototypeMatch, StringPrototypeSlice, StringPrototypeSplit, StringPrototypeToLowerCase, StringPrototypeStartsWith, Symbol, SymbolDispose, SymbolFor, SymbolToPrimitive, TypeError, queueMicrotask, } = primordials; // The genuine timer functions, captured once at runtime so that an enabled mock // clock (node:test's mock.timers) cannot stop a test's timeout from firing in // real time. We cannot read these at module-init time: this polyfill is baked // into the startup snapshot, where the module body runs before the timer APIs // exist on `globalThis`. Instead we capture lazily on the first `test()` / // `suite()` registration (see installErrorHandlers), which always runs after // the runtime is booted and before any test body can call mock.timers.enable(). let realSetTimeout = null; let realClearTimeout = null; function ensureRealTimers() { if (realSetTimeout === null) { realSetTimeout = globalThis.setTimeout; realClearTimeout = globalThis.clearTimeout; } } let errorHandlersInstalled = false; let activeNodeTests = 0; let pendingCallbackReject = null; // Stack of failure "sinks" for the tests whose bodies are currently executing. // A test pushes a sink for the duration of its body (across await boundaries) // and pops it when the body settles. An unhandled rejection or uncaught // exception that fires while a test body is running is attributed to the // innermost still-running test, matching Node's behavior of failing the // currently-active test (see https://github.com/denoland/deno/issues/34818). const activeTestSinks = []; function pushTestSink(sink) { ArrayPrototypePush(activeTestSinks, sink); } function popTestSink(sink) { const idx = ArrayPrototypeLastIndexOf(activeTestSinks, sink); if (idx !== -1) { ArrayPrototypeSplice(activeTestSinks, idx, 1); } } // The innermost test whose body is still running, or null when no test body is // currently on the stack (e.g. between registration and execution). function currentTestSink() { for (let i = activeTestSinks.length - 1; i >= 0; i--) { const sink = activeTestSinks[i]; if (!sink.settled) return sink; } return null; } function buildTimeoutError(timeout) { // Node fails a timed-out test with a `test timed out after Nms` cause and an // ERR_TEST_FAILURE wrapper. We surface the same message and tag the error so // reporters that look at code/failureType behave like Node. const err = new Error(`test timed out after ${timeout}ms`); err.code = "ERR_TEST_FAILURE"; err.failureType = "testTimeoutFailure"; return err; } function buildAbortError(signal) { // When the test is aborted via a caller-supplied signal, Node fails the test // with the signal's reason (or a generic abort error when none was given). if (signal && signal.reason !== undefined) { return signal.reason; } const err = new Error("The test was aborted"); err.code = "ABORT_ERR"; err.failureType = "testAborted"; return err; } function sanitizeThrowValue(err) { if (err === null || err === undefined || typeof err !== "object") { return err; } if (ObjectPrototypeIsPrototypeOf(ErrorPrototype, err)) { return err; } const inspectSymbol = SymbolFor("nodejs.util.inspect.custom"); if (typeof err[inspectSymbol] !== "function") { return err; } try { Deno.inspect(err); return err; } catch { return new Error( "test threw a non-Error object with a throwing custom inspect", ); } } function installErrorHandlers() { // Capture the genuine timers now, before any test body runs and before any // mock clock can replace the globals. ensureRealTimers(); if (errorHandlersInstalled) return; errorHandlersInstalled = true; globalThis.addEventListener("unhandledrejection", (event) => { // Attribute the rejection to the currently-running test so it fails, the // way Node does. This is gated on an active sink rather than on // `activeNodeTests` so it also works in TAP mode, where tests are run by // our own runner and the counter is not used. const sink = currentTestSink(); if (sink !== null) { event.preventDefault(); sink.fail(event.reason); return; } // Preserve the prior behavior of swallowing rejections while Deno.test // backed node tests are pending but no body is actively running. if (activeNodeTests > 0) { event.preventDefault(); } }); globalThis.addEventListener("error", (event) => { // Uncaught exceptions are not routed through the test sink: a synchronous // throw in a test body is already caught by the body wrapper, and Node does // not treat every uncaught `error` event (for example warnings surfaced as // errors) as a test failure. We keep the original behavior of swallowing // the error while node tests are pending and forwarding to a pending // callback-style `done` reject. if (activeNodeTests > 0) { event.preventDefault(); } if (pendingCallbackReject !== null) { pendingCallbackReject(event.error ?? new Error("uncaught error")); pendingCallbackReject = null; } }); } const { validateBoolean, validateFunction, validateInteger, validateNumber, validateObject, validateString, validateStringArray, } = core.loadExtScript("ext:deno_node/internal/validators.mjs"); const nodeErrors = core.loadExtScript("ext:deno_node/internal/errors.ts"); const { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, } = nodeErrors.codes; // `ERR_INVALID_STATE` is a hand-written class exported at the top level of the // errors module; unlike the generated codes it is not registered on `.codes`. const { ERR_INVALID_STATE } = nodeErrors; const { emitExperimentalWarning } = core.loadExtScript( "ext:deno_node/internal/util.mjs", ); const { default: assert } = core.loadExtScript("ext:deno_node/assert.ts"); const { tapEscape, tapIndent, } = core.loadExtScript("ext:deno_node/internal/test/reporters.ts"); const methodsToCopy = [ "deepEqual", "deepStrictEqual", "doesNotMatch", "doesNotReject", "doesNotThrow", "equal", "fail", "ifError", "match", "notDeepEqual", "notDeepStrictEqual", "notEqual", "notStrictEqual", "partialDeepStrictEqual", "rejects", "strictEqual", "throws", "ok", ]; let assertObject = undefined; function getAssertObject() { if (assertObject === undefined) { assertObject = { __proto__: null }; ArrayPrototypeForEach(methodsToCopy, (method) => { assertObject[method] = assert[method]; }); assertObject.fileSnapshot = fileSnapshot; } return assertObject; } // Lazy access so `node:fs` and `node:path` polyfills are pulled in only on the // first `fileSnapshot()` call, mirroring the other lazy loaders in this file // and avoiding circular init during snapshot build. let _fsForSnapshot = null; function getFsForSnapshot() { if (_fsForSnapshot === null) { _fsForSnapshot = core.loadExtScript("ext:deno_node/fs.ts"); } return _fsForSnapshot; } let _pathForSnapshot = null; function getPathForSnapshot() { if (_pathForSnapshot === null) { _pathForSnapshot = core.loadExtScript("ext:deno_node/path/mod.ts"); } return _pathForSnapshot; } // Resolve update-snapshot mode lazily so the env / Rust op is read at most // once. We accept either Deno's own `--update-snapshots` flag (when running // under `deno test`) or Node's `--test-update-snapshots` propagated via // NODE_OPTIONS, so the polyfill behaves the same way the rest of the Node // compat surface does for reporter detection above. // // The deno_test extension's ops are registered at runtime - after this // polyfill's snapshot is built - so they are not on the captured `core.ops`. // They are however reachable via `Deno[Deno.internal].core.ops`, which the // test runner exposes for cli/js code; we look them up lazily through there. let _fileSnapshotUpdateMode = undefined; function isFileSnapshotUpdateMode() { if (_fileSnapshotUpdateMode !== undefined) return _fileSnapshotUpdateMode; const denoInternal = globalThis.Deno?.[globalThis.Deno.internal]; const op = denoInternal?.core?.ops?.op_test_snapshot_in_update_mode; if (typeof op === "function") { try { if (op()) { _fileSnapshotUpdateMode = true; return true; } } catch { /* op not wired up; not running under `deno test` */ } } let nodeOptions = ""; try { nodeOptions = globalThis.Deno?.env?.get("NODE_OPTIONS") || ""; } catch { /* permission denied */ } if ( nodeOptions && RegExpPrototypeTest( new SafeRegExp(/(?:^|\s)--test-update-snapshots(?:\s|=|$)/), nodeOptions, ) ) { _fileSnapshotUpdateMode = true; return true; } _fileSnapshotUpdateMode = false; return false; } // Default serializer pipeline: matches Node's `t.assert.fileSnapshot` default // (`JSON.stringify(value, null, 2)`). function defaultFileSnapshotSerializer(value) { return JSONStringify(value, null, 2); } // `t.assert.fileSnapshot(value, path[, options])`. // // Without `--test-update-snapshots`, serializes `value` and compares against // the contents of the file at `path` using `assert.strictEqual`. With the // flag, writes the serialized value to `path` (creating parent directories // as needed). Both file paths are CWD-relative, matching Node. function fileSnapshot(actual, path, options) { validateString(path, "path"); if (options === undefined) { options = { __proto__: null }; } else { validateObject(options, "options"); } let serializers; if (options.serializers === undefined) { serializers = [defaultFileSnapshotSerializer]; } else { if (!ArrayIsArray(options.serializers)) { throw new ERR_INVALID_ARG_TYPE( "options.serializers", "Array", options.serializers, ); } serializers = options.serializers; for (let i = 0; i < serializers.length; i++) { if (typeof serializers[i] !== "function") { throw new ERR_INVALID_ARG_TYPE( `options.serializers[${i}]`, "function", serializers[i], ); } } } let value = actual; try { for (let i = 0; i < serializers.length; i++) { value = serializers[i](value); } } catch (err) { const e = new ERR_INVALID_STATE( "The provided serializers did not generate a string.", ); e.cause = err; e.input = actual; throw e; } if (typeof value !== "string") { const e = new ERR_INVALID_STATE( "The provided serializers did not generate a string.", ); e.input = actual; throw e; } const fs = getFsForSnapshot(); if (isFileSnapshotUpdateMode()) { try { const parent = getPathForSnapshot().dirname(path); fs.mkdirSync(parent, { __proto__: null, recursive: true }); fs.writeFileSync(path, value, "utf8"); } catch (err) { const e = new ERR_INVALID_STATE( `Cannot write snapshot file '${path}'.`, ); e.cause = err; e.filename = path; throw e; } return; } let expected; try { expected = fs.readFileSync(path, "utf8"); } catch (err) { const isMissing = err && err.code === "ENOENT"; const message = isMissing ? `Cannot read snapshot file '${path}'. Missing snapshots can be ` + "generated by rerunning the command with the --test-update-snapshots " + "flag." : `Cannot read snapshot file '${path}'.`; const e = new ERR_INVALID_STATE(message); e.cause = err; e.filename = path; throw e; } assert.strictEqual(value, expected); } // Lazy access to other node polyfills; loading these eagerly at module // init causes circular initialization issues during snapshotting. let _Readable = null; function getReadable() { if (_Readable === null) { _Readable = core.loadExtScript( "ext:deno_node/internal/streams/readable.js", ).Readable; } return _Readable; } let _fsWatch = null; function getFsWatch() { if (_fsWatch === null) { _fsWatch = core.loadExtScript("ext:deno_node/fs.ts").watch; } return _fsWatch; } const lazyProcess = core.createLazyLoader("node:process"); // node:test `run()` implementation. // // Returns a `TestsStream`-compatible Readable that emits structured events // describing the test run lifecycle. We currently support the watch-mode // event stream (`test:watch:drained`, `test:watch:restarted`) which is the // minimum required for the Node.js `test-runner/test-run-watch-*` fixtures // that drive watch behavior through the programmatic API. Actual test file // discovery / execution remains TODO and is gated behind separate work; the // stream emits a single empty run cycle, then either ends (watch:false) or // waits for filesystem changes to trigger restarts (watch:true). // // See test-runner/test-run-watch-*.mjs in the Node compat suite for the // behavior this implements. function run(options) { options = options ?? {}; // Experimental `testTagFilters`: a bare string is normalized to a // single-element array; every element must be a string. Registering a // non-empty filter emits the one-shot experimental warning. let testTagFilters = options.testTagFilters; if (testTagFilters !== undefined) { if (typeof testTagFilters === "string") { testTagFilters = [testTagFilters]; } if (!ArrayIsArray(testTagFilters)) { throw new ERR_INVALID_ARG_TYPE( "options.testTagFilters", ["string", "string[]"], options.testTagFilters, ); } for (let i = 0; i < testTagFilters.length; i++) { if (typeof testTagFilters[i] !== "string") { throw new ERR_INVALID_ARG_TYPE( `options.testTagFilters[${i}]`, "string", testTagFilters[i], ); } } if (testTagFilters.length > 0 && !tagsWarningEmitted) { tagsWarningEmitted = true; emitExperimentalWarning("Test tags"); } } const watch = options.watch === true; const signal = options.signal; let cwd = options.cwd; if (cwd === undefined) { cwd = lazyProcess().default.cwd(); } const Readable = getReadable(); const stream = new Readable({ __proto__: null, objectMode: true, // We push events imperatively; the consumer just needs a no-op `_read`. read() {}, }); let watcher = null; let finished = false; let pendingRestartTimer = null; function finish() { if (finished) return; finished = true; if (pendingRestartTimer !== null) { clearTimeout(pendingRestartTimer); pendingRestartTimer = null; } if (watcher !== null) { try { watcher.close(); } catch { /* ignore */ } watcher = null; } // deno-lint-ignore deno-internal/prefer-primordials -- stream is a Node Readable, not an Array stream.push(null); } function emit(type) { if (finished) return; const data = { __proto__: null }; // Node's TestsStream emits each lifecycle entry both as a data chunk // (consumed via async iteration / `'data'` listeners) and as a named // event so callers can attach `.on('test:watch:drained', ...)` directly. // deno-lint-ignore deno-internal/prefer-primordials -- stream is a Node Readable, not an Array stream.push({ __proto__: null, type, data }); stream.emit(type, data); } function drained() { emit("test:watch:drained"); } function scheduleRestart() { if (finished) return; // Debounce bursts of fs events so a single user-visible change produces // exactly one restart cycle (Node's watcher coalesces likewise). if (pendingRestartTimer !== null) { clearTimeout(pendingRestartTimer); } pendingRestartTimer = setTimeout(() => { pendingRestartTimer = null; if (finished) return; emit("test:watch:restarted"); drained(); }, 50); } if (signal) { if (signal.aborted) { // Resolve the initial drained on next tick to keep callers that // `await once(stream, 'test:watch:drained')` working. queueMicrotask(() => { drained(); finish(); }); return stream; } signal.addEventListener("abort", finish, { once: true }); } // Emit the initial "drained" event after the current microtask completes // so that consumers attaching `.on('data')` synchronously after `run(...)` // returns still observe the event. queueMicrotask(() => { drained(); if (!watch) { finish(); return; } try { const fsWatch = getFsWatch(); watcher = fsWatch(cwd, { recursive: true }, () => { scheduleRestart(); }); watcher.on("error", () => { finish(); }); } catch { // If we can't watch (e.g. cwd doesn't exist), end the stream gracefully. finish(); } }); return stream; } function noop() {} const skippedSymbol = Symbol("skipped"); // Detect Node.js-compatible `--test-reporter=...` selection so the polyfill // can emit reporter output matching Node's snapshot fixtures. Deno's CLI does // not consume `--test-reporter`; instead, our `child_process` polyfill (via // node_shim) propagates the value through NODE_OPTIONS when one Deno process // spawns another using Node-style flags. Reading the env var here keeps the // detection self-contained and avoids new Rust plumbing. function detectNodeTestReporter() { const env = globalThis.Deno?.env; if (!env) return null; let value = null; try { value = env.get("NODE_TEST_REPORTER"); } catch { /* permission denied */ } if (value) return value; let nodeOptions = ""; try { nodeOptions = env.get("NODE_OPTIONS") || ""; } catch { /* permission denied */ } if (!nodeOptions) return null; // Match the first `--test-reporter` occurrence; support both `=value` and // space-separated forms. We intentionally do not handle multiple reporters // (Node lets you stack reporters with destinations); the snapshot tests use // a single reporter and that is what we target. const match = StringPrototypeMatch( nodeOptions, new SafeRegExp(/--test-reporter(?:=|\s+)(\S+)/), ); return match ? match[1] : null; } // Resolve lazily: testing.ts ships inside Deno's startup snapshot, so any env // lookups performed at module-evaluation time observe the build environment, // not the running process. Memoize on the first call so we still only read the // env once. let nodeTestReporterCache; function getNodeTestReporter() { if (nodeTestReporterCache !== undefined) return nodeTestReporterCache; nodeTestReporterCache = detectNodeTestReporter(); return nodeTestReporterCache; } function isTapMode() { return getNodeTestReporter() === "tap"; } function getTapSuiteALS() { if (tapSuiteALS !== null) return tapSuiteALS; const mod = core.loadExtScript("ext:deno_node/async_hooks.ts"); const ALS = mod.AsyncLocalStorage; tapSuiteALS = new ALS(); return tapSuiteALS; } function getTapCurrentSuite() { if (tapSuiteALS !== null) { const fromAls = tapSuiteALS.getStore(); if (fromAls !== undefined) return fromAls; } return tapCurrentSuiteSync; } // Parse `--test-skip-pattern` from NODE_OPTIONS so the TAP-mode polyfill can // filter tests Node-style. A bare string is interpreted as a regex source; // `/.../flags` is a regex literal. function parsePatternFlag(flag) { const env = globalThis.Deno?.env; if (!env) return null; let nodeOptions = ""; try { nodeOptions = env.get("NODE_OPTIONS") || ""; } catch { /* permission denied */ } if (!nodeOptions) return null; const out = []; const re = new SafeRegExp(`${flag}(?:=|\\s+)(\\S+)`, "g"); let m; while ((m = RegExpPrototypeExec(re, nodeOptions)) !== null) { const value = m[1]; let pattern; const litMatch = StringPrototypeMatch( value, new SafeRegExp(/^\/(.*)\/([a-z]*)$/), ); if (litMatch) { try { pattern = new SafeRegExp(litMatch[1], litMatch[2]); } catch { continue; } } else { try { pattern = new SafeRegExp(value); } catch { continue; } } ArrayPrototypePush(out, pattern); } return out.length > 0 ? out : null; } let testSkipPatternCache; function getTestSkipPatterns() { if (testSkipPatternCache !== undefined) return testSkipPatternCache; testSkipPatternCache = parsePatternFlag("--test-skip-pattern"); return testSkipPatternCache; } let testOnlyFlagCache; function isTestOnlyFlagSet() { if (testOnlyFlagCache !== undefined) return testOnlyFlagCache; const env = globalThis.Deno?.env; if (!env) { testOnlyFlagCache = false; return false; } let nodeOptions = ""; try { nodeOptions = env.get("NODE_OPTIONS") || ""; } catch { /* permission denied */ } testOnlyFlagCache = RegExpPrototypeTest( new SafeRegExp(/(^|\s)--test-only(\s|=|$)/), nodeOptions, ); return testOnlyFlagCache; } const TEST_ONLY_WARNING = "# 'only' and 'runOnly' require the --test-only command-line option."; function matchesAnyPattern(name, patterns) { for (const p of new SafeArrayIterator(patterns)) { if (RegExpPrototypeTest(p, name)) return true; } return false; } // Returns true if the given test/suite name should be excluded from the run. function shouldSkipByPattern(name) { const skip = getTestSkipPatterns(); if (skip && matchesAnyPattern(String(name), skip)) return true; return false; } // Top-level queue of test/suite entries collected synchronously while the // script body evaluates. Children of describe() blocks live under their parent // entry's `children` array, populated during the synchronous descend through // the describe body. const tapTopEntries = []; let tapRunScheduled = false; // AsyncLocalStorage tracking the currently-active describe() so that test() // and describe() calls made inside an async describe body - even after // `await` boundaries - still register against the surrounding suite. The // fallback variable handles synchronous nesting before ALS is loaded. let tapCurrentSuiteSync = null; let tapSuiteALS = null; const tapStats = { tests: 0, suites: 0, pass: 0, fail: 0, cancelled: 0, skipped: 0, todo: 0, }; function tapWrite(line) { // deno-lint-ignore no-console console.log(line); } function tapDirective(options) { if (options.skip) { const msg = options.skip === true ? "" : tapEscape(String(options.skip)); return msg ? ` # SKIP ${msg}` : " # SKIP"; } if (options.todo) { const msg = options.todo === true ? "" : tapEscape(String(options.todo)); return msg ? ` # TODO ${msg}` : " # TODO"; } return ""; } function tapYaml(depth, type) { const pad = tapIndent(depth) + " "; tapWrite(`${pad}---`); tapWrite(`${pad}duration_ms: 0`); tapWrite(`${pad}type: '${type}'`); tapWrite(`${pad}...`); } class TapContext { #diagnostics = []; #name; #depth; #subtestTail = PromiseResolve(); #subtestCount = 0; #parentChildren; #abortController = new AbortController(); // Per-context "warning printed" flag for the `--test-only` diagnostic. // Mutated by `runTapEntry` when a child uses `only: true`. onlyWarningEmitted = false; // When set via `runOnly(true)`, only direct subtests registered with the // `only: true` option are executed; all other subtests are filtered out. #runOnly = false; constructor(name, depth, parentChildren) { this.#name = name; this.#depth = depth; this.#parentChildren = parentChildren; } get name() { return this.#name; } get fullName() { return this.#name; } get signal() { return this.#abortController.signal; } // Aborts this test's own AbortSignal (t.signal); see NodeTestContext._abort. _abort(reason) { if (!this.#abortController.signal.aborted) { this.#abortController.abort(reason); } } get assert() { return getAssertObject(); } get mock() { return mock; } diagnostic(message) { ArrayPrototypePush(this.#diagnostics, String(message)); } _drainDiagnostics() { const out = this.#diagnostics; this.#diagnostics = []; return out; } // Subtest registration: `t.test(name, opts?, fn?)` queues a subtest that runs // sequentially in the order it was registered. Concurrent calls (Promise.all) // are serialized through the parent's subtest tail. runOnly(value) { this.#runOnly = !!value; return null; } test(name, options, fn) { const prepared = prepareOptions(name, options, fn, {}); // In run-only mode, subtests not flagged with `only: true` are filtered // out entirely: they are neither registered nor reported, matching Node. if (this.#runOnly && !prepared.options.only) { return PromiseResolve(); } this.#subtestCount++; const n = this.#subtestCount; const childDepth = this.#depth + 1; const entry = { name: prepared.name, fn: prepared.fn, options: prepared.options, kind: "test", children: [], bodyPromise: null, bodyError: null, }; if (this.#parentChildren) { ArrayPrototypePush(this.#parentChildren, entry); } // deno-lint-ignore no-this-alias const parentState = this; const p = PromisePrototypeThen( this.#subtestTail, () => runTapEntry(entry, childDepth, n, parentState), ); this.#subtestTail = PromisePrototypeThen(p, () => {}, () => {}); return p; } _drainSubtests() { return this.#subtestTail; } _subtestCount() { return this.#subtestCount; } } function scheduleTapRun() { if (tapRunScheduled) return; tapRunScheduled = true; // Defer to the macrotask queue so synchronous top-level test() calls finish // queueing before we start running. setTimeout(() => { runTapTop(); }, 0); } async function runTapTop() { // Hold the event loop open while tests run so that fixtures using // unref'd timers (Node's runner keeps itself alive internally) don't cause // Deno to exit before subtests complete. const keepAlive = setInterval(() => {}, 1 << 30); try { // Match Node's ordering: top-level `before()` callbacks fire before the // `TAP version 13` line, so any console output they produce appears // before the reporter header in the captured stream. if (rootBeforeHooks.length > 0) { const rootCtx = { name: "<root>", fullName: "<root>" }; for (const hook of new SafeArrayIterator(rootBeforeHooks)) { try { const r = ReflectApply(hook, null, [rootCtx]); if (isThenable(r)) await r; } catch { /* swallow to keep parity with Node's lenient hook errors */ } } } tapWrite("TAP version 13"); let n = 0; const topState = { onlyWarningEmitted: false }; for (const entry of new SafeArrayIterator(tapTopEntries)) { n++; await runTapEntry(entry, 0, n, topState); } // Drain top-level `after()` hooks before printing the plan/summary so // their console output appears between the last test and the `1..N` line. if (rootAfterHooks.length > 0) { const rootCtx = { name: "<root>", fullName: "<root>" }; const hooks = ArrayPrototypeSplice( rootAfterHooks, 0, rootAfterHooks.length, ); for (const hook of new SafeArrayIterator(hooks)) { try { const r = ReflectApply(hook, null, [rootCtx]); if (isThenable(r)) await r; } catch { /* swallow */ } } } tapWrite(`1..${n}`); tapWrite(`# tests ${tapStats.tests}`); tapWrite(`# suites ${tapStats.suites}`); tapWrite(`# pass ${tapStats.pass}`); tapWrite(`# fail ${tapStats.fail}`); tapWrite(`# cancelled ${tapStats.cancelled}`); tapWrite(`# skipped ${tapStats.skipped}`); tapWrite(`# todo ${tapStats.todo}`); tapWrite(`# duration_ms 0`); } finally { clearInterval(keepAlive); } if (tapStats.fail > 0 || tapStats.cancelled > 0) { try { globalThis.Deno?.exit?.(1); } catch { /* exit unavailable */ } } } // Recursively run a test or suite entry, emitting TAP output at the given // nesting depth. `parentState` is the runtime state of the immediate parent // (a TapContext for test-bodies, or a `{ onlyWarningEmitted }` object for // suite/root scopes); it's used to emit the `# 'only' and 'runOnly' require // the --test-only command-line option.` warning at most once per parent. async function runTapEntry(entry, depth, n, parentState) { const indent = tapIndent(depth); const isSuite = entry.kind === "suite"; tapWrite(`${indent}# Subtest: ${tapEscape(entry.name)}`); const directive = tapDirective(entry.options); let status = "ok"; let diagnostics = []; let childCount = 0; // Each test/suite body gets its own state object so warnings emitted // because of an `only: true` child don't leak across siblings. const myChildrenState = { onlyWarningEmitted: false }; if (entry.options.skip) { if (isSuite) { tapStats.suites++; } else { tapStats.skipped++; tapStats.tests++; } } else if (entry.options.todo) { // Per Node behavior, a TODO test that throws is not counted as a failure. // The runner skips invoking the body to match snapshot output. if (isSuite) { tapStats.suites++; } else { tapStats.todo++; tapStats.tests++; } } else if (isSuite) { try { if (entry.bodyError) throw entry.bodyError; if (entry.bodyPromise) await entry.bodyPromise; let childN = 0; for (const child of new SafeArrayIterator(entry.children)) { childN++; await runTapEntry(child, depth + 1, childN, myChildrenState); } childCount = childN; } catch (_err) { status = "not ok"; tapStats.fail++; } tapStats.suites++; } else { // test/it body const ctx = new TapContext(entry.name, depth, entry.children); try { await runWithTestGuards(async () => { const ret = ReflectApply(entry.fn, ctx, [ctx]); if (isThenable(ret)) await ret; }, { timeout: entry.options.timeout, signal: entry.options.signal, abort: (reason) => ctx._abort(reason), }); // Wait for any concurrent t.test() calls (e.g. Promise.all([...])). await ctx._drainSubtests(); childCount = ctx._subtestCount(); tapStats.pass++; } catch (_err) { status = "not ok"; tapStats.fail++; // Even on failure, drain any in-flight subtests so their output is // emitted before the parent's "not ok" line. try { await ctx._drainSubtests(); } catch { /* ignore */ } childCount = ctx._subtestCount(); } diagnostics = ctx._drainDiagnostics(); tapStats.tests++; } if (childCount > 0) { tapWrite(`${tapIndent(depth + 1)}1..${childCount}`); } tapWrite(`${indent}${status} ${n} - ${tapEscape(entry.name)}${directive}`); tapYaml(depth, isSuite ? "suite" : "test"); for (const d of new SafeArrayIterator(diagnostics)) { tapWrite(`${indent}# ${tapEscape(d)}`); } // If this entry was registered with `only: true` and the `--test-only` flag // wasn't supplied, Node prints a one-off warning in the parent's scope // immediately after the entry's yaml/diagnostics. Emit it at the same depth // and only once per parent. if ( entry.options.only && !isTestOnlyFlagSet() && parentState && !parentState.onlyWarningEmitted ) { tapWrite(`${indent}${TEST_ONLY_WARNING}`); parentState.onlyWarningEmitted = true; } } function queueTapTest(name, options, fn, overrides) { const prepared = prepareOptions(name, options, fn, overrides); if (shouldSkipByPattern(prepared.name)) { // Filtered out entirely: do not register or run. scheduleTapRun(); return PromiseResolve(); } const entry = { name: prepared.name, fn: prepared.fn, options: prepared.options, kind: "test", children: [], bodyPromise: null, bodyError: null, }; const parentSuite = getTapCurrentSuite(); if (parentSuite !== null) { ArrayPrototypePush(parentSuite.children, entry); } else { ArrayPrototypePush(tapTopEntries, entry); scheduleTapRun(); } return PromiseResolve(); } function queueTapSuite(name, options, fn, overrides) { const prepared = prepareOptions(name, options, fn, overrides); if (shouldSkipByPattern(prepared.name)) { // Filtered out entirely: do not register, but the suite body must not // run (it would otherwise add unwanted children to the parent). scheduleTapRun(); return PromiseResolve(); } const entry = { name: prepared.name, fn: prepared.fn, options: prepared.options, kind: "suite", children: [], bodyPromise: null, bodyError: null, }; const parentSuite = getTapCurrentSuite(); if (parentSuite !== null) { ArrayPrototypePush(parentSuite.children, entry); } else { ArrayPrototypePush(tapTopEntries, entry); scheduleTapRun(); } // Evaluate the suite body inside an AsyncLocalStorage scope so any nested // describe()/test() calls - including those scheduled after `await` inside // the body - register against this suite, not the outer (or null) scope. // The sync fallback handles environments without ALS available. const als = getTapSuiteALS(); const prev = tapCurrentSuiteSync; tapCurrentSuiteSync = entry; try { als.run(entry, () => { try { const ret = ReflectApply(prepared.fn, null, []); if (isThenable(ret)) entry.bodyPromise = ret; } catch (err) { entry.bodyError = err; } }); } finally { tapCurrentSuiteSync = prev; } return PromiseResolve(); } function isThenable(value) { return value !== null && value !== undefined && typeof value.then === "function"; } function getExpectFailureMatch(expectFailure) { if ( expectFailure !== null && typeof expectFailure === "object" && ObjectPrototypeHasOwnProperty(expectFailure, "match") ) { return expectFailure.match; } if ( expectFailure === true || typeof expectFailure === "string" || expectFailure === undefined ) { return undefined; } return expectFailure; } function assertExpectedFailure(err, expectFailure) { const match = getExpectFailureMatch(expectFailure); if (match !== undefined) { assert.throws(() => { throw err; }, match); } } // Runs `invoke()` (a thunk that executes a single test body) while enforcing // the `timeout` option, honoring a caller-supplied abort `signal`, and capturing // any unhandled rejection / uncaught exception that fires during the body. The // returned promise resolves with the body's value, or rejects with the first // failure observed: a body error, a timeout, an abort, or a captured rejection. // `opts.abort(reason)` is invoked on timeout/abort so the test's own // AbortSignal (`t.signal`) is aborted and user code can react. async function runWithTestGuards(invoke, opts) { ensureRealTimers(); const timeout = opts?.timeout; const signal = opts?.signal; const abort = opts?.abort; const failure = PromiseWithResolvers(); const result = PromiseWithResolvers(); const sink = { settled: false, fail(reason) { if (this.settled) return; this.settled = true; failure.reject(reason); }, }; pushTestSink(sink); let timeoutId = null; let onAbort = null; try { if (signal !== undefined && signal !== null) { if (signal.aborted) { if (typeof abort === "function") abort(signal.reason); sink.fail(buildAbortError(signal)); } else { onAbort = () => { if (typeof abort === "function") abort(signal.reason); sink.fail(buildAbortError(signal)); }; signal.addEventListener("abort", onAbort, { once: true }); } } if (typeof timeout === "number" && timeout > 0) { timeoutId = realSetTimeout(() => { const err = buildTimeoutError(timeout); if (typeof abort === "function") abort(err); sink.fail(err); }, timeout); } // Race the body against any externally-signalled failure. We avoid // Promise.race so a late body settlement cannot resurface after a failure. const body = (async () => { const value = await invoke(); // Stop attributing as soon as the body settles. An unhandled rejection // whose `unhandledrejection` event already fired while the body was // running (for example one surfacing during an await, as in the // denoland/deno#34818 repro) has already failed this test through the // sink. This is best-effort and bounded to the body's lifetime: a // rejection that only surfaces after the body returns is treated as // post-test asynchronous activity and not attributed, matching the // limitation Node also has for activity that outlives a test. We avoid // draining extra event-loop turns here because doing so perturbs the // runner's output ordering and timing. sink.settled = true; return value; })(); PromisePrototypeThen( body, (value) => result.resolve(value), (err) => result.reject(err), ); PromisePrototypeThen(failure.promise, undefined, (err) => { result.reject(err); }); return await result.promise; } finally { sink.settled = true; popTestSink(sink); if (timeoutId !== null) { realClearTimeout(timeoutId); } if (onAbort !== null && signal) { signal.removeEventListener("abort", onAbort); } } } async function runNodeTestFunction(fn, nodeTestContext) { if (fn.length >= 2) { // Node-style callback API: fn(t, done) - wait for `done()` (or promise // rejection) before treating the test as complete. await new Promise((testResolve, testReject) => { pendingCallbackReject = testReject; const done = (err) => { pendingCallbackReject = null; if (err) testReject(err); else testResolve(undefined); }; try { const result = ReflectApply(fn, nodeTestContext, [ nodeTestContext, done, ]); if (isThenable(result)) { PromisePrototypeThen(result, undefined, (err) => { pendingCallbackReject = null; testReject(err); }); } } catch (err) { pendingCallbackReject = null; testReject(err); } }); return undefined; } return await ReflectApply(fn, nodeTestContext, [nodeTestContext]); } async function runPossiblyExpectingFailure(fn, nodeTestContext, options) { const guards = { timeout: options.timeout, signal: options.signal, abort: (reason) => nodeTestContext._abort(reason), }; // Install this context as the current one for the duration of the body so a // top-level `test()` called inside it is routed here as a subtest (see the // dispatcher in `test()`). `enterWith` is a synchronous setter that binds the // context for the current async root and its descendants without adding a // stack frame - extra frames can push `ext:cli/40_test.js` out of a failure's // captured stack and break the source location reported for failed tests. No // save/restore is needed: each body runs in its own async root, so concurrent // subtest bodies each keep their own context even when they interleave across // `await`. getTestContextALS().enterWith(nodeTestContext); if ( !options.expectFailure || options.skip || options.todo ) { try { const result = await runWithTestGuards( () => runNodeTestFunction(fn, nodeTestContext), guards, ); nodeTestContext._checkPlan(); return result; } finally { // Drain even on failure so subtests registered before the error finish // their Deno test steps before this test settles. await nodeTestContext._drainSubtests(); } } let failed = false; try { await runWithTestGuards( () => runNodeTestFunction(fn, nodeTestContext), guards, ); nodeTestContext._checkPlan(); } catch (err) { failed = true; assertExpectedFailure(err, options.expectFailure); } finally { await nodeTestContext._drainSubtests(); } if (!failed) { throw new Error("test was expected to fail but passed"); } return undefined; } class TestPlan { #expected; #actual = 0; constructor(count) { this.#expected = count; } increment() { this.#actual++; } check() { if (this.#actual !== this.#expected) { throw new Error( `plan expected ${this.#expected} assertion(s) but received ${this.#actual}`, ); } } } class NodeTestContext { #denoContext; #afterHooks = []; #beforeHooks = []; #parent; #skipped = false; #name; #abortController = new AbortController(); #plan; #planAssert; #beforeEachHooks = []; #afterEachHooks = []; // Guards so the once-per-test `before()`/`after()` hooks run a single time // regardless of how many subtests this context has. #beforeHooksRun = false; #afterHooksRun = false; // When set via `runOnly(true)`, only direct subtests registered with the // `only: true` option are executed; all other subtests are skipped. #runOnly = false; // Promises for top-level `test()` calls routed here from inside this // context's body (see `_trackSubtest`). The body awaits these before settling // so such subtests still complete (Node semantics) and their Deno test steps // finish before the parent step returns. #subtestPromises = []; // Canonical tags declared directly on this test/suite (see prepareTags). #ownTags = []; constructor(t, parent, name, tags) { this.#denoContext = t; this.#parent = parent; this.#name = name; if (tags !== undefined) { this.#ownTags = tags; } } // The effective tag set: the parent's tags followed by this context's own // tags, deduped (parent-first order). Tags are already canonical (lowercased // and deduped) individually. get tags() { const parentTags = this.#parent ? this.#parent.tags : []; if (this.#ownTags.length === 0) { // Return a fresh copy so callers cannot mutate internal state. return ArrayPrototypeSlice(parentTags); } const result = ArrayPrototypeSlice(parentTags); for (let i = 0; i < this.#ownTags.length; i++) { if (!ArrayPrototypeIncludes(result, this.#ownTags[i])) { ArrayPrototypePush(result, this.#ownTags[i]); } } return result; } get [skippedSymbol]() { return this.#skipped || (this.#parent?.[skippedSymbol] ?? false); } get assert() { if (this.#plan) { if (!this.#planAssert) { const plan = this.#plan; const base = getAssertObject(); const wrapped = { __proto__: null }; ArrayPrototypeForEach(methodsToCopy, (method) => { wrapped[method] = function (...args) { plan.increment(); return ReflectApply(base[method], this, args); }; }); wrapped.fileSnapshot = function (...args) { plan.increment(); return ReflectApply(base.fileSnapshot, this, args); }; this.#planAssert = wrapped; } return this.#planAssert; } return getAssertObject(); } plan(count) { validateInteger(count, "count", 1); this.#plan = new TestPlan(count); } _checkPlan() { if (this.#plan) this.#plan.check(); } get signal() { return this.#abortController.signal; } // Aborts this test's own AbortSignal (t.signal). Called by the test guards on // timeout or when a caller-supplied signal aborts, so user code awaiting // t.signal observes the abort. _abort(reason) { if (!this.#abortController.signal.aborted) { this.#abortController.abort(reason); } } get name() { return this.#name; } get fullName() { if (this.#parent) { return this.#parent.fullName + " > " + this.#name; } return this.#name; } diagnostic(message) { // deno-lint-ignore no-console console.log("DIAGNOSTIC:", message); } get mock() { return mock; } runOnly(value) { this.#runOnly = !!value; return null; } skip() { this.#skipped = true; return null; } todo() { this.#skipped = true; return null; } test(name, options, fn, overrides) { const prepared = prepareOptions(name, options, fn, overrides); if (this.#plan) this.#plan.increment(); // deno-lint-ignore no-this-alias const parentContext = this; const stepPromise = PromisePrototypeThen( this.#denoContext.step({ name: prepared.name, fn: async (denoTestContext) => { const newNodeTextContext = new NodeTestContext( denoTestContext, parentContext, prepared.name, prepared.options.tags, ); let bodyOk = false; try { // The parent's `before()` hooks run once, before its first subtest. await parentContext._runBeforeHooksOnce(); for ( const hook of new SafeArrayIterator( parentContext.#beforeEachHooks, ) ) { // `t.beforeEach()` hooks run in the parent test's context, so // `getTestContext()` inside them observes the parent (Node). await runInTestContext( parentContext, () => hook(newNodeTextContext), ); } await runPossiblyExpectingFailure( prepared.fn, newNodeTextContext, prepared.options, ); bodyOk = true; // This subtest's own `after()` hooks run once, after its body and // all of its own subtests have completed. The parent's `after()` // hooks run after the parent finishes, not here. await newNodeTextContext._runAfterHooksOnce(); } catch (err) { if (!bodyOk) { try { await newNodeTextContext._runAfterHooksOnce(); } catch { /* ignore, test is already failing */ } } if (!newNodeTextContext[skippedSymbol]) { throw err; } } finally { for ( const hook of new SafeArrayIterator( parentContext.#afterEachHooks, ) ) { // `t.afterEach()` hooks likewise run in the parent's context. await runInTestContext( parentContext, () => hook(newNodeTextContext), ); } } }, ignore: !!prepared.options.todo || !!prepared.options.skip || (this.#runOnly && !prepared.options.only), sanitizeExit: false, sanitizeOps: false, sanitizeResources: false, }), () => undefined, // A failed step settles `false` rather than rejecting, but guard against // rejection so the returned promise never surfaces as unhandled. () => undefined, ); return stepPromise; } // Records a subtest promise so the body runner can await it before this test // settles. Used only for top-level `test()` calls routed here from inside the // body (see the dispatcher in `test()`); a user awaiting `t.test()` directly // sequences it themselves, and an unawaited `t.test()` keeps its existing // (Node-divergent) "INCOMPLETE" behavior so unref'd-timer subtests are not // forced to resolve. _trackSubtest(promise) { ArrayPrototypePush(this.#subtestPromises, promise); } // Awaits subtests registered via a routed top-level `test()` call. Called by // the body runner after the test function returns so that such subtests, // which are commonly not awaited (e.g. fastify's suites), still run to // completion before the parent test settles. async _drainSubtests() { if (this.#subtestPromises.length === 0) return; const promises = ArrayPrototypeSplice( this.#subtestPromises, 0, this.#subtestPromises.length, ); for (const p of new SafeArrayIterator(promises)) { try { await p; } catch { /* failures already reported through the subtest's own step */ } } } before(fn, _options) { if (typeof fn !== "function") { throw new TypeError("before() requires a function"); } ArrayPrototypePush(this.#beforeHooks, fn); } after(fn, _options) { if (typeof fn !== "function") { throw new TypeError("after() requires a function"); } ArrayPrototypePush(this.#afterHooks, fn); } beforeEach(fn, _options) { if (typeof fn !== "function") { throw new TypeError("beforeEach() requires a function"); } ArrayPrototypePush(this.#beforeEachHooks, fn); } afterEach(fn, _options) { if (typeof fn !== "function") { throw new TypeError("afterEach() requires a function"); } ArrayPrototypePush(this.#afterEachHooks, fn); } // Runs this context's `before()` hooks a single time, before its first // subtest. Idempotent so it is safe to call from every subtest's step. async _runBeforeHooksOnce() { if (this.#beforeHooksRun) return; this.#beforeHooksRun = true; // deno-lint-ignore no-this-alias const ctx = this; for (const hook of new SafeArrayIterator(this.#beforeHooks)) { await runInTestContext(ctx, () => hook(ctx)); } } // Runs this context's `after()` hooks a single time, after its body and all // of its subtests have completed. Idempotent so success and failure paths can // both invoke it without double-running. async _runAfterHooksOnce() { if (this.#afterHooksRun) return; this.#afterHooksRun = true; // deno-lint-ignore no-this-alias const ctx = this; for (const hook of new SafeArrayIterator(this.#afterHooks)) { await runInTestContext(ctx, () => hook(ctx)); } } } let currentSuite = null; // AsyncLocalStorage holding the NodeTestContext whose body is currently // executing, or null when no test body is on the stack. Set by // `runPossiblyExpectingFailure` for the duration of each body so that a // top-level `test()` / `it()` call made from inside another test's body is // registered as a subtest of that test (via `t.step()`), matching Node, rather // than hitting Deno's "Nested Deno.test() calls are not supported" error. // // This must be an ALS, not a single module global with synchronous // save/restore: the unawaited routed-subtest path this enables makes execution // non-LIFO. With concurrent subtest bodies suspended across `await`, a plain // variable can point at a sibling when a parent resumes and silently mis-nest // the next routed `test()`. Each Deno test/step fn is its own async root, so // the store stays scoped to that body's subtree and survives `await` // boundaries, giving every interleaved body its own context. See // https://github.com/denoland/deno/issues/35391. let testContextALS = null; function getTestContextALS() { if (testContextALS !== null) return testContextALS; const mod = core.loadExtScript("ext:deno_node/async_hooks.ts"); const ALS = mod.AsyncLocalStorage; testContextALS = new ALS(); return testContextALS; } function getCurrentTestContext() { if (testContextALS === null) return null; return testContextALS.getStore() ?? null; } // node:test `getTestContext()` (Node v26.1.0+). Returns the TestContext / // SuiteContext of the test, subtest, or hook currently executing, or // `undefined` when called outside of any test. It reads the same // AsyncLocalStorage that routes nested `test()` calls (see getTestContextALS): // every test/it body runs within it, and each hook is executed inside the ALS // scope of the context it was registered on (see runInTestContext) so a hook // observes its owning test/suite context rather than the subtest it runs for. function getTestContext() { return getCurrentTestContext() ?? undefined; } // Runs `fn` with `ctx` installed as the current test context for the duration // of the call, including across its `await` points, so that `getTestContext()` // (and nested `test()` routing) observe `ctx` while a hook runs. function runInTestContext(ctx, fn) { return getTestContextALS().run(ctx, fn); } const rootBeforeHooks = []; const rootAfterHooks = []; const rootBeforeEachHooks = []; const rootAfterEachHooks = []; let rootBeforeRan = false; async function runRootBeforeOnce() { if (rootBeforeRan) return; rootBeforeRan = true; if (rootBeforeHooks.length === 0) return; const rootCtx = { name: "<root>", fullName: "<root>" }; for (const hook of new SafeArrayIterator(rootBeforeHooks)) { await hook(rootCtx); } } async function runRootAfterIfDone() { if (activeNodeTests !== 0) return; if (rootAfterHooks.length === 0) return; const rootCtx = { name: "<root>", fullName: "<root>" }; // Snapshot and clear so we only run once even if more tests get queued. const hooks = ArrayPrototypeSplice( rootAfterHooks, 0, rootAfterHooks.length, ); for (const hook of new SafeArrayIterator(hooks)) { try { await hook(rootCtx); } catch { /* ignore */ } } } class TestSuite { #denoTestContext; nodeTestContext; // The enclosing suite, or null for a top-level suite. Used to cascade // beforeEach()/afterEach() hooks from every ancestor suite onto each test // (issue #35404). parent = null; entries = []; beforeAllHooks = []; afterAllHooks = []; beforeEachHooks = []; afterEachHooks = []; constructor(t, nodeTestContext, parent) { this.#denoTestContext = t; this.nodeTestContext = nodeTestContext; this.parent = parent ?? null; } addTest(name, options, fn, overrides) { const prepared = prepareOptions(name, options, fn, overrides); const suiteNodeContext = this.nodeTestContext; // deno-lint-ignore no-this-alias const suite = this; ArrayPrototypePush(this.entries, { name: prepared.name, fn: async (denoTestContext) => { const newNodeTextContext = new NodeTestContext( denoTestContext, suiteNodeContext, prepared.name, prepared.options.tags, ); // beforeEach()/afterEach() cascade through the whole ancestor chain: // each test runs every enclosing suite's hooks, plus the file-scope // (root) hooks. beforeEach runs outermost-first, afterEach runs // innermost-first, matching Node (issue #35404). The chain is collected // here, at run time, so hooks registered late in a suite body are seen. const suiteChain = []; // innermost-first for (let s = suite; s !== null; s = s.parent) { ArrayPrototypePush(suiteChain, s); } try { for (const hook of new SafeArrayIterator(rootBeforeEachHooks)) { await runInTestContext( newNodeTextContext, () => hook(newNodeTextContext), ); } for (let i = suiteChain.length - 1; i >= 0; i--) { // A suite's beforeEach() runs in that suite's context, so // `getTestContext()` observes the suite, not the subtest (Node). const suiteCtx = suiteChain[i].nodeTestContext; for ( const hook of new SafeArrayIterator(suiteChain[i].beforeEachHooks) ) { await runInTestContext(suiteCtx, () => hook(newNodeTextContext)); } } return await runPossiblyExpectingFailure( prepared.fn, newNodeTextContext, prepared.options, ); } catch (err) { if (newNodeTextContext[skippedSymbol]) { return undefined; } else { throw err; } } finally { for (let i = 0; i < suiteChain.length; i++) { const suiteCtx = suiteChain[i].nodeTestContext; for ( const hook of new SafeArrayIterator(suiteChain[i].afterEachHooks) ) { try { await runInTestContext( suiteCtx, () => hook(newNodeTextContext), ); } catch { /* ignore */ } } } for (const hook of new SafeArrayIterator(rootAfterEachHooks)) { try { await runInTestContext( newNodeTextContext, () => hook(newNodeTextContext), ); } catch { /* ignore */ } } } }, ignore: !!prepared.options.todo || !!prepared.options.skip, }); } addSuite(name, options, fn, overrides) { const prepared = prepareOptions(name, options, fn, overrides); const { promise, resolve } = PromiseWithResolvers(); const parentSuiteContext = this.nodeTestContext; // deno-lint-ignore no-this-alias const parentSuite = this; ArrayPrototypePush(this.entries, { name: prepared.name, fn: wrapSuiteFn( prepared.fn, resolve, prepared.name, parentSuiteContext, parentSuite, prepared.options.tags, ), ignore: !!prepared.options.todo || !!prepared.options.skip, }); return promise; } async execute() { for (const entry of new SafeArrayIterator(this.entries)) { await this.#denoTestContext.step({ name: entry.name, fn: entry.fn, ignore: entry.ignore, sanitizeExit: false, sanitizeOps: false, sanitizeResources: false, }); } } } // Node's experimental test `tags` feature. Tags are validated (array of // non-empty strings), canonicalized (lowercased and deduped, preserving // declaration order), and the experimental warning is emitted once the first // time any tag is registered. let tagsWarningEmitted = false; function prepareTags(tags, argName) { if (!ArrayIsArray(tags)) { throw new ERR_INVALID_ARG_TYPE(argName, "string[]", tags); } const canonical = []; for (let i = 0; i < tags.length; i++) { const tag = tags[i]; if (typeof tag !== "string") { throw new ERR_INVALID_ARG_TYPE(`${argName}[${i}]`, "string", tag); } if (tag === "") { throw new ERR_INVALID_ARG_VALUE( `${argName}[${i}]`, tag, "must not be an empty string", ); } const lower = StringPrototypeToLowerCase(tag); if (!ArrayPrototypeIncludes(canonical, lower)) { ArrayPrototypePush(canonical, lower); } } if (canonical.length > 0 && !tagsWarningEmitted) { tagsWarningEmitted = true; emitExperimentalWarning("Test tags"); } return canonical; } function prepareOptions(name, options, fn, overrides) { if (typeof name === "function") { fn = name; } else if (name !== null && typeof name === "object") { fn = options; options = name; } else if (typeof options === "function") { fn = options; } if (options === null || typeof options !== "object") { options = {}; } const finalOptions = { ...options, ...overrides }; if (finalOptions.tags !== undefined) { finalOptions.tags = prepareTags(finalOptions.tags, "options.tags"); } if (typeof fn !== "function") { fn = noop; } if (typeof name !== "string" || name === "") { name = fn.name || "<anonymous>"; } return { fn, options: finalOptions, name }; } function wrapTestFn(fn, resolve, name, options) { return async function (t) { const nodeTestContext = new NodeTestContext( t, undefined, name, options.tags, ); let beforeEachOk = false; try { await runRootBeforeOnce(); for (const hook of new SafeArrayIterator(rootBeforeEachHooks)) { await runInTestContext(nodeTestContext, () => hook(nodeTestContext)); } beforeEachOk = true; await runPossiblyExpectingFailure(fn, nodeTestContext, options); // The test's own `after()` hooks run once, after its body and all of its // subtests complete (a top-level test awaits its subtests inline). await nodeTestContext._runAfterHooksOnce(); } catch (err) { try { await nodeTestContext._runAfterHooksOnce(); } catch { /* ignore, test is already failing */ } if (!nodeTestContext[skippedSymbol]) { throw sanitizeThrowValue(err); } } finally { if (beforeEachOk) { for (const hook of new SafeArrayIterator(rootAfterEachHooks)) { try { await runInTestContext( nodeTestContext, () => hook(nodeTestContext), ); } catch { /* swallow to match node behavior on hook error */ } } } activeNodeTests--; await runRootAfterIfDone(); resolve(); } }; } function prepareDenoTest(name, options, fn, overrides) { const prepared = prepareOptions(name, options, fn, overrides); activeNodeTests++; const denoTestOptions = { name: prepared.name, fn: wrapTestFn(prepared.fn, noop, prepared.name, prepared.options), only: prepared.options.only, ignore: !!prepared.options.todo || !!prepared.options.skip, sanitizeOnly: false, sanitizeExit: false, sanitizeOps: false, sanitizeResources: false, }; Deno.test(denoTestOptions); // Node resolves the returned promise on test completion, but the // Deno runner only executes registered tests after the module // finishes evaluating, so top-level `await test(...)` deadlocks. // Resolve immediately to unblock; the test still runs and is // reported normally. Trade-off: code that awaits `test()` for // sequencing (`await test('a'); await test('b')`) sees them run // out of order. return PromiseResolve(); } function wrapSuiteFn(fn, resolve, name, parentNodeContext, parentSuite, tags) { return async function (t) { const isTopLevel = parentNodeContext === undefined; if (isTopLevel) await runRootBeforeOnce(); const suiteNodeContext = new NodeTestContext( t, parentNodeContext, name, tags, ); const prevSuite = currentSuite; const suite = currentSuite = new TestSuite( t, suiteNodeContext, parentSuite, ); try { // Run the suite body in the suite's context so `getTestContext()` inside // it (and any synchronously-registered hook bodies) observes the suite. runInTestContext(suiteNodeContext, () => fn(suiteNodeContext)); } finally { currentSuite = prevSuite; } try { // Suite-level before()/after() hooks run in the suite's context and // receive it as their argument, matching Node. for (const hook of new SafeArrayIterator(suite.beforeAllHooks)) { await runInTestContext(suiteNodeContext, () => hook(suiteNodeContext)); } await suite.execute(); } finally { try { for (const hook of new SafeArrayIterator(suite.afterAllHooks)) { await runInTestContext( suiteNodeContext, () => hook(suiteNodeContext), ); } } finally { if (isTopLevel) { activeNodeTests--; await runRootAfterIfDone(); } resolve(); } } }; } function prepareDenoTestForSuite(name, options, fn, overrides) { const prepared = prepareOptions(name, options, fn, overrides); activeNodeTests++; const denoTestOptions = { name: prepared.name, fn: wrapSuiteFn( prepared.fn, noop, prepared.name, undefined, undefined, prepared.options.tags, ), only: prepared.options.only, ignore: !!prepared.options.todo || !!prepared.options.skip, sanitizeOnly: false, sanitizeExit: false, sanitizeOps: false, sanitizeResources: false, }; Deno.test(denoTestOptions); // See `prepareDenoTest` for the Node-divergence trade-off; top-level // `await suite(...)` would deadlock if we waited for completion. return PromiseResolve(); } function test(name, options, fn, overrides) { installErrorHandlers(); if (isTapMode()) { return queueTapTest(name, options, fn, overrides); } if (currentSuite) { return currentSuite.addTest(name, options, fn, overrides); } // A top-level `test()` called from inside another test's body becomes a // subtest of that test, matching Node. Without this it would fall through to // `Deno.test()`, which rejects nested registration. const currentContext = getCurrentTestContext(); if (currentContext !== null) { const promise = currentContext.test(name, options, fn, overrides); // The body that issued this call may not await it; have the running test // wait for it before settling so the subtest still completes (Node). currentContext._trackSubtest(promise); return promise; } return prepareDenoTest(name, options, fn, overrides); } test.skip = function skip(name, options, fn) { return test(name, options, fn, { skip: true }); }; test.todo = function todo(name, options, fn) { return test(name, options, fn, { todo: true }); }; test.only = function only(name, options, fn) { return test(name, options, fn, { only: true }); }; test.expectFailure = function expectFailure(name, options, fn) { return test(name, options, fn, { expectFailure: true }); }; function suite(name, options, fn, overrides) { installErrorHandlers(); if (isTapMode()) { return queueTapSuite(name, options, fn, overrides); } if (currentSuite) { return currentSuite.addSuite(name, options, fn, overrides); } return prepareDenoTestForSuite(name, options, fn, overrides); } suite.skip = function skip(name, options, fn) { return suite(name, options, fn, { skip: true }); }; suite.todo = function todo(name, options, fn) { return suite(name, options, fn, { todo: true }); }; suite.only = function only(name, options, fn) { return suite(name, options, fn, { only: true }); }; const it = test; const describe = suite; function before(fn, _options) { if (typeof fn !== "function") { throw new TypeError("before() requires a function argument"); } if (isTapMode()) { const tapSuite = getTapCurrentSuite(); if (tapSuite !== null) { ArrayPrototypePush(tapSuite.beforeAllHooks ??= [], fn); return; } ArrayPrototypePush(rootBeforeHooks, fn); // A bare top-level `before()` with no tests must still produce TAP // output (`before` runs, then `TAP version 13`, then `1..0`). scheduleTapRun(); return; } if (currentSuite) { ArrayPrototypePush(currentSuite.beforeAllHooks, fn); return; } ArrayPrototypePush(rootBeforeHooks, fn); } function after(fn, _options) { if (typeof fn !== "function") { throw new TypeError("after() requires a function argument"); } if (isTapMode()) { const tapSuite = getTapCurrentSuite(); if (tapSuite !== null) { ArrayPrototypePush(tapSuite.afterAllHooks ??= [], fn); return; } ArrayPrototypePush(rootAfterHooks, fn); scheduleTapRun(); return; } if (currentSuite) { ArrayPrototypePush(currentSuite.afterAllHooks, fn); return; } ArrayPrototypePush(rootAfterHooks, fn); } function beforeEach(fn, _options) { if (typeof fn !== "function") { throw new TypeError("beforeEach() requires a function argument"); } if (currentSuite) { ArrayPrototypePush(currentSuite.beforeEachHooks, fn); return; } ArrayPrototypePush(rootBeforeEachHooks, fn); } function afterEach(fn, _options) { if (typeof fn !== "function") { throw new TypeError("afterEach() requires a function argument"); } if (currentSuite) { ArrayPrototypePush(currentSuite.afterEachHooks, fn); return; } ArrayPrototypePush(rootAfterEachHooks, fn); } test.it = test; test.describe = suite; test.suite = suite; test.before = before; test.after = after; test.beforeEach = beforeEach; test.afterEach = afterEach; test.getTestContext = getTestContext; const activeMocks = []; class MockFunctionContext { #calls = []; #implementation; #restore; #times; #onceImplementations = new SafeMap(); constructor(implementation, restore, times) { this.#implementation = implementation; this.#restore = restore; this.#times = times; } get calls() { return this.#calls; } callCount() { return this.#calls.length; } mockImplementation(implementation) { validateFunction(implementation, "implementation"); this.#implementation = implementation; } mockImplementationOnce(implementation, onCall) { validateFunction(implementation, "implementation"); if (onCall !== undefined) { validateInteger(onCall, "onCall", 0); } const call = onCall ?? this.#calls.length; MapPrototypeSet(this.#onceImplementations, call, implementation); } resetCalls() { ArrayPrototypeSplice(this.#calls, 0, this.#calls.length); } restore() { if (this.#restore) { this.#restore(); this.#restore = undefined; } this._restored = true; const idx = ArrayPrototypeIndexOf(activeMocks, this); if (idx !== -1) { ArrayPrototypeSplice(activeMocks, idx, 1); } } _recordCall(thisArg, args, result, error, target) { ArrayPrototypePush(this.#calls, { arguments: args, error, result, stack: new Error(), target, this: thisArg, }); } _shouldMock() { if (this._restored) return false; if (this.#times === undefined) return true; return this.#calls.length < this.#times; } _getImplementation() { return this.#implementation; } _nextImpl() { const nextCall = this.#calls.length; const onceImpl = MapPrototypeGet(this.#onceImplementations, nextCall); if (onceImpl) { MapPrototypeDelete(this.#onceImplementations, nextCall); return onceImpl; } return this.#implementation; } } class MockPropertyContext { #object; #propertyName; #value; #originalValue; #descriptor; #accesses = []; #onceValues = new SafeMap(); _restored = false; constructor(object, propertyName, hasValue, value) { this.#object = object; this.#propertyName = propertyName; this.#descriptor = ObjectGetOwnPropertyDescriptor(object, propertyName); if (!this.#descriptor) { throw new ERR_INVALID_ARG_VALUE( "propertyName", propertyName, "is not a property of the object", ); } this.#originalValue = object[propertyName]; this.#value = hasValue ? value : this.#originalValue; const { configurable, enumerable } = this.#descriptor; ObjectDefineProperty(object, propertyName, { __proto__: null, configurable, enumerable, get: () => { const nextValue = this.#getAccessValue(this.#value); ArrayPrototypePush(this.#accesses, { type: "get", value: nextValue, stack: new Error(), }); return nextValue; }, set: (v) => this.mockImplementation(v), }); } get accesses() { return ArrayPrototypeSlice(this.#accesses, 0); } accessCount() { return this.#accesses.length; } mockImplementation(value) { if (!this.#descriptor.writable) { throw new ERR_INVALID_ARG_VALUE( "propertyName", this.#propertyName, "cannot be set", ); } const nextValue = this.#getAccessValue(value); ArrayPrototypePush(this.#accesses, { type: "set", value: nextValue, stack: new Error(), }); this.#value = nextValue; } #getAccessValue(value) { const accessIndex = this.#accesses.length; let accessValue; if (MapPrototypeHas(this.#onceValues, accessIndex)) { accessValue = MapPrototypeGet(this.#onceValues, accessIndex); MapPrototypeDelete(this.#onceValues, accessIndex); } else { accessValue = value; } return accessValue; } mockImplementationOnce(value, onAccess) { const nextAccess = this.#accesses.length; const accessIndex = onAccess ?? nextAccess; validateInteger(accessIndex, "onAccess", nextAccess); MapPrototypeSet(this.#onceValues, accessIndex, value); } resetAccesses() { this.#accesses = []; } // Alias used by mock.reset() which iterates activeMocks calling resetCalls(). resetCalls() { this.resetAccesses(); } restore() { if (!this._restored) { // Reinstall the pristine original descriptor. Unlike Node we don't force // a `value` field, since the original property may be an accessor (e.g. // `process.platform` is a getter in Deno) and mixing `value` with // `get`/`set` is an invalid descriptor. ObjectDefineProperty(this.#object, this.#propertyName, { __proto__: null, ...this.#descriptor, }); this._restored = true; } const idx = ArrayPrototypeIndexOf(activeMocks, this); if (idx !== -1) { ArrayPrototypeSplice(activeMocks, idx, 1); } } } function createMockFunction(original, implementation, ctx) { const mockFn = function (...args) { const newTarget = new.target; const isCtor = newTarget !== undefined; // The IIFE wrapping this module is sloppy, so a plain call leaks // globalThis as `this`. Match strict-mode/Node semantics. const thisArg = !isCtor && this === globalThis ? undefined : this; const impl = ctx._shouldMock() ? (ctx._nextImpl() ?? implementation ?? original) : original; let result; let error; // If called directly (not via subclass), use the original constructor // so the produced instance has its prototype, and so call.target reports // the user's class (not the mock wrapper). const ctorTarget = isCtor && newTarget === mockFn ? impl : newTarget; try { if (isCtor) { result = impl ? ReflectConstruct(impl, args, ctorTarget) : undefined; } else { result = impl ? ReflectApply(impl, thisArg, args) : undefined; } } catch (e) { error = e; ctx._recordCall( isCtor ? thisArg : thisArg, args, undefined, error, ctorTarget, ); throw e; } ctx._recordCall( isCtor ? result : thisArg, args, result, undefined, ctorTarget, ); return result; }; ObjectDefineProperty(mockFn, "mock", { __proto__: null, value: ctx, writable: false, enumerable: false, configurable: false, }); return mockFn; } function findPropertyDescriptor(obj, name) { let current = obj; while (current !== null && current !== undefined) { const desc = ObjectGetOwnPropertyDescriptor(current, name); if (desc) return desc; current = ObjectGetPrototypeOf(current); } return undefined; } function mockMethodImpl(object, methodName, implementation, options) { if ( implementation !== null && typeof implementation === "object" && typeof implementation !== "function" ) { options = implementation; implementation = undefined; } const descriptor = findPropertyDescriptor(object, methodName); if (!descriptor) { throw new TypeError( `Cannot mock property '${String(methodName)}' because it does not exist`, ); } const isGetter = options?.getter ?? false; const isSetter = options?.setter ?? false; let original; if (isGetter) { original = descriptor.get; } else if (isSetter) { original = descriptor.set; } else { original = descriptor.value; } if (typeof original !== "function") { throw new TypeError( `Cannot mock property '${ String(methodName) }' because it is not a function`, ); } const restore = () => { ObjectDefineProperty(object, methodName, descriptor); }; const impl = implementation === undefined ? original : implementation; const ctx = new MockFunctionContext(impl, restore, options?.times); ArrayPrototypePush(activeMocks, ctx); const mockFn = createMockFunction(original, impl, ctx); const mockDescriptor = { configurable: descriptor.configurable, enumerable: descriptor.enumerable, }; if (isGetter) { mockDescriptor.get = mockFn; mockDescriptor.set = descriptor.set; } else if (isSetter) { mockDescriptor.get = descriptor.get; mockDescriptor.set = mockFn; } else { mockDescriptor.writable = descriptor.writable; mockDescriptor.value = mockFn; } ObjectDefineProperty(object, methodName, mockDescriptor); return mockFn; } const SUPPORTED_APIS = [ "setTimeout", "setInterval", "setImmediate", "Date", "AbortSignal.timeout", ]; class MockTimersHandle { #id; #timer; #timers; #refed; constructor(timer, timers, refed) { this.#id = timer.id; this.#timer = timer; this.#timers = timers; this.#refed = refed; } ref() { this.#refed = true; return this; } unref() { this.#refed = false; return this; } hasRef() { return this.#refed; } refresh() { if (this.#timer && !this.#timer.interval) { this.#timer.fireAt = this.#timers._now + this.#timer.delay; MapPrototypeSet(this.#timers._timers, this.#id, this.#timer); } return this; } [SymbolToPrimitive]() { return this.#id; } [SymbolFor("Deno.customInspect")]() { return `MockTimer { id: ${this.#id} }`; } get _id() { return this.#id; } } // Installs/uninstalls this MockTimers on the `node:timers` module so that // `require("node:timers")` and `import ... from "node:timers[/promises]"` // callers route through the virtual clock too, not just `globalThis`. const kInstallMockTimers = SymbolFor("Deno.internal.node.mockTimers"); class MockTimers { _enabled = false; _now = 0; _timers = new SafeMap(); _nextId = 1; #originals = new SafeMap(); #mockedApis = new SafeMap(); // `AbortSignal.timeout` is a static method, not a `globalThis` binding, so it // is saved/restored separately from `#originals` (which maps global names). #abortSignalTimeoutOriginal = null; #abortSignalTimeoutMocked = false; #mockGlobal(name, value) { if (!MapPrototypeHas(this.#originals, name)) { MapPrototypeSet(this.#originals, name, globalThis[name]); } globalThis[name] = value; } // Whether `api` (e.g. `"setTimeout"`) is currently being intercepted. Read by // the `node:timers` module functions to decide per-call whether to use the // virtual clock, mirroring the per-api selection of `enable({ apis })`. _apiEnabled(api) { return MapPrototypeHas(this.#mockedApis, api); } enable(options = { __proto__: null }) { if (this._enabled) { throw new ERR_INVALID_STATE( "MockTimers is already enabled. Reset it first to enable it again", ); } validateObject(options, "options"); let { apis, now } = options; if (apis === undefined) { apis = SUPPORTED_APIS; } else { validateStringArray(apis, "options.apis"); for (let i = 0; i < apis.length; i++) { if (!ArrayPrototypeIncludes(SUPPORTED_APIS, apis[i])) { throw new ERR_INVALID_ARG_VALUE( `options.apis[${i}]`, apis[i], `must be one of ${ArrayPrototypeJoin(SUPPORTED_APIS, ", ")}`, ); } } } if (now === undefined) { now = 0; } else if (ObjectPrototypeIsPrototypeOf(originalDatePrototype, now)) { now = DatePrototypeGetTime(now); } validateNumber(now, "options.now", 0); if (!NumberIsFinite(now) || !NumberIsInteger(now)) { throw new ERR_INVALID_ARG_VALUE( "options.now", now, "must be a finite, non-negative integer or a Date object", ); } this._enabled = true; this._now = now; for (let i = 0; i < apis.length; i++) { MapPrototypeSet(this.#mockedApis, apis[i], true); } for (let i = 0; i < apis.length; i++) { const api = apis[i]; if (api === "Date") { this.#mockGlobal("Date", createMockDate(this)); } else if (api === "setTimeout") { this.#mockGlobal( "setTimeout", (callback, delay, ...args) => this._setTimeout(callback, delay, args, false), ); this.#mockGlobal( "clearTimeout", (handle) => this._clearTimer(handle), ); } else if (api === "setInterval") { this.#mockGlobal( "setInterval", (callback, delay, ...args) => this._setInterval(callback, delay, args), ); this.#mockGlobal( "clearInterval", (handle) => this._clearTimer(handle), ); } else if (api === "setImmediate") { this.#mockGlobal( "setImmediate", (callback, ...args) => this._setTimeout(callback, 0, args, true), ); this.#mockGlobal( "clearImmediate", (handle) => this._clearTimer(handle), ); } else if (api === "AbortSignal.timeout") { this.#abortSignalTimeoutOriginal = globalThis.AbortSignal.timeout; this.#abortSignalTimeoutMocked = true; globalThis.AbortSignal.timeout = (delay) => this.#mockedAbortSignalTimeout(delay); } } // Route the `node:timers` / `node:timers/promises` module functions through // this instance too (see `kInstallMockTimers` in `timers.ts`). core.loadExtScript("ext:deno_node/timers.ts")[kInstallMockTimers](this); } reset() { if (!this._enabled) return; core.loadExtScript("ext:deno_node/timers.ts")[kInstallMockTimers](null); for ( const { 0: name, 1: original } of new SafeMapIterator(this.#originals) ) { globalThis[name] = original; } if (this.#abortSignalTimeoutMocked) { globalThis.AbortSignal.timeout = this.#abortSignalTimeoutOriginal; this.#abortSignalTimeoutOriginal = null; this.#abortSignalTimeoutMocked = false; } MapPrototypeClear(this.#originals); MapPrototypeClear(this.#mockedApis); MapPrototypeClear(this._timers); this._now = 0; this._nextId = 1; this._enabled = false; } #assertEnabled() { if (!this._enabled) { throw new ERR_INVALID_STATE( "You should enable MockTimers first by calling the .enable function", ); } } #assertTimeArg(time) { if (time < 0) { throw new ERR_INVALID_ARG_VALUE( "time", time, "must be a non-negative integer", ); } } // Mirrors Node's `MockTimers.tick`: advance the clock to each due timer's // scheduled time as it fires (not straight to the end of the window), so a // callback reading `Date.now()` sees the time the timer was scheduled for. // Intervals re-arm and may fire again within the same tick. tick(milliseconds = 1) { this.#assertEnabled(); this.#assertTimeArg(milliseconds); const target = this._now + milliseconds; while (true) { const next = this.#findNextTimer(); if (next === null || next.fireAt > target) break; this._now = next.fireAt; this.#fireTimer(next); } this._now = target; } // Mirrors Node's `MockTimers.runAll`: tick up to the longest pending timer. // Intervals fire as many times as fit within that window, then re-arm past // `_now` so the loop terminates. runAll() { this.#assertEnabled(); const longest = this.#findLongestTimer(); if (longest === null) return; this.tick(longest.fireAt - this._now); } setTime(milliseconds) { validateNumber(milliseconds, "time"); this.#assertTimeArg(milliseconds); this.#assertEnabled(); this._now = milliseconds; } [SymbolDispose]() { this.reset(); } // Mirrors Node's mocked `AbortSignal.timeout`: the returned signal aborts // with a `TimeoutError` once the virtual clock advances past `delay`, instead // of using real time, so `tick()` / `runAll()` drive the timeout. #mockedAbortSignalTimeout(delay) { const controller = new AbortController(); this._setTimeout( () => { controller.abort( new DOMException("The operation timed out.", "TimeoutError"), ); }, delay, [], false, ); return controller.signal; } _setTimeout(callback, delay, args, immediate) { validateFunction(callback, "callback"); if (delay === undefined || delay === null) delay = 1; if (typeof delay !== "number") delay = +delay; if (!NumberIsFinite(delay) || delay < 0) delay = 1; if (delay > 2147483647) delay = 1; const id = this._nextId++; const timer = { id, callback, args, delay, fireAt: this._now + delay, interval: null, immediate, }; MapPrototypeSet(this._timers, id, timer); return new MockTimersHandle(timer, this, true); } _setInterval(callback, delay, args) { validateFunction(callback, "callback"); if (delay === undefined || delay === null) delay = 1; if (typeof delay !== "number") delay = +delay; if (!NumberIsFinite(delay) || delay < 1) delay = 1; if (delay > 2147483647) delay = 1; const id = this._nextId++; const timer = { id, callback, args, delay, fireAt: this._now + delay, interval: delay, immediate: false, }; MapPrototypeSet(this._timers, id, timer); return new MockTimersHandle(timer, this, true); } _clearTimer(handle) { if (handle === null || handle === undefined) return; let id; if (typeof handle === "number") { id = handle; } else if (typeof handle === "object" && typeof handle._id === "number") { id = handle._id; } else { return; } MapPrototypeDelete(this._timers, id); } #findNextTimer() { let next = null; for (const { 1: t } of new SafeMapIterator(this._timers)) { if ( next === null || t.fireAt < next.fireAt || (t.fireAt === next.fireAt && (t.immediate !== next.immediate ? t.immediate : t.id < next.id)) ) { next = t; } } return next; } #findLongestTimer() { let longest = null; for (const { 1: t } of new SafeMapIterator(this._timers)) { if (longest === null || t.fireAt > longest.fireAt) { longest = t; } } return longest; } #fireTimer(timer) { // Match Node: invoke the callback first (errors propagate synchronously out // of `tick`), then re-arm intervals or drop one-shot timers. A timer that // clears itself inside its own callback is already gone from `_timers`, so // the bookkeeping below is a no-op for it. ReflectApply(timer.callback, undefined, timer.args); if (timer.interval !== null) { timer.fireAt += timer.interval; } else { MapPrototypeDelete(this._timers, timer.id); } } } const originalDate = globalThis.Date; const originalDatePrototype = originalDate.prototype; function createMockDate(mockTimers) { function MockDate(...args) { if (!new.target) { return DatePrototypeToString( ReflectConstruct(originalDate, [mockTimers._now], MockDate), ); } if (args.length === 0) { return ReflectConstruct(originalDate, [mockTimers._now], MockDate); } return ReflectConstruct(originalDate, args, MockDate); } ObjectDefineProperty(MockDate, "prototype", { __proto__: null, value: originalDate.prototype, writable: false, }); ObjectDefineProperty(MockDate, "name", { __proto__: null, value: "Date", configurable: true, }); MockDate.now = () => mockTimers._now; MockDate.parse = originalDate.parse; MockDate.UTC = originalDate.UTC; MockDate.isMock = true; MockDate.toString = () => "function Date() { [native code] }"; return MockDate; } const mockTimers = new MockTimers(); // `mock.module()` support. // // Module mocking is built on top of `module.registerHooks()` (the Node module // customization hooks). A single registration intercepts both `import()` and // `require()`. The whole subsystem is lazily initialized on the first // `mock.module()` call so that importing `node:test` without mocking modules // pays no startup cost and registers no hooks. // Symbol under which the live mock registry is published on `globalThis`. The // synthetic module source generated for a mocked specifier reads the live // export values back out of this registry at evaluation time, so functions and // objects passed to `mock.module()` cross into the mocked module unchanged. const kMockModuleRegistryName = "deno.internal.nodeTestMockModules"; // Search param appended to a mocked ESM url to bust the module cache when // `cache` is false. ESM modules cannot be evicted from the loader cache, so a // fresh url (new version) is resolved on every import instead. const kMockSearchParam = "node-test-mock"; const kBadExportsMessage = "Cannot create mock because named exports cannot " + "be applied to the provided default export."; let mockModuleRegistry = null; let mockModuleHooksHandle = null; let nodeModuleNamespace = null; // Monotonic counter used to version mocked ESM urls. A global (rather than // per-mock) counter guarantees that a freshly created mock never collides with // a versioned url that a previous mock already cached. let mockModuleVersion = 0; function getNodeModuleNamespace() { if (nodeModuleNamespace === null) { nodeModuleNamespace = core.createLazyLoader("node:module")(); } return nodeModuleNamespace; } function isUrlLike(value) { return value !== null && typeof value === "object" && typeof value.href === "string" && typeof value.protocol === "string"; } function ensureMockModuleHooks() { if (mockModuleHooksHandle !== null) { return; } mockModuleRegistry = new SafeMap(); globalThis[SymbolFor(kMockModuleRegistryName)] = mockModuleRegistry; const { registerHooks } = getNodeModuleNamespace(); mockModuleHooksHandle = registerHooks({ __proto__: null, resolve: mockModuleResolveHook, load: mockModuleLoadHook, }); } // Best effort discovery of the file that called `mock.module()`, used to // resolve relative specifiers the same way Node does (eagerly, against the // caller). Returns undefined when no user frame can be found, in which case // the default resolver falls back to the current working directory. const kStackFrameUrl = new SafeRegExp( "((?:file|https?):\\/\\/[^\\s)]+?):\\d+:\\d+", ); function getCallerUrl() { const stack = new Error().stack; if (typeof stack !== "string") { return undefined; } const lines = StringPrototypeSplit(stack, "\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if ( StringPrototypeIncludes(line, "ext:deno_node/") || StringPrototypeIncludes(line, "ext:core/") ) { continue; } const match = RegExpPrototypeExec(kStackFrameUrl, line); if (match !== null) { const candidate = match[1]; if ( StringPrototypeStartsWith(candidate, "file:") || StringPrototypeStartsWith(candidate, "http") ) { return candidate; } } } return undefined; } let nodeUrlNamespace = null; function getNodeUrl() { if (nodeUrlNamespace === null) { nodeUrlNamespace = core.createLazyLoader("node:url")(); } return nodeUrlNamespace; } // Normalizes a builtin specifier so that both `readline` and `node:readline` // map to the same registry key. function normalizeNodeScheme(specifier) { if (StringPrototypeStartsWith(specifier, "node:")) { return specifier; } const { isBuiltin } = getNodeModuleNamespace(); if (isBuiltin(specifier)) { return "node:" + specifier; } return specifier; } // Canonicalizes a resolved url into the registry key. Builtins are normalized // to the `node:` scheme; file urls are resolved through the real path so that // the key registered by `mock.module()` matches the (realpath based) url the // require/import hooks observe, regardless of symlinks such as macOS // `/tmp` to `/private/tmp`. function canonicalizeUrlKey(u) { if (StringPrototypeStartsWith(u, "file:")) { try { const path = getNodeUrl().fileURLToPath(u); const real = core.ops.op_require_real_path(path); return getNodeUrl().pathToFileURL(real).href; } catch { return u; } } return normalizeNodeScheme(u); } // Resolves a `mock.module()` specifier to the canonical registry key (the // resolved module url, with builtins normalized to the `node:` scheme). function resolveSpecifierToKey(specifier) { if (StringPrototypeStartsWith(specifier, "node:")) { return specifier; } const parentUrl = getCallerUrl(); let resolved; try { resolved = core.ops.op_module_default_resolve(specifier, parentUrl); } catch { resolved = specifier; } return canonicalizeUrlKey(resolved); } // Strips the cache busting search param so a versioned ESM url maps back to its // registry key. function stripMockParam(u) { const idx = StringPrototypeIndexOf(u, kMockSearchParam); if (idx === -1) { return u; } try { const parsed = new URL(u); parsed.searchParams.delete(kMockSearchParam); let href = parsed.href; if (StringPrototypeEndsWith(href, "?")) { href = StringPrototypeSlice(href, 0, href.length - 1); } return href; } catch { return u; } } function appendMockParam(u, version) { const sep = StringPrototypeIncludes(u, "?") ? "&" : "?"; return u + sep + kMockSearchParam + "=" + version; } // Looks up the active mock entry (if any) for a url seen by a hook. function lookupMockEntry(u) { if (mockModuleRegistry === null) { return undefined; } let entry = MapPrototypeGet(mockModuleRegistry, u); if (entry === undefined && StringPrototypeIncludes(u, kMockSearchParam)) { entry = MapPrototypeGet(mockModuleRegistry, stripMockParam(u)); } if (entry === undefined) { return undefined; } return entry.active ? entry : undefined; } function detectFormat(key) { if (StringPrototypeStartsWith(key, "node:")) { return "commonjs"; } if ( StringPrototypeEndsWith(key, ".mjs") || StringPrototypeEndsWith(key, ".mts") ) { return "module"; } if ( StringPrototypeEndsWith(key, ".cjs") || StringPrototypeEndsWith(key, ".cts") ) { return "commonjs"; } if (StringPrototypeStartsWith(key, "file:")) { try { const path = getNodeUrl().fileURLToPath(key); return core.ops.op_require_is_maybe_cjs(path) ? "commonjs" : "module"; } catch { return "commonjs"; } } // Remote modules (http/https) are only loadable as ESM. if ( StringPrototypeStartsWith(key, "http:") || StringPrototypeStartsWith(key, "https:") ) { return "module"; } return "commonjs"; } function registryAccessSource() { return "globalThis[Symbol.for(" + JSONStringify(kMockModuleRegistryName) + ")]"; } function generateCjsSource(entry, key) { let src = '"use strict";\n'; src += "const $e = " + registryAccessSource() + ".get(" + JSONStringify(key) + ");\n"; src += "if ($e === undefined) { throw new Error(" + JSONStringify('mock exports not found for "' + key + '"') + "); }\n"; if (entry.hasDefaultExport) { src += "module.exports = $e.moduleExports.default;\n"; } if (entry.exportNames.length > 0) { src += "if (module.exports === null || typeof module.exports !== " + '"object") { throw new Error(' + JSONStringify(kBadExportsMessage) + "); }\n"; for (let i = 0; i < entry.exportNames.length; i++) { const name = entry.exportNames[i]; src += "module.exports[" + JSONStringify(name) + "] = " + "$e.moduleExports[" + JSONStringify(name) + "];\n"; } } return src; } function generateEsmSource(entry, key) { let src = "const $e = " + registryAccessSource() + ".get(" + JSONStringify(key) + ");\n"; src += "if ($e === undefined) { throw new Error(" + JSONStringify('mock exports not found for "' + key + '"') + "); }\n"; if (entry.isCjs) { // For a CommonJS module the named exports are applied onto the default // export object, mirroring how Deno exposes a required CJS module to an // ESM importer (default plus the merged keys). src += "let $d = $e.hasDefaultExport ? $e.moduleExports.default : {};\n"; if (entry.exportNames.length > 0) { src += 'if ($d === null || typeof $d !== "object") { throw new Error(' + JSONStringify(kBadExportsMessage) + "); }\n"; for (let i = 0; i < entry.exportNames.length; i++) { const name = entry.exportNames[i]; src += "$d[" + JSONStringify(name) + "] = $e.moduleExports[" + JSONStringify(name) + "];\n"; } } src += "export default $d;\n"; for (let i = 0; i < entry.exportNames.length; i++) { const name = entry.exportNames[i]; // Use a safe local identifier plus a string export name so export names // that are not valid identifiers (e.g. "foo-bar") still generate valid // source. src += "let $n" + i + " = $d[" + JSONStringify(name) + "];\n"; src += "export { $n" + i + " as " + JSONStringify(name) + " };\n"; } } else { // For an ESM module the default export and named exports are independent. if (entry.hasDefaultExport) { src += "export default $e.moduleExports.default;\n"; } for (let i = 0; i < entry.exportNames.length; i++) { const name = entry.exportNames[i]; src += "let $n" + i + " = $e.moduleExports[" + JSONStringify(name) + "];\n"; src += "export { $n" + i + " as " + JSONStringify(name) + " };\n"; } } return src; } // The require cache (Module._cache) key for a mocked module: builtins are keyed // by their `node:` url, file modules by their (real) path. function cjsCacheKeyFor(key) { if (StringPrototypeStartsWith(key, "file:")) { try { return getNodeUrl().fileURLToPath(key); } catch { return key; } } return key; } // Removes a mocked CommonJS module from the require cache so the next // `require()` re-runs the load hook and produces a fresh exports object. function evictCjsCache(key) { const { default: Module } = getNodeModuleNamespace(); delete Module._cache[cjsCacheKeyFor(key)]; } function mockModuleResolveHook(specifier, context, nextResolve) { const result = nextResolve(specifier, context); if (result === null || result === undefined || result.url == null) { return result; } const conditions = context?.conditions; // Only the ESM import path needs per-import versioned urls. The require path // busts its cache by evicting the require cache entry in the load hook. if ( conditions === undefined || !ArrayPrototypeIncludes(conditions, "import") ) { return result; } const entry = lookupMockEntry(canonicalizeUrlKey(result.url)); if (entry === undefined) { return result; } // A stable version (cache: true) reuses the cached module; otherwise bump the // global counter so every import resolves to a brand new url. const version = entry.cache ? entry.stableVersion : ++mockModuleVersion; return { __proto__: null, url: appendMockParam(result.url, version), shortCircuit: true, }; } function mockModuleLoadHook(url, context, nextLoad) { const key = canonicalizeUrlKey(stripMockParam(url)); const entry = lookupMockEntry(key); if (entry === undefined) { return nextLoad(url, context); } const conditions = context?.conditions; const viaRequire = conditions !== undefined && ArrayPrototypeIncludes(conditions, "require"); if (viaRequire) { if (!entry.cache) { evictCjsCache(key); } return { __proto__: null, source: generateCjsSource(entry, key), format: "commonjs", shortCircuit: true, }; } return { __proto__: null, source: generateEsmSource(entry, key), format: "module", shortCircuit: true, }; } class MockModuleContext { #restore; constructor(restore) { this.#restore = restore; } restore() { if (this.#restore !== undefined) { this.#restore(); this.#restore = undefined; } const idx = ArrayPrototypeIndexOf(activeMocks, this); if (idx !== -1) { ArrayPrototypeSplice(activeMocks, idx, 1); } } // `mock.reset()` iterates `activeMocks` calling `resetCalls()`. Node's // `MockTracker.reset()` restores module mocks, so alias it to the restore // behavior. The splice from `activeMocks` is deferred to `restore()` to // avoid mutating the array mid iteration. resetCalls() { if (this.#restore !== undefined) { this.#restore(); this.#restore = undefined; } } } function mockModule(specifier, options = { __proto__: null }) { let specStr; if (typeof specifier === "string") { specStr = specifier; } else if (isUrlLike(specifier)) { specStr = `${specifier}`; } else { throw new ERR_INVALID_ARG_TYPE( "specifier", ["string", "URL"], specifier, ); } validateObject(options, "options"); const cache = options.cache === undefined ? false : options.cache; validateBoolean(cache, "options.cache"); // `exports` is a legacy alias that bundles `defaultExport` and // `namedExports` into a single object: its `default` property (if any) // becomes the default export and its remaining own enumerable keys become // named exports. It cannot be combined with either of the newer options. const exportsOption = options.exports; const namedExports = options.namedExports; let exportSource = namedExports; let hasDefaultExport = ObjectPrototypeHasOwnProperty( options, "defaultExport", ); let defaultExportValue = options.defaultExport; if (exportsOption !== undefined) { if (namedExports !== undefined || options.defaultExport !== undefined) { throw new ERR_INVALID_ARG_VALUE( "options.exports", exportsOption, 'cannot be used with "namedExports" or "defaultExport"', ); } validateObject(exportsOption, "options.exports"); exportSource = exportsOption; if (ObjectPrototypeHasOwnProperty(exportsOption, "default")) { hasDefaultExport = true; defaultExportValue = exportsOption.default; } } else if (namedExports !== undefined) { validateObject(namedExports, "options.namedExports"); } ensureMockModuleHooks(); const moduleExports = { __proto__: null }; const exportNames = []; if (exportSource !== undefined && exportSource !== null) { const keys = ObjectKeys(exportSource); for (let i = 0; i < keys.length; i++) { const name = keys[i]; // For the `exports` form the `default` key is the default export, not a // named export. if (exportsOption !== undefined && name === "default") { continue; } moduleExports[name] = exportSource[name]; ArrayPrototypePush(exportNames, name); } } if (hasDefaultExport) { moduleExports.default = defaultExportValue; } const key = resolveSpecifierToKey(specStr); const existing = MapPrototypeGet(mockModuleRegistry, key); if (existing !== undefined && existing.active) { throw new ERR_INVALID_STATE( `Cannot mock '${specStr}'. The module is already mocked.`, ); } const format = detectFormat(key); const entry = { __proto__: null, url: key, isCjs: format === "commonjs", cache, stableVersion: cache ? ++mockModuleVersion : 0, active: true, moduleExports, exportNames, hasDefaultExport, }; MapPrototypeSet(mockModuleRegistry, key, entry); // Save and evict any previously loaded copy of this module so the next // require/import re-runs the hooks and observes the mock. The saved entry is // put back on restore so the exact original instance is returned again. const { default: Module } = getNodeModuleNamespace(); const cacheKey = cjsCacheKeyFor(key); entry.savedCacheEntry = Module._cache[cacheKey]; delete Module._cache[cacheKey]; const restore = () => { if (!entry.active) { return; } entry.active = false; if (MapPrototypeGet(mockModuleRegistry, key) === entry) { MapPrototypeDelete(mockModuleRegistry, key); } delete Module._cache[cacheKey]; if (entry.savedCacheEntry !== undefined) { Module._cache[cacheKey] = entry.savedCacheEntry; } }; const ctx = new MockModuleContext(restore); ArrayPrototypePush(activeMocks, ctx); return ctx; } const mock = { fn: (original, implementation, options) => { if (original !== null && typeof original === "object") { options = original; original = undefined; implementation = undefined; } else if ( implementation !== null && typeof implementation === "object" ) { options = implementation; implementation = original; } const ctx = new MockFunctionContext( implementation ?? original, undefined, options?.times, ); ArrayPrototypePush(activeMocks, ctx); const mockFn = createMockFunction( original, implementation ?? original, ctx, ); return mockFn; }, getter: (object, methodName, implementation, options) => { if (implementation !== null && typeof implementation === "object") { options = implementation; implementation = undefined; } return mockMethodImpl(object, methodName, implementation, { ...options, getter: true, }); }, method: (object, methodName, implementation, options) => { return mockMethodImpl(object, methodName, implementation, options); }, module: mockModule, property: function (object, propertyName, value) { validateObject(object, "object"); if ( typeof propertyName !== "string" && typeof propertyName !== "symbol" ) { throw new ERR_INVALID_ARG_TYPE( "propertyName", ["string", "symbol"], propertyName, ); } const hasValue = arguments.length > 2; const ctx = new MockPropertyContext( object, propertyName, hasValue, value, ); ArrayPrototypePush(activeMocks, ctx); return new Proxy(object, { __proto__: null, get(target, property, receiver) { if (property === "mock") { return ctx; } return ReflectGet(target, property, receiver); }, }); }, reset: () => { mockTimers.reset(); ArrayPrototypeForEach(activeMocks, (ctx) => { ctx.resetCalls(); }); }, restoreAll: () => { while (activeMocks.length > 0) { const ctx = activeMocks[activeMocks.length - 1]; ctx.restore(); } }, setter: (object, methodName, implementation, options) => { if (implementation !== null && typeof implementation === "object") { options = implementation; implementation = undefined; } return mockMethodImpl(object, methodName, implementation, { ...options, setter: true, }); }, timers: { enable: (options) => mockTimers.enable(options), reset: () => mockTimers.reset(), tick: (ms) => mockTimers.tick(ms), runAll: () => mockTimers.runAll(), // `setTime` is MockTimers' own method, not Date.prototype.setTime. // deno-lint-ignore deno-internal/prefer-primordials setTime: (ms) => mockTimers.setTime(ms), [SymbolDispose]: () => mockTimers.reset(), }, }; test.test = test; test.mock = mock; test.before = before; test.after = after; test.beforeEach = beforeEach; test.afterEach = afterEach; test.run = run; return { run, test, suite, it, describe, before, after, beforeEach, afterEach, mock, getTestContext, default: test, }; })();