diff options
Diffstat (limited to 'cli/js/40_testing.js')
-rw-r--r-- | cli/js/40_testing.js | 1392 |
1 files changed, 0 insertions, 1392 deletions
diff --git a/cli/js/40_testing.js b/cli/js/40_testing.js deleted file mode 100644 index 23a8da71b..000000000 --- a/cli/js/40_testing.js +++ /dev/null @@ -1,1392 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// deno-lint-ignore-file - -import { core, primordials } from "ext:core/mod.js"; -const ops = core.ops; -const { - ArrayPrototypeFilter, - ArrayPrototypeJoin, - ArrayPrototypePush, - ArrayPrototypeShift, - DateNow, - Error, - Map, - MapPrototypeGet, - MapPrototypeHas, - MapPrototypeSet, - MathCeil, - ObjectKeys, - Promise, - SafeArrayIterator, - Set, - StringPrototypeReplaceAll, - SymbolToStringTag, - TypeError, -} = primordials; - -import { setExitHandler } from "ext:runtime/30_os.js"; -import { Console } from "ext:deno_console/01_console.js"; -import { serializePermissions } from "ext:runtime/10_permissions.js"; -import { setTimeout } from "ext:deno_web/02_timers.js"; - -const opSanitizerDelayResolveQueue = []; -let hasSetOpSanitizerDelayMacrotask = false; - -// 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 -// all ready async ops resolve, and the timer macrotask. Using just a macrotask -// callback without delaying is sufficient, because when the macrotask callback -// runs after async op dispatch, we know that all async ops that can currently -// return `Poll::Ready` have done so, and have been dispatched to JS. -// -// Worker ops are an exception to this, because there is no way for the user to -// await shutdown of the worker from the thread calling `worker.terminate()`. -// Because of this, we give extra leeway for worker ops to complete, by waiting -// for a whole millisecond if there are pending worker ops. -function opSanitizerDelay(hasPendingWorkerOps) { - if (!hasSetOpSanitizerDelayMacrotask) { - core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask); - hasSetOpSanitizerDelayMacrotask = true; - } - const p = new Promise((resolve) => { - // Schedule an async op to complete immediately to ensure the macrotask is - // run. We rely on the fact that enqueueing the resolver callback during the - // timeout callback will mean that the resolver gets called in the same - // event loop tick as the timeout callback. - setTimeout(() => { - ArrayPrototypePush(opSanitizerDelayResolveQueue, resolve); - }, hasPendingWorkerOps ? 1 : 0); - }); - return p; -} - -function handleOpSanitizerDelayMacrotask() { - const resolve = ArrayPrototypeShift(opSanitizerDelayResolveQueue); - if (resolve) { - resolve(); - return opSanitizerDelayResolveQueue.length === 0; - } - return undefined; // we performed no work, so can skip microtasks checkpoint -} - -// 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 `file.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 `file.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_host_recv_message": ["receive a message from a web worker", "terminating a `Worker`"], - "op_host_recv_ctrl": ["receive a message from a web worker", "terminating a `Worker`"], - "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_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_text": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"], - "op_ws_send_binary": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"], - "op_ws_send_binary_ab": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"], - "op_ws_send_ping": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"], - "op_ws_send_pong": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"], - }; - -let opIdHostRecvMessage = -1; -let opIdHostRecvCtrl = -1; -let opNames = null; - -function populateOpNames() { - opNames = ops.op_op_names(); - opIdHostRecvMessage = opNames.indexOf("op_host_recv_message"); - opIdHostRecvCtrl = opNames.indexOf("op_host_recv_ctrl"); -} - -// 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) { - if (opNames === null) populateOpNames(); - const res = ops.op_test_op_sanitizer_collect( - desc.id, - false, - opIdHostRecvMessage, - opIdHostRecvCtrl, - ); - if (res !== 0) { - await opSanitizerDelay(res === 2); - ops.op_test_op_sanitizer_collect( - desc.id, - true, - opIdHostRecvMessage, - opIdHostRecvCtrl, - ); - } - const preTraces = new Map(core.opCallTraces); - let postTraces; - let report = null; - - try { - const innerResult = await fn(desc); - if (innerResult) return innerResult; - } finally { - let res = ops.op_test_op_sanitizer_finish( - desc.id, - false, - opIdHostRecvMessage, - opIdHostRecvCtrl, - ); - if (res === 1 || res === 2) { - await opSanitizerDelay(res === 2); - res = ops.op_test_op_sanitizer_finish( - desc.id, - true, - opIdHostRecvMessage, - opIdHostRecvCtrl, - ); - } - postTraces = new Map(core.opCallTraces); - if (res === 3) { - report = ops.op_test_op_sanitizer_report(desc.id); - } - } - - if (report === null) return null; - - const details = []; - for (const opReport of report) { - const opName = opNames[opReport.id]; - const diff = opReport.diff; - - if (diff > 0) { - const [name, hint] = OP_DETAILS[opName] || [opName, null]; - const count = diff; - 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: traceOpName, stack }] of postTraces) { - if (traceOpName !== opName) 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 (diff < 0) { - const [name, hint] = OP_DETAILS[opName] || [opName, null]; - const count = -diff; - let message = `${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.`; - if (hint) { - message += ` This is often caused by not ${hint}.`; - } - const traces = []; - for (const [id, { opName: traceOpName, stack }] of preTraces) { - if (opName !== traceOpName) continue; - if (MapPrototypeHas(postTraces, 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 { - throw new Error("unreachable"); - } - } - - return { - failed: { leakedOps: [details, core.isOpCallTracingEnabled()] }, - }; - }; -} - -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 "fetchResponse": - 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 "fetchResponse": - 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()` or `await child.stdout.cancel()`."; - case "childStderr": - return "Close the child process stderr by calling `proc.stderr.close()` or `await child.stderr.cancel()`."; - 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(); - const innerResult = await fn(desc); - if (innerResult) return innerResult; - 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); - } - } - if (details.length == 0) { - return null; - } - return { failed: { leakedResources: details } }; - }; -} - -// 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) => { - throw new Error( - `${ - isTest ? "Test case" : "Bench" - } attempted to exit with exit code: ${exitCode}`, - ); - }); - - try { - const innerResult = await fn(...new SafeArrayIterator(params)); - if (innerResult) return innerResult; - } finally { - setExitHandler(null); - } - }; -} - -function wrapOuter(fn, desc) { - return async function outerWrapped() { - try { - if (desc.ignore) { - return "ignored"; - } - return await fn(desc) ?? "ok"; - } catch (error) { - return { failed: { jsError: core.destructureError(error) } }; - } finally { - const state = MapPrototypeGet(testStates, desc.id); - for (const childDesc of state.children) { - stepReportResult(childDesc, { failed: "incomplete" }, 0); - } - state.completed = true; - } - }; -} - -function wrapInner(fn) { - /** @param desc {TestDescription | TestStepDescription} */ - return async function innerWrapped(desc) { - 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.completed) { - ArrayPrototypePush(results, siblingDesc); - } - } - childDesc = childDesc.parent; - } - return results; - } - const runningStepDescs = getRunningStepDescs(); - const runningStepDescsWithSanitizers = ArrayPrototypeFilter( - runningStepDescs, - (d) => usesSanitizer(d), - ); - - if (runningStepDescsWithSanitizers.length > 0) { - return { - failed: { - overlapsWithSanitizers: runningStepDescsWithSanitizers.map( - getFullName, - ), - }, - }; - } - - if (usesSanitizer(desc) && runningStepDescs.length > 0) { - return { - failed: { - hasSanitizersAndOverlaps: runningStepDescs.map(getFullName), - }, - }; - } - await fn(MapPrototypeGet(testStates, desc.id).context); - let failedSteps = 0; - for (const childDesc of MapPrototypeGet(testStates, desc.id).children) { - const state = MapPrototypeGet(testStates, childDesc.id); - if (!state.completed) { - return { failed: "incompleteSteps" }; - } - if (state.failed) { - failedSteps++; - } - } - return failedSteps == 0 ? null : { failed: { failedSteps } }; - }; -} - -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 { - return await fn(...new SafeArrayIterator(params)); - } finally { - restorePermissions(token); - } - }; -} - -const ESCAPE_ASCII_CHARS = [ - ["\b", "\\b"], - ["\f", "\\f"], - ["\t", "\\t"], - ["\n", "\\n"], - ["\r", "\\r"], - ["\v", "\\v"], -]; - -/** - * @param {string} name - * @returns {string} - */ -function escapeName(name) { - // Check if we need to escape a character - for (let i = 0; i < name.length; i++) { - const ch = name.charCodeAt(i); - if (ch <= 13 && ch >= 8) { - // Slow path: We do need to escape it - for (const [escape, replaceWith] of ESCAPE_ASCII_CHARS) { - name = StringPrototypeReplaceAll(name, escape, replaceWith); - } - return name; - } - } - - // We didn't need to escape anything, return original string - return name; -} - -/** - * @typedef {{ - * id: number, - * name: string, - * fn: TestFunction - * origin: string, - * location: TestLocation, - * 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[], - * completed: boolean, - * }} TestState - * - * @typedef {{ - * context: TestContext, - * children: TestStepDescription[], - * completed: boolean, - * failed: boolean, - * }} TestStepState - * - * @typedef {{ - * id: number, - * name: string, - * fn: BenchFunction - * origin: string, - * ignore: boolean, - * only: boolean. - * sanitizeExit: boolean, - * permissions: PermissionOptions, - * }} BenchDescription - */ - -/** @type {Map<number, TestState | TestStepState>} */ -const testStates = new Map(); -/** @type {number | null} */ -let currentBenchId = null; -// These local variables are used to track time measurements at -// `BenchContext::{start,end}` calls. They are global instead of using a state -// map to minimise the overhead of assigning them. -/** @type {number | null} */ -let currentBenchUserExplicitStart = null; -/** @type {number | null} */ -let currentBenchUserExplicitEnd = null; - -const registerTestIdRetBuf = new Uint32Array(1); -const registerTestIdRetBufU8 = new Uint8Array(registerTestIdRetBuf.buffer); - -function testInner( - nameOrFnOrOptions, - optionsOrFn, - maybeFn, - overrides = {}, -) { - if (typeof ops.op_register_test != "function") { - 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 }; - } - - testDesc = { ...testDesc, ...overrides }; - - // Delete this prop in case the user passed it. It's used to detect steps. - delete testDesc.parent; - - testDesc.location = core.currentUserCallSite(); - testDesc.fn = wrapTest(testDesc); - testDesc.name = escapeName(testDesc.name); - - const origin = ops.op_register_test( - testDesc.fn, - testDesc.name, - testDesc.ignore, - testDesc.only, - testDesc.location.fileName, - testDesc.location.lineNumber, - testDesc.location.columnNumber, - registerTestIdRetBufU8, - ); - testDesc.id = registerTestIdRetBuf[0]; - testDesc.origin = origin; - MapPrototypeSet(testStates, testDesc.id, { - context: createTestContext(testDesc), - children: [], - completed: false, - }); -} - -// Main test function provided by Deno. -function test( - nameOrFnOrOptions, - optionsOrFn, - maybeFn, -) { - return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn); -} - -test.ignore = function (nameOrFnOrOptions, optionsOrFn, maybeFn) { - return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn, { ignore: true }); -}; - -test.only = function ( - nameOrFnOrOptions, - optionsOrFn, - maybeFn, -) { - return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn, { only: true }); -}; - -let registeredWarmupBench = false; - -// Main bench function provided by Deno. -function bench( - nameOrFnOrOptions, - optionsOrFn, - maybeFn, -) { - if (typeof ops.op_register_bench != "function") { - return; - } - - if (!registeredWarmupBench) { - registeredWarmupBench = true; - const warmupBenchDesc = { - name: "<warmup>", - fn: function warmup() {}, - async: false, - ignore: false, - baseline: false, - only: false, - sanitizeExit: true, - permissions: null, - warmup: true, - }; - warmupBenchDesc.fn = wrapBenchmark(warmupBenchDesc); - const { id, origin } = ops.op_register_bench(warmupBenchDesc); - warmupBenchDesc.id = id; - warmupBenchDesc.origin = origin; - } - - 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 }; - } - - const AsyncFunction = (async () => {}).constructor; - benchDesc.async = AsyncFunction === benchDesc.fn.constructor; - benchDesc.fn = wrapBenchmark(benchDesc); - benchDesc.warmup = false; - benchDesc.name = escapeName(benchDesc.name); - - const { id, origin } = ops.op_register_bench(benchDesc); - benchDesc.id = id; - benchDesc.origin = origin; -} - -function compareMeasurements(a, b) { - if (a > b) return 1; - if (a < b) return -1; - - return 0; -} - -function benchStats( - n, - highPrecision, - usedExplicitTimers, - 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), - highPrecision, - usedExplicitTimers, - }; -} - -async function benchMeasure(timeBudget, fn, async, context) { - let n = 0; - let avg = 0; - let wavg = 0; - let usedExplicitTimers = false; - const all = []; - let min = Infinity; - let max = -Infinity; - const lowPrecisionThresholdInNs = 1e4; - - // warmup step - let c = 0; - let iterations = 20; - let budget = 10 * 1e6; - - if (!async) { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - fn(context); - const t2 = benchNow(); - const totalTime = t2 - t1; - if (currentBenchUserExplicitStart !== null) { - currentBenchUserExplicitStart = null; - usedExplicitTimers = true; - } - if (currentBenchUserExplicitEnd !== null) { - currentBenchUserExplicitEnd = null; - usedExplicitTimers = true; - } - - c++; - wavg += totalTime; - budget -= totalTime; - } - } else { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - await fn(context); - const t2 = benchNow(); - const totalTime = t2 - t1; - if (currentBenchUserExplicitStart !== null) { - currentBenchUserExplicitStart = null; - usedExplicitTimers = true; - } - if (currentBenchUserExplicitEnd !== null) { - currentBenchUserExplicitEnd = null; - usedExplicitTimers = true; - } - - c++; - wavg += totalTime; - budget -= totalTime; - } - } - - wavg /= c; - - // measure step - if (wavg > lowPrecisionThresholdInNs) { - let iterations = 10; - let budget = timeBudget * 1e6; - - if (!async) { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - fn(context); - const t2 = benchNow(); - const totalTime = t2 - t1; - let measuredTime = totalTime; - if (currentBenchUserExplicitStart !== null) { - measuredTime -= currentBenchUserExplicitStart - t1; - currentBenchUserExplicitStart = null; - } - if (currentBenchUserExplicitEnd !== null) { - measuredTime -= t2 - currentBenchUserExplicitEnd; - currentBenchUserExplicitEnd = null; - } - - n++; - avg += measuredTime; - budget -= totalTime; - ArrayPrototypePush(all, measuredTime); - if (measuredTime < min) min = measuredTime; - if (measuredTime > max) max = measuredTime; - } - } else { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - await fn(context); - const t2 = benchNow(); - const totalTime = t2 - t1; - let measuredTime = totalTime; - if (currentBenchUserExplicitStart !== null) { - measuredTime -= currentBenchUserExplicitStart - t1; - currentBenchUserExplicitStart = null; - } - if (currentBenchUserExplicitEnd !== null) { - measuredTime -= t2 - currentBenchUserExplicitEnd; - currentBenchUserExplicitEnd = null; - } - - n++; - avg += measuredTime; - budget -= totalTime; - ArrayPrototypePush(all, measuredTime); - if (measuredTime < min) min = measuredTime; - if (measuredTime > max) max = measuredTime; - } - } - } else { - context.start = function start() {}; - context.end = function end() {}; - let iterations = 10; - let budget = timeBudget * 1e6; - - if (!async) { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - for (let c = 0; c < lowPrecisionThresholdInNs; c++) { - fn(context); - } - 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(context); - currentBenchUserExplicitStart = null; - currentBenchUserExplicitEnd = null; - } - 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, - usedExplicitTimers, - avg, - min, - max, - all, - ); -} - -/** @param desc {BenchDescription} */ -function createBenchContext(desc) { - return { - [SymbolToStringTag]: "BenchContext", - name: desc.name, - origin: desc.origin, - start() { - if (currentBenchId !== desc.id) { - throw new TypeError( - "The benchmark which this context belongs to is not being executed.", - ); - } - if (currentBenchUserExplicitStart != null) { - throw new TypeError( - "BenchContext::start() has already been invoked.", - ); - } - currentBenchUserExplicitStart = benchNow(); - }, - end() { - const end = benchNow(); - if (currentBenchId !== desc.id) { - throw new TypeError( - "The benchmark which this context belongs to is not being executed.", - ); - } - if (currentBenchUserExplicitEnd != null) { - throw new TypeError("BenchContext::end() has already been invoked."); - } - currentBenchUserExplicitEnd = end; - }, - }; -} - -/** Wrap a user benchmark function in one which returns a structured result. */ -function wrapBenchmark(desc) { - const fn = desc.fn; - return async function outerWrapped() { - let token = null; - const originalConsole = globalThis.console; - currentBenchId = desc.id; - - try { - globalThis.console = new Console((s) => { - ops.op_dispatch_bench_event({ output: s }); - }); - - if (desc.permissions) { - token = pledgePermissions(desc.permissions); - } - - if (desc.sanitizeExit) { - setExitHandler((exitCode) => { - throw new Error( - `Bench attempted to exit with exit code: ${exitCode}`, - ); - }); - } - - const benchTimeInMs = 500; - const context = createBenchContext(desc); - const stats = await benchMeasure( - benchTimeInMs, - fn, - desc.async, - context, - ); - - return { ok: stats }; - } catch (error) { - return { failed: core.destructureError(error) }; - } finally { - globalThis.console = originalConsole; - currentBenchId = null; - currentBenchUserExplicitStart = null; - currentBenchUserExplicitEnd = null; - if (bench.sanitizeExit) setExitHandler(null); - if (token !== null) restorePermissions(token); - } - }; -} - -function benchNow() { - return ops.op_bench_now(); -} - -function getFullName(desc) { - if ("parent" in desc) { - return `${getFullName(desc.parent)} ... ${desc.name}`; - } - return desc.name; -} - -function usesSanitizer(desc) { - return desc.sanitizeResources || desc.sanitizeOps || desc.sanitizeExit; -} - -function stepReportResult(desc, result, elapsed) { - const state = MapPrototypeGet(testStates, desc.id); - for (const childDesc of state.children) { - stepReportResult(childDesc, { failed: "incomplete" }, 0); - } - if (result === "ok") { - ops.op_test_event_step_result_ok(desc.id, elapsed); - } else if (result === "ignored") { - ops.op_test_event_step_result_ignored(desc.id, elapsed); - } else { - ops.op_test_event_step_result_failed(desc.id, result.failed, elapsed); - } -} - -/** @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 nameOrFnOrOptions {string | TestStepDefinition | ((t: TestContext) => void | Promise<void>)} - * @param maybeFn {((t: TestContext) => void | Promise<void>) | undefined} - */ - async step(nameOrFnOrOptions, maybeFn) { - if (MapPrototypeGet(testStates, desc.id).completed) { - 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 nameOrFnOrOptions === "string") { - if (typeof maybeFn !== "function") { - throw new TypeError("Expected function for second argument."); - } - stepDesc = { - name: nameOrFnOrOptions, - fn: maybeFn, - }; - } else if (typeof nameOrFnOrOptions === "function") { - if (!nameOrFnOrOptions.name) { - throw new TypeError("The step function must have a name."); - } - if (maybeFn != undefined) { - throw new TypeError( - "Unexpected second argument to TestContext.step()", - ); - } - stepDesc = { - name: nameOrFnOrOptions.name, - fn: nameOrFnOrOptions, - }; - } else if (typeof nameOrFnOrOptions === "object") { - stepDesc = nameOrFnOrOptions; - } 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.location = core.currentUserCallSite(); - stepDesc.level = level + 1; - stepDesc.parent = desc; - stepDesc.rootId = rootId; - stepDesc.name = escapeName(stepDesc.name); - stepDesc.rootName = escapeName(rootName); - stepDesc.fn = wrapTest(stepDesc); - const id = ops.op_register_test_step( - stepDesc.name, - stepDesc.location.fileName, - stepDesc.location.lineNumber, - stepDesc.location.columnNumber, - stepDesc.level, - stepDesc.parent.id, - stepDesc.rootId, - stepDesc.rootName, - ); - stepDesc.id = id; - stepDesc.origin = desc.origin; - const state = { - context: createTestContext(stepDesc), - children: [], - failed: false, - completed: false, - }; - MapPrototypeSet(testStates, stepDesc.id, state); - ArrayPrototypePush( - MapPrototypeGet(testStates, stepDesc.parent.id).children, - stepDesc, - ); - - ops.op_test_event_step_wait(stepDesc.id); - const earlier = DateNow(); - const result = await stepDesc.fn(stepDesc); - const elapsed = DateNow() - earlier; - state.failed = !!result.failed; - stepReportResult(stepDesc, result, elapsed); - return result == "ok"; - }, - }; -} - -/** - * Wrap a user test function in one which returns a structured result. - * @template T {Function} - * @param testFn {T} - * @param desc {TestDescription | TestStepDescription} - * @returns {T} - */ -function wrapTest(desc) { - let testFn = wrapInner(desc.fn); - if (desc.sanitizeOps) { - testFn = assertOps(testFn); - } - if (desc.sanitizeResources) { - testFn = assertResources(testFn); - } - if (desc.sanitizeExit) { - testFn = assertExit(testFn, true); - } - if (!("parent" in desc) && desc.permissions) { - testFn = withPermissions(testFn, desc.permissions); - } - return wrapOuter(testFn, desc); -} - -globalThis.Deno.bench = bench; -globalThis.Deno.test = test; |