summaryrefslogtreecommitdiff
path: root/runtime/js/40_testing.js
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/js/40_testing.js')
-rw-r--r--runtime/js/40_testing.js1408
1 files changed, 0 insertions, 1408 deletions
diff --git a/runtime/js/40_testing.js b/runtime/js/40_testing.js
deleted file mode 100644
index e8590faf1..000000000
--- a/runtime/js/40_testing.js
+++ /dev/null
@@ -1,1408 +0,0 @@
-// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
-"use strict";
-
-((window) => {
- const core = window.Deno.core;
- const ops = core.ops;
- const { setExitHandler } = window.__bootstrap.os;
- const { Console } = window.__bootstrap.console;
- const { serializePermissions } = window.__bootstrap.permissions;
- const { assert } = window.__bootstrap.infra;
- const {
- ArrayFrom,
- ArrayPrototypeFilter,
- ArrayPrototypeJoin,
- ArrayPrototypeMap,
- ArrayPrototypePush,
- ArrayPrototypeShift,
- ArrayPrototypeSort,
- BigInt,
- DateNow,
- Error,
- FunctionPrototype,
- Map,
- MapPrototypeGet,
- MapPrototypeHas,
- MapPrototypeSet,
- MathCeil,
- ObjectKeys,
- ObjectPrototypeIsPrototypeOf,
- Promise,
- SafeArrayIterator,
- Set,
- SymbolToStringTag,
- TypeError,
- } = window.__bootstrap.primordials;
-
- const opSanitizerDelayResolveQueue = [];
-
- // Even if every resource is closed by the end of a test, there can be a delay
- // until the pending ops have all finished. This function returns a promise
- // that resolves when it's (probably) fine to run the op sanitizer.
- //
- // This is implemented by adding a macrotask callback that runs after the
- // timer macrotasks, so we can guarantee that a currently running interval
- // will have an associated op. An additional `setTimeout` of 0 is needed
- // before that, though, in order to give time for worker message ops to finish
- // (since timeouts of 0 don't queue tasks in the timer queue immediately).
- function opSanitizerDelay() {
- return new Promise((resolve) => {
- setTimeout(() => {
- ArrayPrototypePush(opSanitizerDelayResolveQueue, resolve);
- }, 0);
- });
- }
-
- function handleOpSanitizerDelayMacrotask() {
- ArrayPrototypeShift(opSanitizerDelayResolveQueue)?.();
- return opSanitizerDelayResolveQueue.length === 0;
- }
-
- // An async operation to $0 was started in this test, but never completed. This is often caused by not $1.
- // An async operation to $0 was started in this test, but never completed. Async operations should not complete in a test if they were not started in that test.
- // deno-fmt-ignore
- const OP_DETAILS = {
- "op_blob_read_part": ["read from a Blob or File", "awaiting the result of a Blob or File read"],
- "op_broadcast_recv": ["receive a message from a BroadcastChannel", "closing the BroadcastChannel"],
- "op_broadcast_send": ["send a message to a BroadcastChannel", "closing the BroadcastChannel"],
- "op_chmod_async": ["change the permissions of a file", "awaiting the result of a `Deno.chmod` call"],
- "op_chown_async": ["change the owner of a file", "awaiting the result of a `Deno.chown` call"],
- "op_copy_file_async": ["copy a file", "awaiting the result of a `Deno.copyFile` call"],
- "op_crypto_decrypt": ["decrypt data", "awaiting the result of a `crypto.subtle.decrypt` call"],
- "op_crypto_derive_bits": ["derive bits from a key", "awaiting the result of a `crypto.subtle.deriveBits` call"],
- "op_crypto_encrypt": ["encrypt data", "awaiting the result of a `crypto.subtle.encrypt` call"],
- "op_crypto_generate_key": ["generate a key", "awaiting the result of a `crypto.subtle.generateKey` call"],
- "op_crypto_sign_key": ["sign data", "awaiting the result of a `crypto.subtle.sign` call"],
- "op_crypto_subtle_digest": ["digest data", "awaiting the result of a `crypto.subtle.digest` call"],
- "op_crypto_verify_key": ["verify data", "awaiting the result of a `crypto.subtle.verify` call"],
- "op_net_recv_udp": ["receive a datagram message via UDP", "awaiting the result of `Deno.DatagramConn#receive` call, or not breaking out of a for await loop looping over a `Deno.DatagramConn`"],
- "op_net_recv_unixpacket": ["receive a datagram message via Unixpacket", "awaiting the result of `Deno.DatagramConn#receive` call, or not breaking out of a for await loop looping over a `Deno.DatagramConn`"],
- "op_net_send_udp": ["send a datagram message via UDP", "awaiting the result of `Deno.DatagramConn#send` call"],
- "op_net_send_unixpacket": ["send a datagram message via Unixpacket", "awaiting the result of `Deno.DatagramConn#send` call"],
- "op_dns_resolve": ["resolve a DNS name", "awaiting the result of a `Deno.resolveDns` call"],
- "op_fdatasync_async": ["flush pending data operations for a file to disk", "awaiting the result of a `Deno.fdatasync` call"],
- "op_fetch_send": ["send a HTTP request", "awaiting the result of a `fetch` call"],
- "op_ffi_call_nonblocking": ["do a non blocking ffi call", "awaiting the returned promise"] ,
- "op_ffi_call_ptr_nonblocking": ["do a non blocking ffi call", "awaiting the returned promise"],
- "op_flock_async": ["lock a file", "awaiting the result of a `Deno.flock` call"],
- "op_fs_events_poll": ["get the next file system event", "breaking out of a for await loop looping over `Deno.FsEvents`"],
- "op_fstat_async": ["get file metadata", "awaiting the result of a `Deno.File#fstat` call"],
- "op_fsync_async": ["flush pending data operations for a file to disk", "awaiting the result of a `Deno.fsync` call"],
- "op_ftruncate_async": ["truncate a file", "awaiting the result of a `Deno.ftruncate` call"],
- "op_funlock_async": ["unlock a file", "awaiting the result of a `Deno.funlock` call"],
- "op_futime_async": ["change file timestamps", "awaiting the result of a `Deno.futime` call"],
- "op_http_accept": ["accept a HTTP request", "closing a `Deno.HttpConn`"],
- "op_http_shutdown": ["shutdown a HTTP connection", "awaiting `Deno.HttpEvent#respondWith`"],
- "op_http_upgrade_websocket": ["upgrade a HTTP connection to a WebSocket", "awaiting `Deno.HttpEvent#respondWith`"],
- "op_http_write_headers": ["write HTTP response headers", "awaiting `Deno.HttpEvent#respondWith`"],
- "op_http_write": ["write HTTP response body", "awaiting `Deno.HttpEvent#respondWith`"],
- "op_link_async": ["create a hard link", "awaiting the result of a `Deno.link` call"],
- "op_make_temp_dir_async": ["create a temporary directory", "awaiting the result of a `Deno.makeTempDir` call"],
- "op_make_temp_file_async": ["create a temporary file", "awaiting the result of a `Deno.makeTempFile` call"],
- "op_message_port_recv_message": ["receive a message from a MessagePort", "awaiting the result of not closing a `MessagePort`"],
- "op_mkdir_async": ["create a directory", "awaiting the result of a `Deno.mkdir` call"],
- "op_net_accept_tcp": ["accept a TCP stream", "closing a `Deno.Listener`"],
- "op_net_accept_unix": ["accept a Unix stream", "closing a `Deno.Listener`"],
- "op_net_connect_tcp": ["connect to a TCP server", "awaiting a `Deno.connect` call"],
- "op_net_connect_unix": ["connect to a Unix server", "awaiting a `Deno.connect` call"],
- "op_open_async": ["open a file", "awaiting the result of a `Deno.open` call"],
- "op_read_dir_async": ["read a directory", "collecting all items in the async iterable returned from a `Deno.readDir` call"],
- "op_read_link_async": ["read a symlink", "awaiting the result of a `Deno.readLink` call"],
- "op_realpath_async": ["resolve a path", "awaiting the result of a `Deno.realpath` call"],
- "op_remove_async": ["remove a file or directory", "awaiting the result of a `Deno.remove` call"],
- "op_rename_async": ["rename a file or directory", "awaiting the result of a `Deno.rename` call"],
- "op_run_status": ["get the status of a subprocess", "awaiting the result of a `Deno.Process#status` call"],
- "op_seek_async": ["seek in a file", "awaiting the result of a `Deno.File#seek` call"],
- "op_signal_poll": ["get the next signal", "un-registering a OS signal handler"],
- "op_sleep": ["sleep for a duration", "cancelling a `setTimeout` or `setInterval` call"],
- "op_stat_async": ["get file metadata", "awaiting the result of a `Deno.stat` call"],
- "op_symlink_async": ["create a symlink", "awaiting the result of a `Deno.symlink` call"],
- "op_net_accept_tls": ["accept a TLS stream", "closing a `Deno.TlsListener`"],
- "op_net_connect_tls": ["connect to a TLS server", "awaiting a `Deno.connectTls` call"],
- "op_tls_handshake": ["perform a TLS handshake", "awaiting a `Deno.TlsConn#handshake` call"],
- "op_tls_start": ["start a TLS connection", "awaiting a `Deno.startTls` call"],
- "op_truncate_async": ["truncate a file", "awaiting the result of a `Deno.truncate` call"],
- "op_utime_async": ["change file timestamps", "awaiting the result of a `Deno.utime` call"],
- "op_webgpu_buffer_get_map_async": ["map a WebGPU buffer", "awaiting the result of a `GPUBuffer#mapAsync` call"],
- "op_webgpu_request_adapter": ["request a WebGPU adapter", "awaiting the result of a `navigator.gpu.requestAdapter` call"],
- "op_webgpu_request_device": ["request a WebGPU device", "awaiting the result of a `GPUAdapter#requestDevice` call"],
- "op_worker_recv_message": ["receive a message from a web worker", "terminating a `Worker`"],
- "op_ws_close": ["close a WebSocket", "awaiting until the `close` event is emitted on a `WebSocket`, or the `WebSocketStream#closed` promise resolves"],
- "op_ws_create": ["create a WebSocket", "awaiting until the `open` event is emitted on a `WebSocket`, or the result of a `WebSocketStream#connection` promise"],
- "op_ws_next_event": ["receive the next message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"],
- "op_ws_send": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"],
- };
-
- // Wrap test function in additional assertion that makes sure
- // the test case does not leak async "ops" - ie. number of async
- // completed ops after the test is the same as number of dispatched
- // ops. Note that "unref" ops are ignored since in nature that are
- // optional.
- function assertOps(fn) {
- /** @param desc {TestDescription | TestStepDescription} */
- return async function asyncOpSanitizer(desc) {
- const pre = core.metrics();
- const preTraces = new Map(core.opCallTraces);
- try {
- await fn(desc);
- } finally {
- // Defer until next event loop turn - that way timeouts and intervals
- // cleared can actually be removed from resource table, otherwise
- // false positives may occur (https://github.com/denoland/deno/issues/4591)
- await opSanitizerDelay();
- await opSanitizerDelay();
- }
-
- if (shouldSkipSanitizers(desc)) return;
-
- const post = core.metrics();
- const postTraces = new Map(core.opCallTraces);
-
- // We're checking diff because one might spawn HTTP server in the background
- // that will be a pending async op before test starts.
- const dispatchedDiff = post.opsDispatchedAsync - pre.opsDispatchedAsync;
- const completedDiff = post.opsCompletedAsync - pre.opsCompletedAsync;
-
- if (dispatchedDiff === completedDiff) return;
-
- const details = [];
- for (const key in post.ops) {
- const preOp = pre.ops[key] ??
- { opsDispatchedAsync: 0, opsCompletedAsync: 0 };
- const postOp = post.ops[key];
- const dispatchedDiff = postOp.opsDispatchedAsync -
- preOp.opsDispatchedAsync;
- const completedDiff = postOp.opsCompletedAsync -
- preOp.opsCompletedAsync;
-
- if (dispatchedDiff > completedDiff) {
- const [name, hint] = OP_DETAILS[key] || [key, null];
- const count = dispatchedDiff - completedDiff;
- let message = `${count} async operation${
- count === 1 ? "" : "s"
- } to ${name} ${
- count === 1 ? "was" : "were"
- } started in this test, but never completed.`;
- if (hint) {
- message += ` This is often caused by not ${hint}.`;
- }
- const traces = [];
- for (const [id, { opName, stack }] of postTraces) {
- if (opName !== key) continue;
- if (MapPrototypeHas(preTraces, id)) continue;
- ArrayPrototypePush(traces, stack);
- }
- if (traces.length === 1) {
- message += " The operation was started here:\n";
- message += traces[0];
- } else if (traces.length > 1) {
- message += " The operations were started here:\n";
- message += ArrayPrototypeJoin(traces, "\n\n");
- }
- ArrayPrototypePush(details, message);
- } else if (dispatchedDiff < completedDiff) {
- const [name, hint] = OP_DETAILS[key] || [key, null];
- const count = completedDiff - dispatchedDiff;
- ArrayPrototypePush(
- details,
- `${count} async operation${count === 1 ? "" : "s"} to ${name} ${
- count === 1 ? "was" : "were"
- } started before this test, but ${
- count === 1 ? "was" : "were"
- } completed during the test. Async operations should not complete in a test if they were not started in that test.
- ${hint ? `This is often caused by not ${hint}.` : ""}`,
- );
- }
- }
-
- let msg = `Test case is leaking async ops.
-
- - ${ArrayPrototypeJoin(details, "\n - ")}`;
-
- if (!core.isOpCallTracingEnabled()) {
- msg +=
- `\n\nTo get more details where ops were leaked, run again with --trace-ops flag.`;
- } else {
- msg += "\n";
- }
-
- throw assert(false, msg);
- };
- }
-
- function prettyResourceNames(name) {
- switch (name) {
- case "fsFile":
- return ["A file", "opened", "closed"];
- case "fetchRequest":
- return ["A fetch request", "started", "finished"];
- case "fetchRequestBody":
- return ["A fetch request body", "created", "closed"];
- case "fetchResponseBody":
- return ["A fetch response body", "created", "consumed"];
- case "httpClient":
- return ["An HTTP client", "created", "closed"];
- case "dynamicLibrary":
- return ["A dynamic library", "loaded", "unloaded"];
- case "httpConn":
- return ["An inbound HTTP connection", "accepted", "closed"];
- case "httpStream":
- return ["An inbound HTTP request", "accepted", "closed"];
- case "tcpStream":
- return ["A TCP connection", "opened/accepted", "closed"];
- case "unixStream":
- return ["A Unix connection", "opened/accepted", "closed"];
- case "tlsStream":
- return ["A TLS connection", "opened/accepted", "closed"];
- case "tlsListener":
- return ["A TLS listener", "opened", "closed"];
- case "unixListener":
- return ["A Unix listener", "opened", "closed"];
- case "unixDatagram":
- return ["A Unix datagram", "opened", "closed"];
- case "tcpListener":
- return ["A TCP listener", "opened", "closed"];
- case "udpSocket":
- return ["A UDP socket", "opened", "closed"];
- case "timer":
- return ["A timer", "started", "fired/cleared"];
- case "textDecoder":
- return ["A text decoder", "created", "finished"];
- case "messagePort":
- return ["A message port", "created", "closed"];
- case "webSocketStream":
- return ["A WebSocket", "opened", "closed"];
- case "fsEvents":
- return ["A file system watcher", "created", "closed"];
- case "childStdin":
- return ["A child process stdin", "opened", "closed"];
- case "childStdout":
- return ["A child process stdout", "opened", "closed"];
- case "childStderr":
- return ["A child process stderr", "opened", "closed"];
- case "child":
- return ["A child process", "started", "closed"];
- case "signal":
- return ["A signal listener", "created", "fired/cleared"];
- case "stdin":
- return ["The stdin pipe", "opened", "closed"];
- case "stdout":
- return ["The stdout pipe", "opened", "closed"];
- case "stderr":
- return ["The stderr pipe", "opened", "closed"];
- case "compression":
- return ["A CompressionStream", "created", "closed"];
- default:
- return [`A "${name}" resource`, "created", "cleaned up"];
- }
- }
-
- function resourceCloseHint(name) {
- switch (name) {
- case "fsFile":
- return "Close the file handle by calling `file.close()`.";
- case "fetchRequest":
- return "Await the promise returned from `fetch()` or abort the fetch with an abort signal.";
- case "fetchRequestBody":
- return "Terminate the request body `ReadableStream` by closing or erroring it.";
- case "fetchResponseBody":
- return "Consume or close the response body `ReadableStream`, e.g `await resp.text()` or `await resp.body.cancel()`.";
- case "httpClient":
- return "Close the HTTP client by calling `httpClient.close()`.";
- case "dynamicLibrary":
- return "Unload the dynamic library by calling `dynamicLibrary.close()`.";
- case "httpConn":
- return "Close the inbound HTTP connection by calling `httpConn.close()`.";
- case "httpStream":
- return "Close the inbound HTTP request by responding with `e.respondWith().` or closing the HTTP connection.";
- case "tcpStream":
- return "Close the TCP connection by calling `tcpConn.close()`.";
- case "unixStream":
- return "Close the Unix socket connection by calling `unixConn.close()`.";
- case "tlsStream":
- return "Close the TLS connection by calling `tlsConn.close()`.";
- case "tlsListener":
- return "Close the TLS listener by calling `tlsListener.close()`.";
- case "unixListener":
- return "Close the Unix socket listener by calling `unixListener.close()`.";
- case "unixDatagram":
- return "Close the Unix datagram socket by calling `unixDatagram.close()`.";
- case "tcpListener":
- return "Close the TCP listener by calling `tcpListener.close()`.";
- case "udpSocket":
- return "Close the UDP socket by calling `udpSocket.close()`.";
- case "timer":
- return "Clear the timer by calling `clearInterval` or `clearTimeout`.";
- case "textDecoder":
- return "Close the text decoder by calling `textDecoder.decode('')` or `await textDecoderStream.readable.cancel()`.";
- case "messagePort":
- return "Close the message port by calling `messagePort.close()`.";
- case "webSocketStream":
- return "Close the WebSocket by calling `webSocket.close()`.";
- case "fsEvents":
- return "Close the file system watcher by calling `watcher.close()`.";
- case "childStdin":
- return "Close the child process stdin by calling `proc.stdin.close()`.";
- case "childStdout":
- return "Close the child process stdout by calling `proc.stdout.close()`.";
- case "childStderr":
- return "Close the child process stderr by calling `proc.stderr.close()`.";
- case "child":
- return "Close the child process by calling `proc.kill()` or `proc.close()`.";
- case "signal":
- return "Clear the signal listener by calling `Deno.removeSignalListener`.";
- case "stdin":
- return "Close the stdin pipe by calling `Deno.stdin.close()`.";
- case "stdout":
- return "Close the stdout pipe by calling `Deno.stdout.close()`.";
- case "stderr":
- return "Close the stderr pipe by calling `Deno.stderr.close()`.";
- case "compression":
- return "Close the compression stream by calling `await stream.writable.close()`.";
- default:
- return "Close the resource before the end of the test.";
- }
- }
-
- // Wrap test function in additional assertion that makes sure
- // the test case does not "leak" resources - ie. resource table after
- // the test has exactly the same contents as before the test.
- function assertResources(fn) {
- /** @param desc {TestDescription | TestStepDescription} */
- return async function resourceSanitizer(desc) {
- const pre = core.resources();
- await fn(desc);
-
- if (shouldSkipSanitizers(desc)) {
- return;
- }
-
- const post = core.resources();
-
- const allResources = new Set([
- ...new SafeArrayIterator(ObjectKeys(pre)),
- ...new SafeArrayIterator(ObjectKeys(post)),
- ]);
-
- const details = [];
- for (const resource of allResources) {
- const preResource = pre[resource];
- const postResource = post[resource];
- if (preResource === postResource) continue;
-
- if (preResource === undefined) {
- const [name, action1, action2] = prettyResourceNames(postResource);
- const hint = resourceCloseHint(postResource);
- const detail =
- `${name} (rid ${resource}) was ${action1} during the test, but not ${action2} during the test. ${hint}`;
- ArrayPrototypePush(details, detail);
- } else {
- const [name, action1, action2] = prettyResourceNames(preResource);
- const detail =
- `${name} (rid ${resource}) was ${action1} before the test started, but was ${action2} during the test. Do not close resources in a test that were not created during that test.`;
- ArrayPrototypePush(details, detail);
- }
- }
-
- const message = `Test case is leaking ${details.length} resource${
- details.length === 1 ? "" : "s"
- }:
-
- - ${details.join("\n - ")}
-`;
- assert(details.length === 0, message);
- };
- }
-
- // Wrap test function in additional assertion that makes sure
- // that the test case does not accidentally exit prematurely.
- function assertExit(fn, isTest) {
- return async function exitSanitizer(...params) {
- setExitHandler((exitCode) => {
- assert(
- false,
- `${
- isTest ? "Test case" : "Bench"
- } attempted to exit with exit code: ${exitCode}`,
- );
- });
-
- try {
- await fn(...new SafeArrayIterator(params));
- } catch (err) {
- throw err;
- } finally {
- setExitHandler(null);
- }
- };
- }
-
- function assertTestStepScopes(fn) {
- /** @param desc {TestDescription | TestStepDescription} */
- return async function testStepSanitizer(desc) {
- preValidation();
- // only report waiting after pre-validation
- if (canStreamReporting(desc) && "parent" in desc) {
- stepReportWait(desc);
- }
- await fn(MapPrototypeGet(testStates, desc.id).context);
- testStepPostValidation(desc);
-
- function preValidation() {
- const runningStepDescs = getRunningStepDescs();
- const runningStepDescsWithSanitizers = ArrayPrototypeFilter(
- runningStepDescs,
- (d) => usesSanitizer(d),
- );
-
- if (runningStepDescsWithSanitizers.length > 0) {
- throw new Error(
- "Cannot start test step while another test step with sanitizers is running.\n" +
- runningStepDescsWithSanitizers
- .map((d) => ` * ${getFullName(d)}`)
- .join("\n"),
- );
- }
-
- if (usesSanitizer(desc) && runningStepDescs.length > 0) {
- throw new Error(
- "Cannot start test step with sanitizers while another test step is running.\n" +
- runningStepDescs.map((d) => ` * ${getFullName(d)}`).join("\n"),
- );
- }
-
- function getRunningStepDescs() {
- const results = [];
- let childDesc = desc;
- while (childDesc.parent != null) {
- const state = MapPrototypeGet(testStates, childDesc.parent.id);
- for (const siblingDesc of state.children) {
- if (siblingDesc.id == childDesc.id) {
- continue;
- }
- const siblingState = MapPrototypeGet(testStates, siblingDesc.id);
- if (!siblingState.finalized) {
- ArrayPrototypePush(results, siblingDesc);
- }
- }
- childDesc = childDesc.parent;
- }
- return results;
- }
- }
- };
- }
-
- function testStepPostValidation(desc) {
- // check for any running steps
- for (const childDesc of MapPrototypeGet(testStates, desc.id).children) {
- if (MapPrototypeGet(testStates, childDesc.id).status == "pending") {
- throw new Error(
- "There were still test steps running after the current scope finished execution. Ensure all steps are awaited (ex. `await t.step(...)`).",
- );
- }
- }
-
- // check if an ancestor already completed
- let currentDesc = desc.parent;
- while (currentDesc != null) {
- if (MapPrototypeGet(testStates, currentDesc.id).finalized) {
- throw new Error(
- "Parent scope completed before test step finished execution. Ensure all steps are awaited (ex. `await t.step(...)`).",
- );
- }
- currentDesc = currentDesc.parent;
- }
- }
-
- function pledgePermissions(permissions) {
- return ops.op_pledge_test_permissions(
- serializePermissions(permissions),
- );
- }
-
- function restorePermissions(token) {
- ops.op_restore_test_permissions(token);
- }
-
- function withPermissions(fn, permissions) {
- return async function applyPermissions(...params) {
- const token = pledgePermissions(permissions);
-
- try {
- await fn(...new SafeArrayIterator(params));
- } finally {
- restorePermissions(token);
- }
- };
- }
-
- /**
- * @typedef {{
- * id: number,
- * name: string,
- * fn: TestFunction
- * origin: string,
- * location: TestLocation,
- * filteredOut: boolean,
- * ignore: boolean,
- * only: boolean.
- * sanitizeOps: boolean,
- * sanitizeResources: boolean,
- * sanitizeExit: boolean,
- * permissions: PermissionOptions,
- * }} TestDescription
- *
- * @typedef {{
- * id: number,
- * name: string,
- * fn: TestFunction
- * origin: string,
- * location: TestLocation,
- * ignore: boolean,
- * level: number,
- * parent: TestDescription | TestStepDescription,
- * rootId: number,
- * rootName: String,
- * sanitizeOps: boolean,
- * sanitizeResources: boolean,
- * sanitizeExit: boolean,
- * }} TestStepDescription
- *
- * @typedef {{
- * context: TestContext,
- * children: TestStepDescription[],
- * finalized: boolean,
- * }} TestState
- *
- * @typedef {{
- * context: TestContext,
- * children: TestStepDescription[],
- * finalized: boolean,
- * status: "pending" | "ok" | ""failed" | ignored",
- * error: unknown,
- * elapsed: number | null,
- * reportedWait: boolean,
- * reportedResult: boolean,
- * }} TestStepState
- *
- * @typedef {{
- * id: number,
- * name: string,
- * fn: BenchFunction
- * origin: string,
- * filteredOut: boolean,
- * ignore: boolean,
- * only: boolean.
- * sanitizeExit: boolean,
- * permissions: PermissionOptions,
- * }} BenchDescription
- */
-
- /** @type {TestDescription[]} */
- const testDescs = [];
- /** @type {Map<number, TestState | TestStepState>} */
- const testStates = new Map();
- /** @type {BenchDescription[]} */
- const benchDescs = [];
- let isTestSubcommand = false;
- let isBenchSubcommand = false;
-
- // Main test function provided by Deno.
- function test(
- nameOrFnOrOptions,
- optionsOrFn,
- maybeFn,
- ) {
- if (!isTestSubcommand) {
- return;
- }
-
- let testDesc;
- const defaults = {
- ignore: false,
- only: false,
- sanitizeOps: true,
- sanitizeResources: true,
- sanitizeExit: true,
- permissions: null,
- };
-
- if (typeof nameOrFnOrOptions === "string") {
- if (!nameOrFnOrOptions) {
- throw new TypeError("The test name can't be empty");
- }
- if (typeof optionsOrFn === "function") {
- testDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults };
- } else {
- if (!maybeFn || typeof maybeFn !== "function") {
- throw new TypeError("Missing test function");
- }
- if (optionsOrFn.fn != undefined) {
- throw new TypeError(
- "Unexpected 'fn' field in options, test function is already provided as the third argument.",
- );
- }
- if (optionsOrFn.name != undefined) {
- throw new TypeError(
- "Unexpected 'name' field in options, test name is already provided as the first argument.",
- );
- }
- testDesc = {
- ...defaults,
- ...optionsOrFn,
- fn: maybeFn,
- name: nameOrFnOrOptions,
- };
- }
- } else if (typeof nameOrFnOrOptions === "function") {
- if (!nameOrFnOrOptions.name) {
- throw new TypeError("The test function must have a name");
- }
- if (optionsOrFn != undefined) {
- throw new TypeError("Unexpected second argument to Deno.test()");
- }
- if (maybeFn != undefined) {
- throw new TypeError("Unexpected third argument to Deno.test()");
- }
- testDesc = {
- ...defaults,
- fn: nameOrFnOrOptions,
- name: nameOrFnOrOptions.name,
- };
- } else {
- let fn;
- let name;
- if (typeof optionsOrFn === "function") {
- fn = optionsOrFn;
- if (nameOrFnOrOptions.fn != undefined) {
- throw new TypeError(
- "Unexpected 'fn' field in options, test function is already provided as the second argument.",
- );
- }
- name = nameOrFnOrOptions.name ?? fn.name;
- } else {
- if (
- !nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function"
- ) {
- throw new TypeError(
- "Expected 'fn' field in the first argument to be a test function.",
- );
- }
- fn = nameOrFnOrOptions.fn;
- name = nameOrFnOrOptions.name ?? fn.name;
- }
- if (!name) {
- throw new TypeError("The test name can't be empty");
- }
- testDesc = { ...defaults, ...nameOrFnOrOptions, fn, name };
- }
-
- // Delete this prop in case the user passed it. It's used to detect steps.
- delete testDesc.parent;
- testDesc.fn = wrapTestFnWithSanitizers(testDesc.fn, testDesc);
- if (testDesc.permissions) {
- testDesc.fn = withPermissions(
- testDesc.fn,
- testDesc.permissions,
- );
- }
- testDesc.origin = getTestOrigin();
- const jsError = Deno.core.destructureError(new Error());
- testDesc.location = {
- fileName: jsError.frames[1].fileName,
- lineNumber: jsError.frames[1].lineNumber,
- columnNumber: jsError.frames[1].columnNumber,
- };
-
- const { id, filteredOut } = ops.op_register_test(testDesc);
- testDesc.id = id;
- testDesc.filteredOut = filteredOut;
-
- ArrayPrototypePush(testDescs, testDesc);
- MapPrototypeSet(testStates, testDesc.id, {
- context: createTestContext(testDesc),
- children: [],
- finalized: false,
- });
- }
-
- // Main bench function provided by Deno.
- function bench(
- nameOrFnOrOptions,
- optionsOrFn,
- maybeFn,
- ) {
- if (!isBenchSubcommand) {
- return;
- }
-
- let benchDesc;
- const defaults = {
- ignore: false,
- baseline: false,
- only: false,
- sanitizeExit: true,
- permissions: null,
- };
-
- if (typeof nameOrFnOrOptions === "string") {
- if (!nameOrFnOrOptions) {
- throw new TypeError("The bench name can't be empty");
- }
- if (typeof optionsOrFn === "function") {
- benchDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults };
- } else {
- if (!maybeFn || typeof maybeFn !== "function") {
- throw new TypeError("Missing bench function");
- }
- if (optionsOrFn.fn != undefined) {
- throw new TypeError(
- "Unexpected 'fn' field in options, bench function is already provided as the third argument.",
- );
- }
- if (optionsOrFn.name != undefined) {
- throw new TypeError(
- "Unexpected 'name' field in options, bench name is already provided as the first argument.",
- );
- }
- benchDesc = {
- ...defaults,
- ...optionsOrFn,
- fn: maybeFn,
- name: nameOrFnOrOptions,
- };
- }
- } else if (typeof nameOrFnOrOptions === "function") {
- if (!nameOrFnOrOptions.name) {
- throw new TypeError("The bench function must have a name");
- }
- if (optionsOrFn != undefined) {
- throw new TypeError("Unexpected second argument to Deno.bench()");
- }
- if (maybeFn != undefined) {
- throw new TypeError("Unexpected third argument to Deno.bench()");
- }
- benchDesc = {
- ...defaults,
- fn: nameOrFnOrOptions,
- name: nameOrFnOrOptions.name,
- };
- } else {
- let fn;
- let name;
- if (typeof optionsOrFn === "function") {
- fn = optionsOrFn;
- if (nameOrFnOrOptions.fn != undefined) {
- throw new TypeError(
- "Unexpected 'fn' field in options, bench function is already provided as the second argument.",
- );
- }
- name = nameOrFnOrOptions.name ?? fn.name;
- } else {
- if (
- !nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function"
- ) {
- throw new TypeError(
- "Expected 'fn' field in the first argument to be a bench function.",
- );
- }
- fn = nameOrFnOrOptions.fn;
- name = nameOrFnOrOptions.name ?? fn.name;
- }
- if (!name) {
- throw new TypeError("The bench name can't be empty");
- }
- benchDesc = { ...defaults, ...nameOrFnOrOptions, fn, name };
- }
-
- benchDesc.origin = getBenchOrigin();
- const AsyncFunction = (async () => {}).constructor;
- benchDesc.async = AsyncFunction === benchDesc.fn.constructor;
-
- const { id, filteredOut } = ops.op_register_bench(benchDesc);
- benchDesc.id = id;
- benchDesc.filteredOut = filteredOut;
-
- ArrayPrototypePush(benchDescs, benchDesc);
- }
-
- async function runTest(desc) {
- if (desc.ignore) {
- return "ignored";
- }
-
- try {
- await desc.fn(desc);
- const failCount = failedChildStepsCount(desc);
- return failCount === 0 ? "ok" : {
- "failed": core.destructureError(
- new Error(
- `${failCount} test step${failCount === 1 ? "" : "s"} failed.`,
- ),
- ),
- };
- } catch (error) {
- return {
- "failed": core.destructureError(error),
- };
- } finally {
- const state = MapPrototypeGet(testStates, desc.id);
- state.finalized = true;
- // ensure the children report their result
- for (const childDesc of state.children) {
- stepReportResult(childDesc);
- }
- }
- }
-
- function compareMeasurements(a, b) {
- if (a > b) return 1;
- if (a < b) return -1;
-
- return 0;
- }
-
- function benchStats(n, highPrecision, avg, min, max, all) {
- return {
- n,
- min,
- max,
- p75: all[MathCeil(n * (75 / 100)) - 1],
- p99: all[MathCeil(n * (99 / 100)) - 1],
- p995: all[MathCeil(n * (99.5 / 100)) - 1],
- p999: all[MathCeil(n * (99.9 / 100)) - 1],
- avg: !highPrecision ? (avg / n) : MathCeil(avg / n),
- };
- }
-
- async function benchMeasure(timeBudget, desc) {
- const fn = desc.fn;
- let n = 0;
- let avg = 0;
- let wavg = 0;
- const all = [];
- let min = Infinity;
- let max = -Infinity;
- const lowPrecisionThresholdInNs = 1e4;
-
- // warmup step
- let c = 0;
- let iterations = 20;
- let budget = 10 * 1e6;
-
- if (!desc.async) {
- while (budget > 0 || iterations-- > 0) {
- const t1 = benchNow();
-
- fn();
- const iterationTime = benchNow() - t1;
-
- c++;
- wavg += iterationTime;
- budget -= iterationTime;
- }
- } else {
- while (budget > 0 || iterations-- > 0) {
- const t1 = benchNow();
-
- await fn();
- const iterationTime = benchNow() - t1;
-
- c++;
- wavg += iterationTime;
- budget -= iterationTime;
- }
- }
-
- wavg /= c;
-
- // measure step
- if (wavg > lowPrecisionThresholdInNs) {
- let iterations = 10;
- let budget = timeBudget * 1e6;
-
- if (!desc.async) {
- while (budget > 0 || iterations-- > 0) {
- const t1 = benchNow();
-
- fn();
- const iterationTime = benchNow() - t1;
-
- n++;
- avg += iterationTime;
- budget -= iterationTime;
- ArrayPrototypePush(all, iterationTime);
- if (iterationTime < min) min = iterationTime;
- if (iterationTime > max) max = iterationTime;
- }
- } else {
- while (budget > 0 || iterations-- > 0) {
- const t1 = benchNow();
-
- await fn();
- const iterationTime = benchNow() - t1;
-
- n++;
- avg += iterationTime;
- budget -= iterationTime;
- ArrayPrototypePush(all, iterationTime);
- if (iterationTime < min) min = iterationTime;
- if (iterationTime > max) max = iterationTime;
- }
- }
- } else {
- let iterations = 10;
- let budget = timeBudget * 1e6;
-
- if (!desc.async) {
- while (budget > 0 || iterations-- > 0) {
- const t1 = benchNow();
- for (let c = 0; c < lowPrecisionThresholdInNs; c++) fn();
- const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs;
-
- n++;
- avg += iterationTime;
- ArrayPrototypePush(all, iterationTime);
- if (iterationTime < min) min = iterationTime;
- if (iterationTime > max) max = iterationTime;
- budget -= iterationTime * lowPrecisionThresholdInNs;
- }
- } else {
- while (budget > 0 || iterations-- > 0) {
- const t1 = benchNow();
- for (let c = 0; c < lowPrecisionThresholdInNs; c++) await fn();
- const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs;
-
- n++;
- avg += iterationTime;
- ArrayPrototypePush(all, iterationTime);
- if (iterationTime < min) min = iterationTime;
- if (iterationTime > max) max = iterationTime;
- budget -= iterationTime * lowPrecisionThresholdInNs;
- }
- }
- }
-
- all.sort(compareMeasurements);
- return benchStats(n, wavg > lowPrecisionThresholdInNs, avg, min, max, all);
- }
-
- async function runBench(desc) {
- let token = null;
-
- try {
- if (desc.permissions) {
- token = pledgePermissions(desc.permissions);
- }
-
- if (desc.sanitizeExit) {
- setExitHandler((exitCode) => {
- assert(
- false,
- `Bench attempted to exit with exit code: ${exitCode}`,
- );
- });
- }
-
- const benchTimeInMs = 500;
- const stats = await benchMeasure(benchTimeInMs, desc);
-
- return { ok: stats };
- } catch (error) {
- return { failed: core.destructureError(error) };
- } finally {
- if (bench.sanitizeExit) setExitHandler(null);
- if (token !== null) restorePermissions(token);
- }
- }
-
- let origin = null;
-
- function getTestOrigin() {
- if (origin == null) {
- origin = ops.op_get_test_origin();
- }
- return origin;
- }
-
- function getBenchOrigin() {
- if (origin == null) {
- origin = ops.op_get_bench_origin();
- }
- return origin;
- }
-
- function benchNow() {
- return ops.op_bench_now();
- }
-
- function enableTest() {
- isTestSubcommand = true;
- }
-
- function enableBench() {
- isBenchSubcommand = true;
- }
-
- async function runTests({
- shuffle = null,
- } = {}) {
- core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask);
-
- const origin = getTestOrigin();
- const only = ArrayPrototypeFilter(testDescs, (test) => test.only);
- const filtered = ArrayPrototypeFilter(
- only.length > 0 ? only : testDescs,
- (desc) => !desc.filteredOut,
- );
-
- ops.op_dispatch_test_event({
- plan: {
- origin,
- total: filtered.length,
- filteredOut: testDescs.length - filtered.length,
- usedOnly: only.length > 0,
- },
- });
-
- if (shuffle !== null) {
- // http://en.wikipedia.org/wiki/Linear_congruential_generator
- // Use BigInt for everything because the random seed is u64.
- const nextInt = (function (state) {
- const m = 0x80000000n;
- const a = 1103515245n;
- const c = 12345n;
-
- return function (max) {
- return state = ((a * state + c) % m) % BigInt(max);
- };
- }(BigInt(shuffle)));
-
- for (let i = filtered.length - 1; i > 0; i--) {
- const j = nextInt(i);
- [filtered[i], filtered[j]] = [filtered[j], filtered[i]];
- }
- }
-
- for (const desc of filtered) {
- ops.op_dispatch_test_event({ wait: desc.id });
- const earlier = DateNow();
- const result = await runTest(desc);
- const elapsed = DateNow() - earlier;
- ops.op_dispatch_test_event({
- result: [desc.id, result, elapsed],
- });
- }
- }
-
- async function runBenchmarks() {
- core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask);
-
- const origin = getBenchOrigin();
- const originalConsole = globalThis.console;
-
- globalThis.console = new Console((s) => {
- ops.op_dispatch_bench_event({ output: s });
- });
-
- const only = ArrayPrototypeFilter(benchDescs, (bench) => bench.only);
- const filtered = ArrayPrototypeFilter(
- only.length > 0 ? only : benchDescs,
- (desc) => !desc.filteredOut && !desc.ignore,
- );
-
- let groups = new Set();
- // make sure ungrouped benchmarks are placed above grouped
- groups.add(undefined);
-
- for (const desc of filtered) {
- desc.group ||= undefined;
- groups.add(desc.group);
- }
-
- groups = ArrayFrom(groups);
- ArrayPrototypeSort(
- filtered,
- (a, b) => groups.indexOf(a.group) - groups.indexOf(b.group),
- );
-
- ops.op_dispatch_bench_event({
- plan: {
- origin,
- total: filtered.length,
- usedOnly: only.length > 0,
- names: ArrayPrototypeMap(filtered, (desc) => desc.name),
- },
- });
-
- for (const desc of filtered) {
- desc.baseline = !!desc.baseline;
- ops.op_dispatch_bench_event({ wait: desc.id });
- ops.op_dispatch_bench_event({
- result: [desc.id, await runBench(desc)],
- });
- }
-
- globalThis.console = originalConsole;
- }
-
- function getFullName(desc) {
- if ("parent" in desc) {
- return `${desc.parent.name} > ${desc.name}`;
- }
- return desc.name;
- }
-
- function usesSanitizer(desc) {
- return desc.sanitizeResources || desc.sanitizeOps || desc.sanitizeExit;
- }
-
- function canStreamReporting(desc) {
- let currentDesc = desc;
- while (currentDesc != null) {
- if (!usesSanitizer(currentDesc)) {
- return false;
- }
- currentDesc = currentDesc.parent;
- }
- for (const childDesc of MapPrototypeGet(testStates, desc.id).children) {
- const state = MapPrototypeGet(testStates, childDesc.id);
- if (!usesSanitizer(childDesc) && !state.finalized) {
- return false;
- }
- }
- return true;
- }
-
- function stepReportWait(desc) {
- const state = MapPrototypeGet(testStates, desc.id);
- if (state.reportedWait) {
- return;
- }
- ops.op_dispatch_test_event({ stepWait: desc.id });
- state.reportedWait = true;
- }
-
- function stepReportResult(desc) {
- const state = MapPrototypeGet(testStates, desc.id);
- if (state.reportedResult) {
- return;
- }
- stepReportWait(desc);
- for (const childDesc of state.children) {
- stepReportResult(childDesc);
- }
- let result;
- if (state.status == "pending" || state.status == "failed") {
- result = {
- [state.status]: state.error && core.destructureError(state.error),
- };
- } else {
- result = state.status;
- }
- ops.op_dispatch_test_event({
- stepResult: [desc.id, result, state.elapsed],
- });
- state.reportedResult = true;
- }
-
- function failedChildStepsCount(desc) {
- return ArrayPrototypeFilter(
- MapPrototypeGet(testStates, desc.id).children,
- (d) => MapPrototypeGet(testStates, d.id).status === "failed",
- ).length;
- }
-
- /** If a test validation error already occurred then don't bother checking
- * the sanitizers as that will create extra noise.
- */
- function shouldSkipSanitizers(desc) {
- try {
- testStepPostValidation(desc);
- return false;
- } catch {
- return true;
- }
- }
-
- /** @param desc {TestDescription | TestStepDescription} */
- function createTestContext(desc) {
- let parent;
- let level;
- let rootId;
- let rootName;
- if ("parent" in desc) {
- parent = MapPrototypeGet(testStates, desc.parent.id).context;
- level = desc.level;
- rootId = desc.rootId;
- rootName = desc.rootName;
- } else {
- parent = undefined;
- level = 0;
- rootId = desc.id;
- rootName = desc.name;
- }
- return {
- [SymbolToStringTag]: "TestContext",
- /**
- * The current test name.
- */
- name: desc.name,
- /**
- * Parent test context.
- */
- parent,
- /**
- * File Uri of the test code.
- */
- origin: desc.origin,
- /**
- * @param nameOrTestDefinition {string | TestStepDefinition}
- * @param fn {(t: TestContext) => void | Promise<void>}
- */
- async step(nameOrTestDefinition, fn) {
- if (MapPrototypeGet(testStates, desc.id).finalized) {
- throw new Error(
- "Cannot run test step after parent scope has finished execution. " +
- "Ensure any `.step(...)` calls are executed before their parent scope completes execution.",
- );
- }
-
- let stepDesc;
- if (typeof nameOrTestDefinition === "string") {
- if (!(ObjectPrototypeIsPrototypeOf(FunctionPrototype, fn))) {
- throw new TypeError("Expected function for second argument.");
- }
- stepDesc = {
- name: nameOrTestDefinition,
- fn,
- };
- } else if (typeof nameOrTestDefinition === "object") {
- stepDesc = nameOrTestDefinition;
- } else {
- throw new TypeError(
- "Expected a test definition or name and function.",
- );
- }
- stepDesc.ignore ??= false;
- stepDesc.sanitizeOps ??= desc.sanitizeOps;
- stepDesc.sanitizeResources ??= desc.sanitizeResources;
- stepDesc.sanitizeExit ??= desc.sanitizeExit;
- stepDesc.origin = getTestOrigin();
- const jsError = Deno.core.destructureError(new Error());
- stepDesc.location = {
- fileName: jsError.frames[1].fileName,
- lineNumber: jsError.frames[1].lineNumber,
- columnNumber: jsError.frames[1].columnNumber,
- };
- stepDesc.level = level + 1;
- stepDesc.parent = desc;
- stepDesc.rootId = rootId;
- stepDesc.rootName = rootName;
- const { id } = ops.op_register_test_step(stepDesc);
- stepDesc.id = id;
- const state = {
- context: createTestContext(stepDesc),
- children: [],
- finalized: false,
- status: "pending",
- error: null,
- elapsed: null,
- reportedWait: false,
- reportedResult: false,
- };
- MapPrototypeSet(testStates, stepDesc.id, state);
- ArrayPrototypePush(
- MapPrototypeGet(testStates, stepDesc.parent.id).children,
- stepDesc,
- );
-
- try {
- if (stepDesc.ignore) {
- state.status = "ignored";
- state.finalized = true;
- if (canStreamReporting(stepDesc)) {
- stepReportResult(stepDesc);
- }
- return false;
- }
-
- const testFn = wrapTestFnWithSanitizers(stepDesc.fn, stepDesc);
- const start = DateNow();
-
- try {
- await testFn(stepDesc);
-
- if (failedChildStepsCount(stepDesc) > 0) {
- state.status = "failed";
- } else {
- state.status = "ok";
- }
- } catch (error) {
- state.error = error;
- state.status = "failed";
- }
-
- state.elapsed = DateNow() - start;
-
- if (MapPrototypeGet(testStates, stepDesc.parent.id).finalized) {
- // always point this test out as one that was still running
- // if the parent step finalized
- state.status = "pending";
- }
-
- state.finalized = true;
-
- if (state.reportedWait && canStreamReporting(stepDesc)) {
- stepReportResult(stepDesc);
- }
-
- return state.status === "ok";
- } finally {
- if (canStreamReporting(stepDesc.parent)) {
- const parentState = MapPrototypeGet(testStates, stepDesc.parent.id);
- // flush any buffered steps
- for (const childDesc of parentState.children) {
- stepReportResult(childDesc);
- }
- }
- }
- },
- };
- }
-
- /**
- * @template T {Function}
- * @param testFn {T}
- * @param opts {{
- * sanitizeOps: boolean,
- * sanitizeResources: boolean,
- * sanitizeExit: boolean,
- * }}
- * @returns {T}
- */
- function wrapTestFnWithSanitizers(testFn, opts) {
- testFn = assertTestStepScopes(testFn);
-
- if (opts.sanitizeOps) {
- testFn = assertOps(testFn);
- }
- if (opts.sanitizeResources) {
- testFn = assertResources(testFn);
- }
- if (opts.sanitizeExit) {
- testFn = assertExit(testFn, true);
- }
- return testFn;
- }
-
- window.__bootstrap.testing = {
- bench,
- enableBench,
- enableTest,
- runBenchmarks,
- runTests,
- test,
- };
-})(this);