2022-01-07 22:09:52 -05:00
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
2021-02-04 17:18:32 -05:00
"use strict" ;
2020-07-19 13:49:44 -04:00
( ( window ) => {
2020-09-17 12:09:50 -04:00
const core = window . Deno . core ;
2021-05-18 11:24:01 -04:00
const { setExitHandler } = window . _ _bootstrap . os ;
2022-05-30 13:58:44 -04:00
const { Console } = window . _ _bootstrap . console ;
2021-10-13 13:04:44 -04:00
const { serializePermissions } = window . _ _bootstrap . permissions ;
2022-03-19 08:57:37 -04:00
const { assert } = window . _ _bootstrap . infra ;
2021-07-03 18:17:52 -04:00
const {
2022-04-20 15:06:39 -04:00
ArrayFrom ,
2021-07-03 18:17:52 -04:00
ArrayPrototypeFilter ,
2022-02-16 13:53:17 -05:00
ArrayPrototypeJoin ,
2022-04-20 15:06:39 -04:00
ArrayPrototypeMap ,
2021-07-03 18:17:52 -04:00
ArrayPrototypePush ,
2021-12-10 18:30:46 -05:00
ArrayPrototypeShift ,
2021-10-11 09:45:02 -04:00
ArrayPrototypeSome ,
2022-04-20 15:06:39 -04:00
ArrayPrototypeSort ,
2021-07-03 18:17:52 -04:00
DateNow ,
2021-10-11 09:45:02 -04:00
Error ,
2022-02-01 12:06:11 -05:00
FunctionPrototype ,
2022-02-16 13:53:17 -05:00
Map ,
MapPrototypeHas ,
2022-04-20 15:06:39 -04:00
MathCeil ,
2022-01-25 11:03:38 -05:00
ObjectKeys ,
2022-02-16 13:53:17 -05:00
ObjectPrototypeIsPrototypeOf ,
2021-07-03 18:17:52 -04:00
Promise ,
2022-01-25 11:03:38 -05:00
RegExp ,
RegExpPrototypeTest ,
2022-02-07 07:54:32 -05:00
SafeArrayIterator ,
2022-02-16 13:53:17 -05:00
Set ,
2021-07-03 18:17:52 -04:00
StringPrototypeEndsWith ,
StringPrototypeIncludes ,
StringPrototypeSlice ,
2022-01-25 11:03:38 -05:00
StringPrototypeStartsWith ,
2021-10-11 09:45:02 -04:00
SymbolToStringTag ,
2022-01-25 11:03:38 -05:00
TypeError ,
2021-07-03 18:17:52 -04:00
} = window . _ _bootstrap . primordials ;
2020-07-19 13:49:44 -04:00
2021-12-10 18:30:46 -05:00
const opSanitizerDelayResolveQueue = [ ] ;
2021-11-29 19:27:30 -05:00
// 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 ( ) {
2021-12-10 18:30:46 -05:00
return new Promise ( ( resolve ) => {
2021-11-29 19:27:30 -05:00
setTimeout ( ( ) => {
2021-12-10 18:30:46 -05:00
ArrayPrototypePush ( opSanitizerDelayResolveQueue , resolve ) ;
2021-11-29 19:27:30 -05:00
} , 0 ) ;
} ) ;
}
function handleOpSanitizerDelayMacrotask ( ) {
2021-12-10 18:30:46 -05:00
ArrayPrototypeShift ( opSanitizerDelayResolveQueue ) ? . ( ) ;
return opSanitizerDelayResolveQueue . length === 0 ;
2021-11-29 19:27:30 -05:00
}
2022-02-16 13:53:17 -05:00
// 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_dgram_recv" : [ "receive a datagram message" , "awaiting the result of `Deno.DatagramConn#receive` call, or not breaking out of a for await loop looping over a `Deno.DatagramConn`" ] ,
"op_dgram_send" : [ "send a datagram message" , "awaiting the result of `Deno.DatagramConn#send` call" ] ,
"op_dns_resolve" : [ "resolve a DNS name" , "awaiting the result of a `Deno.resolveDns` call" ] ,
"op_emit" : [ "transpile code" , "awaiting the result of a `Deno.emit` 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" ] ,
2022-03-19 08:57:37 -04:00
"op_ffi_call_ptr_nonblocking" : [ "do a non blocking ffi call" , "awaiting the returned promise" ] ,
2022-02-16 13:53:17 -05:00
"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_read" : [ "read the body of a HTTP request" , "consuming the entire request body" ] ,
"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" : [ "accept a TCP connection" , "closing a `Deno.Listener`" ] ,
"op_net_connect" : [ "connect to a TCP or UDP 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_tls_accept" : [ "accept a TLS connection" , "closing a `Deno.TlsListener`" ] ,
"op_tls_connect" : [ "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`" ] ,
} ;
2020-07-19 13:49:44 -04:00
// 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 ) {
2021-10-12 09:58:04 -04:00
/** @param step {TestStep} */
return async function asyncOpSanitizer ( step ) {
2022-02-16 13:53:17 -05:00
const pre = core . metrics ( ) ;
const preTraces = new Map ( core . opCallTraces ) ;
2021-02-21 11:21:25 -05:00
try {
2021-10-12 09:58:04 -04:00
await fn ( step ) ;
2021-02-21 11:21:25 -05:00
} 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)
2021-11-29 19:27:30 -05:00
await opSanitizerDelay ( ) ;
2021-02-21 11:21:25 -05:00
}
2021-04-28 14:17:04 -04:00
2022-02-16 13:53:17 -05:00
if ( step . shouldSkipSanitizers ) return ;
2021-10-12 09:58:04 -04:00
2022-02-16 13:53:17 -05:00
const post = core . metrics ( ) ;
const postTraces = new Map ( core . opCallTraces ) ;
2021-10-11 11:00:33 -04:00
2020-07-19 13:49:44 -04:00
// 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 ;
2021-10-11 11:00:33 -04:00
2022-02-16 13:53:17 -05:00
if ( dispatchedDiff === completedDiff ) return ;
2021-10-11 11:00:33 -04:00
const details = [ ] ;
for ( const key in post . ops ) {
2022-02-16 13:53:17 -05:00
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 ;
2021-10-11 11:00:33 -04:00
2022-02-16 13:53:17 -05:00
if ( dispatchedDiff > completedDiff ) {
2022-03-22 19:24:45 -04:00
const [ name , hint ] = OP _DETAILS [ key ] || [ key , null ] ;
2022-02-16 13:53:17 -05:00
const count = dispatchedDiff - completedDiff ;
let message = ` ${ count } async operation ${
count === 1 ? "" : "s"
} to $ { name } $ {
count === 1 ? "was" : "were"
2022-03-22 19:24:45 -04:00
} started in this test , but never completed . ` ;
if ( hint ) {
message += ` This is often caused by not ${ hint } . ` ;
}
2022-02-16 13:53:17 -05:00
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 ) {
2022-04-13 08:18:12 -04:00
const [ name , hint ] = OP _DETAILS [ key ] || [ key , null ] ;
2022-02-16 13:53:17 -05:00
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"
2022-04-13 08:18:12 -04:00
} 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 } . ` : "" } ` ,
2022-02-16 13:53:17 -05:00
) ;
2021-10-11 11:00:33 -04:00
}
}
2022-02-25 10:14:46 -05:00
let msg = ` Test case is leaking async ops.
2021-10-11 11:00:33 -04:00
2022-06-06 14:26:57 -04:00
- $ { ArrayPrototypeJoin ( details , "\n - " ) } ` ;
2022-02-25 10:14:46 -05:00
if ( ! core . isOpCallTracingEnabled ( ) ) {
msg +=
` \n \n To get more details where ops were leaked, run again with --trace-ops flag. ` ;
2022-06-06 14:26:57 -04:00
} else {
msg += "\n" ;
2022-02-25 10:14:46 -05:00
}
2022-06-06 14:26:57 -04:00
throw assert ( false , msg ) ;
2020-07-19 13:49:44 -04:00
} ;
}
2022-01-25 11:03:38 -05:00
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" , "finsihed" ] ;
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." ;
}
}
2020-07-19 13:49:44 -04:00
// 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 ,
) {
2021-10-12 09:58:04 -04:00
/** @param step {TestStep} */
return async function resourceSanitizer ( step ) {
2020-09-17 12:09:50 -04:00
const pre = core . resources ( ) ;
2021-10-12 09:58:04 -04:00
await fn ( step ) ;
2021-10-13 08:55:12 -04:00
if ( step . shouldSkipSanitizers ) {
return ;
2021-10-12 09:58:04 -04:00
}
2020-09-17 12:09:50 -04:00
const post = core . resources ( ) ;
2020-07-19 13:49:44 -04:00
2022-02-07 07:54:32 -05:00
const allResources = new Set ( [
... new SafeArrayIterator ( ObjectKeys ( pre ) ) ,
... new SafeArrayIterator ( ObjectKeys ( post ) ) ,
] ) ;
2020-07-19 13:49:44 -04:00
2022-01-25 11:03:38 -05:00
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 } ` ;
details . push ( 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. ` ;
details . push ( detail ) ;
}
}
const message = ` Test case is leaking ${ details . length } resource ${
details . length === 1 ? "" : "s"
} :
- $ { details . join ( "\n - " ) }
` ;
assert ( details . length === 0 , message ) ;
2020-07-19 13:49:44 -04:00
} ;
}
2021-02-24 07:55:50 -05:00
// Wrap test function in additional assertion that makes sure
// that the test case does not accidentally exit prematurely.
2022-03-11 17:07:02 -05:00
function assertExit ( fn , isTest ) {
2021-10-11 09:45:02 -04:00
return async function exitSanitizer ( ... params ) {
2021-02-24 07:55:50 -05:00
setExitHandler ( ( exitCode ) => {
assert (
false ,
2022-03-11 17:07:02 -05:00
` ${
isTest ? "Test case" : "Bench"
} attempted to exit with exit code : $ { exitCode } ` ,
2021-02-24 07:55:50 -05:00
) ;
} ) ;
try {
2022-02-07 07:54:32 -05:00
await fn ( ... new SafeArrayIterator ( params ) ) ;
2021-02-24 07:55:50 -05:00
} catch ( err ) {
throw err ;
} finally {
setExitHandler ( null ) ;
}
} ;
}
2021-10-11 09:45:02 -04:00
function assertTestStepScopes ( fn ) {
/** @param step {TestStep} */
return async function testStepSanitizer ( step ) {
preValidation ( ) ;
// only report waiting after pre-validation
if ( step . canStreamReporting ( ) ) {
step . reportWait ( ) ;
}
await fn ( createTestContext ( step ) ) ;
postValidation ( ) ;
function preValidation ( ) {
const runningSteps = getPotentialConflictingRunningSteps ( ) ;
const runningStepsWithSanitizers = ArrayPrototypeFilter (
runningSteps ,
( t ) => t . usesSanitizer ,
) ;
if ( runningStepsWithSanitizers . length > 0 ) {
throw new Error (
"Cannot start test step while another test step with sanitizers is running.\n" +
runningStepsWithSanitizers
. map ( ( s ) => ` * ${ s . getFullName ( ) } ` )
. join ( "\n" ) ,
) ;
}
if ( step . usesSanitizer && runningSteps . length > 0 ) {
throw new Error (
"Cannot start test step with sanitizers while another test step is running.\n" +
runningSteps . map ( ( s ) => ` * ${ s . getFullName ( ) } ` ) . join ( "\n" ) ,
) ;
}
function getPotentialConflictingRunningSteps ( ) {
/** @type {TestStep[]} */
const results = [ ] ;
let childStep = step ;
for ( const ancestor of step . ancestors ( ) ) {
for ( const siblingStep of ancestor . children ) {
if ( siblingStep === childStep ) {
continue ;
}
if ( ! siblingStep . finalized ) {
ArrayPrototypePush ( results , siblingStep ) ;
}
}
childStep = ancestor ;
}
return results ;
}
}
function postValidation ( ) {
// check for any running steps
2021-10-12 09:58:04 -04:00
if ( step . hasRunningChildren ) {
2021-10-11 09:45:02 -04:00
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
for ( const ancestor of step . ancestors ( ) ) {
if ( ancestor . finalized ) {
throw new Error (
"Parent scope completed before test step finished execution. " +
"Ensure all steps are awaited (ex. `await t.step(...)`)." ,
) ;
}
}
}
} ;
}
2022-04-23 08:39:56 -04:00
function pledgePermissions ( permissions ) {
return core . opSync (
"op_pledge_test_permissions" ,
serializePermissions ( permissions ) ,
) ;
}
2021-07-13 18:11:02 -04:00
2022-04-23 08:39:56 -04:00
function restorePermissions ( token ) {
core . opSync ( "op_restore_test_permissions" , token ) ;
}
2021-07-13 18:11:02 -04:00
2022-04-23 08:39:56 -04:00
function withPermissions ( fn , permissions ) {
2021-10-11 09:45:02 -04:00
return async function applyPermissions ( ... params ) {
2021-07-13 18:11:02 -04:00
const token = pledgePermissions ( permissions ) ;
try {
2022-02-07 07:54:32 -05:00
await fn ( ... new SafeArrayIterator ( params ) ) ;
2021-07-13 18:11:02 -04:00
} finally {
restorePermissions ( token ) ;
}
} ;
}
2022-05-30 13:58:44 -04:00
/ * *
* @ typedef { {
* id : number ,
* name : string ,
* fn : BenchFunction
* origin : string ,
* filteredOut : boolean ,
* ignore : boolean ,
* only : boolean .
* sanitizeExit : boolean ,
* permissions : PermissionOptions ,
* } } BenchDescription
* /
2021-04-28 14:17:04 -04:00
const tests = [ ] ;
2022-05-30 13:58:44 -04:00
/** @type {BenchDescription[]} */
const benchDescs = [ ] ;
2020-07-19 13:49:44 -04:00
2021-10-11 09:45:02 -04:00
// Main test function provided by Deno.
2020-07-19 13:49:44 -04:00
function test (
feat(test): Add more overloads for "Deno.test" (#12749)
This commit adds 4 more overloads to "Deno.test()" API.
```
// Deno.test(function testName() { });
export function test(fn: (t: TestContext) => void | Promise<void>): void;
// Deno.test("test name", { only: true }, function() { });
export function test(
name: string,
options: Omit<TestDefinition, "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ name: "test name" }, function() { });
export function test(
options: Omit<TestDefinition, "fn">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ only: true }, function testName() { });
export function test(
options: Omit<TestDefinition, "fn" | "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
```
2021-11-23 08:57:51 -05:00
nameOrFnOrOptions ,
optionsOrFn ,
maybeFn ,
2020-07-19 13:49:44 -04:00
) {
let testDef ;
const defaults = {
ignore : false ,
only : false ,
sanitizeOps : true ,
sanitizeResources : true ,
2021-02-24 07:55:50 -05:00
sanitizeExit : true ,
2021-04-25 17:38:59 -04:00
permissions : null ,
2020-07-19 13:49:44 -04:00
} ;
feat(test): Add more overloads for "Deno.test" (#12749)
This commit adds 4 more overloads to "Deno.test()" API.
```
// Deno.test(function testName() { });
export function test(fn: (t: TestContext) => void | Promise<void>): void;
// Deno.test("test name", { only: true }, function() { });
export function test(
name: string,
options: Omit<TestDefinition, "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ name: "test name" }, function() { });
export function test(
options: Omit<TestDefinition, "fn">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ only: true }, function testName() { });
export function test(
options: Omit<TestDefinition, "fn" | "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
```
2021-11-23 08:57:51 -05:00
if ( typeof nameOrFnOrOptions === "string" ) {
if ( ! nameOrFnOrOptions ) {
2020-07-19 13:49:44 -04:00
throw new TypeError ( "The test name can't be empty" ) ;
}
feat(test): Add more overloads for "Deno.test" (#12749)
This commit adds 4 more overloads to "Deno.test()" API.
```
// Deno.test(function testName() { });
export function test(fn: (t: TestContext) => void | Promise<void>): void;
// Deno.test("test name", { only: true }, function() { });
export function test(
name: string,
options: Omit<TestDefinition, "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ name: "test name" }, function() { });
export function test(
options: Omit<TestDefinition, "fn">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ only: true }, function testName() { });
export function test(
options: Omit<TestDefinition, "fn" | "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
```
2021-11-23 08:57:51 -05:00
if ( typeof optionsOrFn === "function" ) {
testDef = { 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." ,
) ;
}
testDef = {
... 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()" ) ;
}
testDef = {
... defaults ,
fn : nameOrFnOrOptions ,
name : nameOrFnOrOptions . name ,
} ;
2020-07-19 13:49:44 -04:00
} else {
feat(test): Add more overloads for "Deno.test" (#12749)
This commit adds 4 more overloads to "Deno.test()" API.
```
// Deno.test(function testName() { });
export function test(fn: (t: TestContext) => void | Promise<void>): void;
// Deno.test("test name", { only: true }, function() { });
export function test(
name: string,
options: Omit<TestDefinition, "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ name: "test name" }, function() { });
export function test(
options: Omit<TestDefinition, "fn">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ only: true }, function testName() { });
export function test(
options: Omit<TestDefinition, "fn" | "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
```
2021-11-23 08:57:51 -05:00
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 ;
2020-07-19 13:49:44 -04:00
}
feat(test): Add more overloads for "Deno.test" (#12749)
This commit adds 4 more overloads to "Deno.test()" API.
```
// Deno.test(function testName() { });
export function test(fn: (t: TestContext) => void | Promise<void>): void;
// Deno.test("test name", { only: true }, function() { });
export function test(
name: string,
options: Omit<TestDefinition, "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ name: "test name" }, function() { });
export function test(
options: Omit<TestDefinition, "fn">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ only: true }, function testName() { });
export function test(
options: Omit<TestDefinition, "fn" | "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
```
2021-11-23 08:57:51 -05:00
if ( ! name ) {
2020-07-19 13:49:44 -04:00
throw new TypeError ( "The test name can't be empty" ) ;
}
feat(test): Add more overloads for "Deno.test" (#12749)
This commit adds 4 more overloads to "Deno.test()" API.
```
// Deno.test(function testName() { });
export function test(fn: (t: TestContext) => void | Promise<void>): void;
// Deno.test("test name", { only: true }, function() { });
export function test(
name: string,
options: Omit<TestDefinition, "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ name: "test name" }, function() { });
export function test(
options: Omit<TestDefinition, "fn">,
fn: (t: TestContext) => void | Promise<void>,
): void;
// Deno.test({ only: true }, function testName() { });
export function test(
options: Omit<TestDefinition, "fn" | "name">,
fn: (t: TestContext) => void | Promise<void>,
): void;
```
2021-11-23 08:57:51 -05:00
testDef = { ... defaults , ... nameOrFnOrOptions , fn , name } ;
2020-07-19 13:49:44 -04:00
}
2021-10-11 09:45:02 -04:00
testDef . fn = wrapTestFnWithSanitizers ( testDef . fn , testDef ) ;
2021-02-24 07:55:50 -05:00
2021-07-13 18:11:02 -04:00
if ( testDef . permissions ) {
testDef . fn = withPermissions (
testDef . fn ,
2021-10-13 13:04:44 -04:00
testDef . permissions ,
2021-07-13 18:11:02 -04:00
) ;
}
2022-05-04 19:15:54 -04:00
const jsError = Deno . core . destructureError ( new Error ( ) ) ;
// Note: There might pop up a case where one of the filename, line number or
// column number from the caller isn't defined. We assume never for now.
// Make `TestDescription::location` optional if such a case is found.
testDef . location = {
fileName : jsError . frames [ 1 ] . fileName ,
lineNumber : jsError . frames [ 1 ] . lineNumber ,
columnNumber : jsError . frames [ 1 ] . columnNumber ,
} ;
2021-07-03 18:17:52 -04:00
ArrayPrototypePush ( tests , testDef ) ;
2020-07-19 13:49:44 -04:00
}
2022-03-11 17:07:02 -05:00
// Main bench function provided by Deno.
function bench (
nameOrFnOrOptions ,
optionsOrFn ,
maybeFn ,
) {
2022-03-23 11:33:42 -04:00
core . opSync ( "op_bench_check_unstable" ) ;
2022-05-30 13:58:44 -04:00
let benchDesc ;
2022-03-11 17:07:02 -05:00
const defaults = {
ignore : false ,
2022-05-30 13:58:44 -04:00
baseline : false ,
2022-03-11 17:07:02 -05:00
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" ) {
2022-05-30 13:58:44 -04:00
benchDesc = { fn : optionsOrFn , name : nameOrFnOrOptions , ... defaults } ;
2022-03-11 17:07:02 -05:00
} 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." ,
) ;
}
2022-05-30 13:58:44 -04:00
benchDesc = {
2022-03-11 17:07:02 -05:00
... 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()" ) ;
}
2022-05-30 13:58:44 -04:00
benchDesc = {
2022-03-11 17:07:02 -05:00
... 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" ) ;
}
2022-05-30 13:58:44 -04:00
benchDesc = { ... defaults , ... nameOrFnOrOptions , fn , name } ;
2022-03-11 17:07:02 -05:00
}
2022-05-30 13:58:44 -04:00
benchDesc . origin = getBenchOrigin ( ) ;
2022-04-20 15:06:39 -04:00
const AsyncFunction = ( async ( ) => { } ) . constructor ;
2022-05-30 13:58:44 -04:00
benchDesc . async = AsyncFunction === benchDesc . fn . constructor ;
2022-04-20 15:06:39 -04:00
2022-05-30 13:58:44 -04:00
const { id , filteredOut } = core . opSync ( "op_register_bench" , benchDesc ) ;
benchDesc . id = id ;
benchDesc . filteredOut = filteredOut ;
2021-09-30 15:54:56 -04:00
2022-05-30 13:58:44 -04:00
ArrayPrototypePush ( benchDescs , benchDesc ) ;
2021-09-30 15:54:56 -04:00
}
2022-03-29 18:59:27 -04:00
/ * *
* @ param { string | { include ? : string [ ] , exclude ? : string [ ] } } filter
* @ returns { ( def : { name : string } ) => boolean }
* /
2021-04-28 14:17:04 -04:00
function createTestFilter ( filter ) {
2022-03-29 18:59:27 -04:00
if ( ! filter ) {
return ( ) => true ;
}
const regex =
typeof filter === "string" && StringPrototypeStartsWith ( filter , "/" ) &&
StringPrototypeEndsWith ( filter , "/" )
? new RegExp ( StringPrototypeSlice ( filter , 1 , filter . length - 1 ) )
: undefined ;
const filterIsObject = filter != null && typeof filter === "object" ;
2021-04-28 14:17:04 -04:00
return ( def ) => {
2022-03-29 18:59:27 -04:00
if ( regex ) {
return RegExpPrototypeTest ( regex , def . name ) ;
}
if ( filterIsObject ) {
if ( filter . include && ! filter . include . includes ( def . name ) ) {
return false ;
} else if ( filter . exclude && filter . exclude . includes ( def . name ) ) {
return false ;
} else {
return true ;
2020-07-19 13:49:44 -04:00
}
}
2022-03-29 18:59:27 -04:00
return StringPrototypeIncludes ( def . name , filter ) ;
2021-04-28 14:17:04 -04:00
} ;
2020-07-19 13:49:44 -04:00
}
2021-10-11 09:45:02 -04:00
async function runTest ( test , description ) {
if ( test . ignore ) {
2021-07-05 12:36:43 -04:00
return "ignored" ;
}
2021-10-11 09:45:02 -04:00
const step = new TestStep ( {
name : test . name ,
parent : undefined ,
2022-04-06 10:51:38 -04:00
parentContext : undefined ,
2021-10-11 09:45:02 -04:00
rootTestDescription : description ,
sanitizeOps : test . sanitizeOps ,
sanitizeResources : test . sanitizeResources ,
sanitizeExit : test . sanitizeExit ,
} ) ;
2021-04-28 14:17:04 -04:00
try {
2021-10-11 09:45:02 -04:00
await test . fn ( step ) ;
const failCount = step . failedChildStepsCount ( ) ;
return failCount === 0 ? "ok" : {
2022-04-16 13:51:12 -04:00
"failed" : core . destructureError (
2021-10-11 09:45:02 -04:00
new Error (
` ${ failCount } test step ${ failCount === 1 ? "" : "s" } failed. ` ,
) ,
) ,
} ;
2021-04-28 14:17:04 -04:00
} catch ( error ) {
2021-10-11 09:45:02 -04:00
return {
2022-04-16 13:51:12 -04:00
"failed" : core . destructureError ( error ) ,
2021-10-11 09:45:02 -04:00
} ;
} finally {
2021-10-13 08:55:12 -04:00
step . finalized = true ;
2021-10-11 09:45:02 -04:00
// ensure the children report their result
for ( const child of step . children ) {
child . reportResult ( ) ;
}
2021-04-28 14:17:04 -04:00
}
2020-07-19 13:49:44 -04:00
}
2022-04-20 15:06:39 -04:00
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 ) ,
} ;
}
2022-05-30 13:58:44 -04:00
async function benchMeasure ( timeBudget , desc ) {
const fn = desc . fn ;
2022-04-20 15:06:39 -04:00
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 ;
2022-05-30 13:58:44 -04:00
if ( ! desc . async ) {
2022-04-20 15:06:39 -04:00
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 ;
2022-05-30 13:58:44 -04:00
if ( ! desc . async ) {
2022-04-20 15:06:39 -04:00
while ( budget > 0 || iterations -- > 0 ) {
const t1 = benchNow ( ) ;
fn ( ) ;
const iterationTime = benchNow ( ) - t1 ;
n ++ ;
avg += iterationTime ;
budget -= iterationTime ;
2022-05-30 13:58:44 -04:00
ArrayPrototypePush ( all , iterationTime ) ;
2022-04-20 15:06:39 -04:00
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 ;
2022-05-30 13:58:44 -04:00
ArrayPrototypePush ( all , iterationTime ) ;
2022-04-20 15:06:39 -04:00
if ( iterationTime < min ) min = iterationTime ;
if ( iterationTime > max ) max = iterationTime ;
}
}
} else {
let iterations = 10 ;
let budget = timeBudget * 1e6 ;
2022-05-30 13:58:44 -04:00
if ( ! desc . async ) {
2022-04-20 15:06:39 -04:00
while ( budget > 0 || iterations -- > 0 ) {
const t1 = benchNow ( ) ;
for ( let c = 0 ; c < lowPrecisionThresholdInNs ; c ++ ) fn ( ) ;
const iterationTime = ( benchNow ( ) - t1 ) / lowPrecisionThresholdInNs ;
n ++ ;
avg += iterationTime ;
2022-05-30 13:58:44 -04:00
ArrayPrototypePush ( all , iterationTime ) ;
2022-04-20 15:06:39 -04:00
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 ;
all . push ( iterationTime ) ;
if ( iterationTime < min ) min = iterationTime ;
if ( iterationTime > max ) max = iterationTime ;
budget -= iterationTime * lowPrecisionThresholdInNs ;
}
}
2022-03-11 17:07:02 -05:00
}
2022-04-20 15:06:39 -04:00
all . sort ( compareMeasurements ) ;
return benchStats ( n , wavg > lowPrecisionThresholdInNs , avg , min , max , all ) ;
}
2022-05-30 13:58:44 -04:00
async function runBench ( desc ) {
2022-04-20 15:06:39 -04:00
let token = null ;
2022-03-11 17:07:02 -05:00
2022-04-20 15:06:39 -04:00
try {
2022-05-30 13:58:44 -04:00
if ( desc . permissions ) {
token = pledgePermissions ( desc . permissions ) ;
2022-04-23 08:39:56 -04:00
}
2022-05-30 13:58:44 -04:00
if ( desc . sanitizeExit ) {
2022-04-23 08:39:56 -04:00
setExitHandler ( ( exitCode ) => {
assert (
false ,
` Bench attempted to exit with exit code: ${ exitCode } ` ,
) ;
} ) ;
2022-03-11 17:07:02 -05:00
}
2022-04-20 15:06:39 -04:00
const benchTimeInMs = 500 ;
2022-05-30 13:58:44 -04:00
const stats = await benchMeasure ( benchTimeInMs , desc ) ;
2022-03-11 17:07:02 -05:00
2022-05-30 13:58:44 -04:00
return { ok : stats } ;
2022-03-11 17:07:02 -05:00
} catch ( error ) {
2022-05-30 13:58:44 -04:00
return { failed : core . destructureError ( error ) } ;
2022-04-20 15:06:39 -04:00
} finally {
2022-04-23 08:39:56 -04:00
if ( bench . sanitizeExit ) setExitHandler ( null ) ;
if ( token !== null ) restorePermissions ( token ) ;
2022-03-11 17:07:02 -05:00
}
}
2022-05-30 13:58:44 -04:00
let origin = null ;
2021-07-14 15:05:16 -04:00
function getTestOrigin ( ) {
2022-05-30 13:58:44 -04:00
if ( origin == null ) {
origin = core . opSync ( "op_get_test_origin" ) ;
}
return origin ;
2021-07-14 15:05:16 -04:00
}
2022-03-11 17:07:02 -05:00
function getBenchOrigin ( ) {
2022-05-30 13:58:44 -04:00
if ( origin == null ) {
origin = core . opSync ( "op_get_bench_origin" ) ;
}
return origin ;
2022-03-11 17:07:02 -05:00
}
2021-09-05 16:42:35 -04:00
function reportTestPlan ( plan ) {
core . opSync ( "op_dispatch_test_event" , {
plan ,
} ) ;
}
function reportTestWait ( test ) {
core . opSync ( "op_dispatch_test_event" , {
wait : test ,
} ) ;
}
function reportTestResult ( test , result , elapsed ) {
core . opSync ( "op_dispatch_test_event" , {
result : [ test , result , elapsed ] ,
} ) ;
2021-07-14 15:05:16 -04:00
}
2021-10-11 09:45:02 -04:00
function reportTestStepWait ( testDescription ) {
core . opSync ( "op_dispatch_test_event" , {
stepWait : testDescription ,
} ) ;
}
function reportTestStepResult ( testDescription , result , elapsed ) {
core . opSync ( "op_dispatch_test_event" , {
stepResult : [ testDescription , result , elapsed ] ,
} ) ;
}
2022-03-11 17:07:02 -05:00
function benchNow ( ) {
return core . opSync ( "op_bench_now" ) ;
}
2020-07-19 13:49:44 -04:00
async function runTests ( {
2021-04-28 14:17:04 -04:00
filter = null ,
2021-07-05 21:20:33 -04:00
shuffle = null ,
2020-07-19 13:49:44 -04:00
} = { } ) {
2021-11-29 19:27:30 -05:00
core . setMacrotaskCallback ( handleOpSanitizerDelayMacrotask ) ;
2021-07-14 15:05:16 -04:00
const origin = getTestOrigin ( ) ;
2020-07-19 13:49:44 -04:00
2021-07-03 18:17:52 -04:00
const only = ArrayPrototypeFilter ( tests , ( test ) => test . only ) ;
2021-07-14 15:05:16 -04:00
const filtered = ArrayPrototypeFilter (
2021-10-12 09:58:04 -04:00
only . length > 0 ? only : tests ,
2021-04-28 14:17:04 -04:00
createTestFilter ( filter ) ,
) ;
2021-07-14 15:05:16 -04:00
2021-09-05 16:42:35 -04:00
reportTestPlan ( {
origin ,
total : filtered . length ,
filteredOut : tests . length - filtered . length ,
usedOnly : only . length > 0 ,
2021-04-28 14:17:04 -04:00
} ) ;
2020-07-19 13:49:44 -04:00
2021-07-05 21:20:33 -04:00
if ( shuffle !== null ) {
// http://en.wikipedia.org/wiki/Linear_congruential_generator
const nextInt = ( function ( state ) {
const m = 0x80000000 ;
const a = 1103515245 ;
const c = 12345 ;
return function ( max ) {
return state = ( ( a * state + c ) % m ) % max ;
} ;
} ( shuffle ) ) ;
2021-07-14 15:05:16 -04:00
for ( let i = filtered . length - 1 ; i > 0 ; i -- ) {
2021-07-05 21:20:33 -04:00
const j = nextInt ( i ) ;
2021-07-14 15:05:16 -04:00
[ filtered [ i ] , filtered [ j ] ] = [ filtered [ j ] , filtered [ i ] ] ;
2021-07-05 21:20:33 -04:00
}
}
2021-07-14 15:05:16 -04:00
for ( const test of filtered ) {
const description = {
origin ,
name : test . name ,
2022-05-04 19:15:54 -04:00
location : test . location ,
2021-07-14 15:05:16 -04:00
} ;
2021-07-05 04:26:57 -04:00
const earlier = DateNow ( ) ;
2021-09-05 16:42:35 -04:00
reportTestWait ( description ) ;
2021-07-05 04:26:57 -04:00
2021-10-11 09:45:02 -04:00
const result = await runTest ( test , description ) ;
2021-07-14 15:05:16 -04:00
const elapsed = DateNow ( ) - earlier ;
2021-07-05 04:26:57 -04:00
2021-09-05 16:42:35 -04:00
reportTestResult ( description , result , elapsed ) ;
2020-07-19 13:49:44 -04:00
}
}
2022-05-30 13:58:44 -04:00
async function runBenchmarks ( ) {
2022-03-11 17:07:02 -05:00
core . setMacrotaskCallback ( handleOpSanitizerDelayMacrotask ) ;
const origin = getBenchOrigin ( ) ;
const originalConsole = globalThis . console ;
2022-05-30 13:58:44 -04:00
globalThis . console = new Console ( ( s ) => {
core . opSync ( "op_dispatch_bench_event" , { output : s } ) ;
} ) ;
2022-03-11 17:07:02 -05:00
2022-05-30 13:58:44 -04:00
const only = ArrayPrototypeFilter ( benchDescs , ( bench ) => bench . only ) ;
2022-03-11 17:07:02 -05:00
const filtered = ArrayPrototypeFilter (
2022-05-30 13:58:44 -04:00
only . length > 0 ? only : benchDescs ,
( desc ) => ! desc . filteredOut && ! desc . ignore ,
2022-03-11 17:07:02 -05:00
) ;
2022-04-20 15:06:39 -04:00
let groups = new Set ( ) ;
// make sure ungrouped benchmarks are placed above grouped
groups . add ( undefined ) ;
2022-05-30 13:58:44 -04:00
for ( const desc of filtered ) {
desc . group || = undefined ;
groups . add ( desc . group ) ;
2022-04-20 15:06:39 -04:00
}
groups = ArrayFrom ( groups ) ;
ArrayPrototypeSort (
2022-05-30 13:58:44 -04:00
filtered ,
2022-04-20 15:06:39 -04:00
( a , b ) => groups . indexOf ( a . group ) - groups . indexOf ( b . group ) ,
) ;
2022-05-30 13:58:44 -04:00
core . opSync ( "op_dispatch_bench_event" , {
plan : {
origin ,
total : filtered . length ,
usedOnly : only . length > 0 ,
names : ArrayPrototypeMap ( filtered , ( desc ) => desc . name ) ,
} ,
2022-03-11 17:07:02 -05:00
} ) ;
2022-05-30 13:58:44 -04:00
for ( const desc of filtered ) {
desc . baseline = ! ! desc . baseline ;
core . opSync ( "op_dispatch_bench_event" , { wait : desc . id } ) ;
core . opSync ( "op_dispatch_bench_event" , {
result : [ desc . id , await runBench ( desc ) ] ,
} ) ;
2022-03-11 17:07:02 -05:00
}
globalThis . console = originalConsole ;
}
2021-10-11 09:45:02 -04:00
/ * *
* @ typedef { {
* fn : ( t : TestContext ) => void | Promise < void > ,
* name : string ,
* ignore ? : boolean ,
* sanitizeOps ? : boolean ,
* sanitizeResources ? : boolean ,
* sanitizeExit ? : boolean ,
* } } TestStepDefinition
*
* @ typedef { {
2022-04-06 10:51:38 -04:00
* name : string ,
2021-10-11 09:45:02 -04:00
* parent : TestStep | undefined ,
2022-04-06 10:51:38 -04:00
* parentContext : TestContext | undefined ,
2021-10-11 09:45:02 -04:00
* rootTestDescription : { origin : string ; name : string } ;
* sanitizeOps : boolean ,
* sanitizeResources : boolean ,
* sanitizeExit : boolean ,
* } } TestStepParams
* /
class TestStep {
/** @type {TestStepParams} */
# params ;
reportedWait = false ;
# reportedResult = false ;
finalized = false ;
elapsed = 0 ;
2021-10-12 09:58:04 -04:00
/** @type "ok" | "ignored" | "pending" | "failed" */
2021-10-11 09:45:02 -04:00
status = "pending" ;
error = undefined ;
/** @type {TestStep[]} */
children = [ ] ;
/** @param params {TestStepParams} */
constructor ( params ) {
this . # params = params ;
}
get name ( ) {
return this . # params . name ;
}
get parent ( ) {
return this . # params . parent ;
}
2022-04-06 10:51:38 -04:00
get parentContext ( ) {
return this . # params . parentContext ;
}
2021-10-11 09:45:02 -04:00
get rootTestDescription ( ) {
return this . # params . rootTestDescription ;
}
get sanitizerOptions ( ) {
return {
sanitizeResources : this . # params . sanitizeResources ,
sanitizeOps : this . # params . sanitizeOps ,
sanitizeExit : this . # params . sanitizeExit ,
} ;
}
get usesSanitizer ( ) {
return this . # params . sanitizeResources ||
this . # params . sanitizeOps ||
this . # params . sanitizeExit ;
}
2021-10-13 08:55:12 -04:00
/ * * I f a t e s t v a l i d a t i o n e r r o r a l r e a d y o c c u r r e d t h e n d o n ' t b o t h e r c h e c k i n g
* the sanitizers as that will create extra noise .
* /
get shouldSkipSanitizers ( ) {
return this . hasRunningChildren || this . parent ? . finalized ;
}
2021-10-12 09:58:04 -04:00
get hasRunningChildren ( ) {
return ArrayPrototypeSome (
this . children ,
/** @param step {TestStep} */
( step ) => step . status === "pending" ,
) ;
}
2021-10-11 09:45:02 -04:00
failedChildStepsCount ( ) {
return ArrayPrototypeFilter (
this . children ,
/** @param step {TestStep} */
( step ) => step . status === "failed" ,
) . length ;
}
canStreamReporting ( ) {
// there should only ever be one sub step running when running with
// sanitizers, so we can use this to tell if we can stream reporting
return this . selfAndAllAncestorsUseSanitizer ( ) &&
this . children . every ( ( c ) => c . usesSanitizer || c . finalized ) ;
}
selfAndAllAncestorsUseSanitizer ( ) {
if ( ! this . usesSanitizer ) {
return false ;
}
for ( const ancestor of this . ancestors ( ) ) {
if ( ! ancestor . usesSanitizer ) {
return false ;
}
}
return true ;
}
* ancestors ( ) {
let ancestor = this . parent ;
while ( ancestor ) {
yield ancestor ;
ancestor = ancestor . parent ;
}
}
getFullName ( ) {
if ( this . parent ) {
return ` ${ this . parent . getFullName ( ) } > ${ this . name } ` ;
} else {
return this . name ;
}
}
reportWait ( ) {
if ( this . reportedWait || ! this . parent ) {
return ;
}
reportTestStepWait ( this . # getTestStepDescription ( ) ) ;
this . reportedWait = true ;
}
reportResult ( ) {
if ( this . # reportedResult || ! this . parent ) {
return ;
}
this . reportWait ( ) ;
for ( const child of this . children ) {
child . reportResult ( ) ;
}
reportTestStepResult (
this . # getTestStepDescription ( ) ,
this . # getStepResult ( ) ,
this . elapsed ,
) ;
this . # reportedResult = true ;
}
# getStepResult ( ) {
switch ( this . status ) {
case "ok" :
return "ok" ;
case "ignored" :
return "ignored" ;
case "pending" :
return {
2022-04-16 13:51:12 -04:00
"pending" : this . error && core . destructureError ( this . error ) ,
2021-10-11 09:45:02 -04:00
} ;
case "failed" :
return {
2022-04-16 13:51:12 -04:00
"failed" : this . error && core . destructureError ( this . error ) ,
2021-10-11 09:45:02 -04:00
} ;
default :
throw new Error ( ` Unhandled status: ${ this . status } ` ) ;
}
}
# getTestStepDescription ( ) {
return {
test : this . rootTestDescription ,
name : this . name ,
level : this . # getLevel ( ) ,
} ;
}
# getLevel ( ) {
let count = 0 ;
for ( const _ of this . ancestors ( ) ) {
count ++ ;
}
return count ;
}
}
/** @param parentStep {TestStep} */
function createTestContext ( parentStep ) {
return {
[ SymbolToStringTag ] : "TestContext" ,
2022-04-06 10:51:38 -04:00
/ * *
* The current test name .
* /
name : parentStep . name ,
/ * *
* Parent test context .
* /
parent : parentStep . parentContext ? ? undefined ,
/ * *
* File Uri of the test code .
* /
origin : parentStep . rootTestDescription . origin ,
2021-10-11 09:45:02 -04:00
/ * *
* @ param nameOrTestDefinition { string | TestStepDefinition }
* @ param fn { ( t : TestContext ) => void | Promise < void > }
* /
async step ( nameOrTestDefinition , fn ) {
if ( parentStep . 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." ,
) ;
}
const definition = getDefinition ( ) ;
const subStep = new TestStep ( {
name : definition . name ,
parent : parentStep ,
2022-04-06 10:51:38 -04:00
parentContext : this ,
2021-10-11 09:45:02 -04:00
rootTestDescription : parentStep . rootTestDescription ,
sanitizeOps : getOrDefault (
definition . sanitizeOps ,
parentStep . sanitizerOptions . sanitizeOps ,
) ,
sanitizeResources : getOrDefault (
definition . sanitizeResources ,
parentStep . sanitizerOptions . sanitizeResources ,
) ,
sanitizeExit : getOrDefault (
definition . sanitizeExit ,
parentStep . sanitizerOptions . sanitizeExit ,
) ,
} ) ;
ArrayPrototypePush ( parentStep . children , subStep ) ;
try {
if ( definition . ignore ) {
subStep . status = "ignored" ;
subStep . finalized = true ;
if ( subStep . canStreamReporting ( ) ) {
subStep . reportResult ( ) ;
}
return false ;
}
const testFn = wrapTestFnWithSanitizers (
definition . fn ,
subStep . sanitizerOptions ,
) ;
const start = DateNow ( ) ;
try {
await testFn ( subStep ) ;
if ( subStep . failedChildStepsCount ( ) > 0 ) {
subStep . status = "failed" ;
} else {
subStep . status = "ok" ;
}
} catch ( error ) {
2022-04-16 13:51:12 -04:00
subStep . error = error ;
2021-10-11 09:45:02 -04:00
subStep . status = "failed" ;
}
subStep . elapsed = DateNow ( ) - start ;
if ( subStep . parent ? . finalized ) {
// always point this test out as one that was still running
// if the parent step finalized
subStep . status = "pending" ;
}
subStep . finalized = true ;
if ( subStep . reportedWait && subStep . canStreamReporting ( ) ) {
subStep . reportResult ( ) ;
}
return subStep . status === "ok" ;
} finally {
if ( parentStep . canStreamReporting ( ) ) {
// flush any buffered steps
for ( const parentChild of parentStep . children ) {
parentChild . reportResult ( ) ;
}
}
}
/** @returns {TestStepDefinition} */
function getDefinition ( ) {
if ( typeof nameOrTestDefinition === "string" ) {
2022-02-01 12:06:11 -05:00
if ( ! ( ObjectPrototypeIsPrototypeOf ( FunctionPrototype , fn ) ) ) {
2021-10-11 09:45:02 -04:00
throw new TypeError ( "Expected function for second argument." ) ;
}
return {
name : nameOrTestDefinition ,
fn ,
} ;
} else if ( typeof nameOrTestDefinition === "object" ) {
return nameOrTestDefinition ;
} else {
throw new TypeError (
"Expected a test definition or name and function." ,
) ;
}
}
} ,
} ;
}
/ * *
* @ 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 ) {
2022-03-11 17:07:02 -05:00
testFn = assertExit ( testFn , true ) ;
2021-10-11 09:45:02 -04:00
}
return testFn ;
}
/ * *
* @ template T
* @ param value { T | undefined }
* @ param defaultValue { T }
* @ returns T
* /
function getOrDefault ( value , defaultValue ) {
return value == null ? defaultValue : value ;
}
2021-03-12 15:23:59 -05:00
window . _ _bootstrap . internals = {
... window . _ _bootstrap . internals ? ? { } ,
runTests ,
2022-03-11 17:07:02 -05:00
runBenchmarks ,
2021-03-12 15:23:59 -05:00
} ;
2020-07-19 13:49:44 -04:00
window . _ _bootstrap . testing = {
test ,
2022-03-11 17:07:02 -05:00
bench ,
2020-07-19 13:49:44 -04:00
} ;
} ) ( this ) ;