deno.land / x / deno@v1.28.2 / cli / js / 40_testing.js
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413// 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.internals = { ...window.__bootstrap.internals ?? {}, testing: { runTests, runBenchmarks, enableTest, enableBench, }, };
window.__bootstrap.denoNs.bench = bench; window.__bootstrap.denoNs.test = test;})(this);
Version Info