2024-01-01 14:58:21 -05:00
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2022-02-16 13:53:17 -05:00
2024-01-10 17:37:25 -05:00
import { core , primordials } from "ext:core/mod.js" ;
2024-01-25 14:54:35 -05:00
import { escapeName , withPermissions } from "ext:cli/40_test_common.js" ;
2023-12-08 02:33:25 -05:00
const ops = core . ops ;
const {
ArrayPrototypeFilter ,
ArrayPrototypeJoin ,
ArrayPrototypePush ,
ArrayPrototypeShift ,
DateNow ,
Error ,
Map ,
MapPrototypeGet ,
MapPrototypeHas ,
MapPrototypeSet ,
ObjectKeys ,
Promise ,
SafeArrayIterator ,
Set ,
SymbolToStringTag ,
TypeError ,
} = primordials ;
2024-01-10 17:37:25 -05:00
import { setExitHandler } from "ext:runtime/30_os.js" ;
import { setTimeout } from "ext:deno_web/02_timers.js" ;
2024-01-25 14:54:35 -05:00
/ * *
* @ typedef { {
* id : number ,
* name : string ,
* fn : TestFunction
* origin : string ,
* location : TestLocation ,
* ignore : boolean ,
* only : boolean .
* sanitizeOps : boolean ,
* sanitizeResources : boolean ,
* sanitizeExit : boolean ,
* permissions : PermissionOptions ,
* } } TestDescription
*
* @ typedef { {
* id : number ,
* name : string ,
* fn : TestFunction
* origin : string ,
* location : TestLocation ,
* ignore : boolean ,
* level : number ,
* parent : TestDescription | TestStepDescription ,
* rootId : number ,
* rootName : String ,
* sanitizeOps : boolean ,
* sanitizeResources : boolean ,
* sanitizeExit : boolean ,
* } } TestStepDescription
*
* @ typedef { {
* context : TestContext ,
* children : TestStepDescription [ ] ,
* completed : boolean ,
* } } TestState
*
* @ typedef { {
* context : TestContext ,
* children : TestStepDescription [ ] ,
* completed : boolean ,
* failed : boolean ,
* } } TestStepState
*
* @ typedef { {
* id : number ,
* name : string ,
* fn : BenchFunction
* origin : string ,
* ignore : boolean ,
* only : boolean .
* sanitizeExit : boolean ,
* permissions : PermissionOptions ,
* } } BenchDescription
* /
/** @type {Map<number, TestState | TestStepState>} */
const testStates = new Map ( ) ;
2023-12-08 02:33:25 -05:00
const opSanitizerDelayResolveQueue = [ ] ;
let hasSetOpSanitizerDelayMacrotask = false ;
// Even if every resource is closed by the end of a test, there can be a delay
// until the pending ops have all finished. This function returns a promise
// that resolves when it's (probably) fine to run the op sanitizer.
2023-11-10 08:33:03 -05:00
//
2023-12-08 02:33:25 -05:00
// This is implemented by adding a macrotask callback that runs after the
// all ready async ops resolve, and the timer macrotask. Using just a macrotask
// callback without delaying is sufficient, because when the macrotask callback
// runs after async op dispatch, we know that all async ops that can currently
// return `Poll::Ready` have done so, and have been dispatched to JS.
//
// Worker ops are an exception to this, because there is no way for the user to
// await shutdown of the worker from the thread calling `worker.terminate()`.
// Because of this, we give extra leeway for worker ops to complete, by waiting
// for a whole millisecond if there are pending worker ops.
function opSanitizerDelay ( hasPendingWorkerOps ) {
if ( ! hasSetOpSanitizerDelayMacrotask ) {
core . setMacrotaskCallback ( handleOpSanitizerDelayMacrotask ) ;
hasSetOpSanitizerDelayMacrotask = true ;
2023-11-24 22:46:16 -05:00
}
2023-12-08 02:33:25 -05:00
const p = new Promise ( ( resolve ) => {
// Schedule an async op to complete immediately to ensure the macrotask is
// run. We rely on the fact that enqueueing the resolver callback during the
// timeout callback will mean that the resolver gets called in the same
// event loop tick as the timeout callback.
setTimeout ( ( ) => {
ArrayPrototypePush ( opSanitizerDelayResolveQueue , resolve ) ;
} , hasPendingWorkerOps ? 1 : 0 ) ;
} ) ;
return p ;
}
function handleOpSanitizerDelayMacrotask ( ) {
const resolve = ArrayPrototypeShift ( opSanitizerDelayResolveQueue ) ;
if ( resolve ) {
resolve ( ) ;
return opSanitizerDelayResolveQueue . length === 0 ;
2023-11-24 22:46:16 -05:00
}
2023-12-08 02:33:25 -05:00
return undefined ; // we performed no work, so can skip microtasks checkpoint
}
2023-11-10 09:34:41 -05:00
2023-12-08 02:33:25 -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 = {
2023-11-24 22:46:16 -05:00
"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" ] ,
2024-01-23 18:31:52 -05:00
"op_fdatasync_async" : [ "flush pending data operations for a file to disk" , "awaiting the result of a `file.fdatasync` call" ] ,
2023-11-24 22:46:16 -05:00
"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" ] ,
2024-01-23 19:07:06 -05:00
"op_fsync_async" : [ "flush pending data operations for a file to disk" , "awaiting the result of a `file.fsync` call" ] ,
2023-11-24 22:46:16 -05:00
"op_ftruncate_async" : [ "truncate a file" , "awaiting the result of a `Deno.ftruncate` call" ] ,
"op_funlock_async" : [ "unlock a file" , "awaiting the result of a `Deno.funlock` call" ] ,
"op_futime_async" : [ "change file timestamps" , "awaiting the result of a `Deno.futime` call" ] ,
"op_http_accept" : [ "accept a HTTP request" , "closing a `Deno.HttpConn`" ] ,
"op_http_shutdown" : [ "shutdown a HTTP connection" , "awaiting `Deno.HttpEvent#respondWith`" ] ,
"op_http_upgrade_websocket" : [ "upgrade a HTTP connection to a WebSocket" , "awaiting `Deno.HttpEvent#respondWith`" ] ,
"op_http_write_headers" : [ "write HTTP response headers" , "awaiting `Deno.HttpEvent#respondWith`" ] ,
"op_http_write" : [ "write HTTP response body" , "awaiting `Deno.HttpEvent#respondWith`" ] ,
"op_link_async" : [ "create a hard link" , "awaiting the result of a `Deno.link` call" ] ,
"op_make_temp_dir_async" : [ "create a temporary directory" , "awaiting the result of a `Deno.makeTempDir` call" ] ,
"op_make_temp_file_async" : [ "create a temporary file" , "awaiting the result of a `Deno.makeTempFile` call" ] ,
"op_message_port_recv_message" : [ "receive a message from a MessagePort" , "awaiting the result of not closing a `MessagePort`" ] ,
"op_mkdir_async" : [ "create a directory" , "awaiting the result of a `Deno.mkdir` call" ] ,
"op_net_accept_tcp" : [ "accept a TCP stream" , "closing a `Deno.Listener`" ] ,
"op_net_accept_unix" : [ "accept a Unix stream" , "closing a `Deno.Listener`" ] ,
"op_net_connect_tcp" : [ "connect to a TCP server" , "awaiting a `Deno.connect` call" ] ,
"op_net_connect_unix" : [ "connect to a Unix server" , "awaiting a `Deno.connect` call" ] ,
"op_open_async" : [ "open a file" , "awaiting the result of a `Deno.open` call" ] ,
"op_read_dir_async" : [ "read a directory" , "collecting all items in the async iterable returned from a `Deno.readDir` call" ] ,
"op_read_link_async" : [ "read a symlink" , "awaiting the result of a `Deno.readLink` call" ] ,
"op_realpath_async" : [ "resolve a path" , "awaiting the result of a `Deno.realpath` call" ] ,
"op_remove_async" : [ "remove a file or directory" , "awaiting the result of a `Deno.remove` call" ] ,
"op_rename_async" : [ "rename a file or directory" , "awaiting the result of a `Deno.rename` call" ] ,
"op_run_status" : [ "get the status of a subprocess" , "awaiting the result of a `Deno.Process#status` call" ] ,
"op_seek_async" : [ "seek in a file" , "awaiting the result of a `Deno.File#seek` call" ] ,
"op_signal_poll" : [ "get the next signal" , "un-registering a OS signal handler" ] ,
"op_sleep" : [ "sleep for a duration" , "cancelling a `setTimeout` or `setInterval` call" ] ,
"op_stat_async" : [ "get file metadata" , "awaiting the result of a `Deno.stat` call" ] ,
"op_symlink_async" : [ "create a symlink" , "awaiting the result of a `Deno.symlink` call" ] ,
"op_net_accept_tls" : [ "accept a TLS stream" , "closing a `Deno.TlsListener`" ] ,
"op_net_connect_tls" : [ "connect to a TLS server" , "awaiting a `Deno.connectTls` call" ] ,
"op_tls_handshake" : [ "perform a TLS handshake" , "awaiting a `Deno.TlsConn#handshake` call" ] ,
"op_tls_start" : [ "start a TLS connection" , "awaiting a `Deno.startTls` call" ] ,
"op_truncate_async" : [ "truncate a file" , "awaiting the result of a `Deno.truncate` call" ] ,
"op_utime_async" : [ "change file timestamps" , "awaiting the result of a `Deno.utime` call" ] ,
"op_host_recv_message" : [ "receive a message from a web worker" , "terminating a `Worker`" ] ,
"op_host_recv_ctrl" : [ "receive a message from a web worker" , "terminating a `Worker`" ] ,
2023-12-08 19:19:16 -05:00
"op_webgpu_buffer_get_map_async" : [ "map a WebGPU buffer" , "awaiting the result of a `GPUBuffer#mapAsync` call" ] ,
"op_webgpu_request_adapter" : [ "request a WebGPU adapter" , "awaiting the result of a `navigator.gpu.requestAdapter` call" ] ,
"op_webgpu_request_device" : [ "request a WebGPU device" , "awaiting the result of a `GPUAdapter#requestDevice` call" ] ,
"op_ws_close" : [ "close a WebSocket" , "awaiting until the `close` event is emitted on a `WebSocket`, or the `WebSocketStream#closed` promise resolves" ] ,
2023-11-24 22:46:16 -05:00
"op_ws_create" : [ "create a WebSocket" , "awaiting until the `open` event is emitted on a `WebSocket`, or the result of a `WebSocketStream#connection` promise" ] ,
"op_ws_next_event" : [ "receive the next message on a WebSocket" , "closing a `WebSocket` or `WebSocketStream`" ] ,
"op_ws_send_text" : [ "send a message on a WebSocket" , "closing a `WebSocket` or `WebSocketStream`" ] ,
"op_ws_send_binary" : [ "send a message on a WebSocket" , "closing a `WebSocket` or `WebSocketStream`" ] ,
"op_ws_send_binary_ab" : [ "send a message on a WebSocket" , "closing a `WebSocket` or `WebSocketStream`" ] ,
"op_ws_send_ping" : [ "send a message on a WebSocket" , "closing a `WebSocket` or `WebSocketStream`" ] ,
"op_ws_send_pong" : [ "send a message on a WebSocket" , "closing a `WebSocket` or `WebSocketStream`" ] ,
} ;
2023-02-07 14:22:46 -05:00
2023-12-08 02:33:25 -05:00
let opIdHostRecvMessage = - 1 ;
let opIdHostRecvCtrl = - 1 ;
let opNames = null ;
function populateOpNames ( ) {
2024-01-10 17:37:25 -05:00
opNames = ops . op _op _names ( ) ;
2023-12-08 02:33:25 -05:00
opIdHostRecvMessage = opNames . indexOf ( "op_host_recv_message" ) ;
opIdHostRecvCtrl = opNames . indexOf ( "op_host_recv_ctrl" ) ;
}
// Wrap test function in additional assertion that makes sure
// the test case does not leak async "ops" - ie. number of async
// completed ops after the test is the same as number of dispatched
// ops. Note that "unref" ops are ignored since in nature that are
// optional.
function assertOps ( fn ) {
/** @param desc {TestDescription | TestStepDescription} */
return async function asyncOpSanitizer ( desc ) {
if ( opNames === null ) populateOpNames ( ) ;
2024-01-10 17:37:25 -05:00
const res = ops . op _test _op _sanitizer _collect (
2023-12-08 02:33:25 -05:00
desc . id ,
false ,
opIdHostRecvMessage ,
opIdHostRecvCtrl ,
) ;
if ( res !== 0 ) {
await opSanitizerDelay ( res === 2 ) ;
2024-01-10 17:37:25 -05:00
ops . op _test _op _sanitizer _collect (
2023-12-08 02:33:25 -05:00
desc . id ,
true ,
opIdHostRecvMessage ,
opIdHostRecvCtrl ,
) ;
}
const preTraces = new Map ( core . opCallTraces ) ;
let postTraces ;
let report = null ;
2023-09-16 01:48:31 -04:00
2023-12-08 02:33:25 -05:00
try {
const innerResult = await fn ( desc ) ;
if ( innerResult ) return innerResult ;
} finally {
2024-01-10 17:37:25 -05:00
let res = ops . op _test _op _sanitizer _finish (
2023-09-19 06:10:20 -04:00
desc . id ,
false ,
opIdHostRecvMessage ,
opIdHostRecvCtrl ,
) ;
2023-12-08 02:33:25 -05:00
if ( res === 1 || res === 2 ) {
2023-09-19 06:10:20 -04:00
await opSanitizerDelay ( res === 2 ) ;
2024-01-10 17:37:25 -05:00
res = ops . op _test _op _sanitizer _finish (
2023-09-19 06:10:20 -04:00
desc . id ,
true ,
opIdHostRecvMessage ,
opIdHostRecvCtrl ,
) ;
}
2023-12-08 02:33:25 -05:00
postTraces = new Map ( core . opCallTraces ) ;
if ( res === 3 ) {
2024-01-10 17:37:25 -05:00
report = ops . op _test _op _sanitizer _report ( desc . id ) ;
2023-11-24 22:46:16 -05:00
}
2023-12-08 02:33:25 -05:00
}
2023-11-24 22:46:16 -05:00
2023-12-08 02:33:25 -05:00
if ( report === null ) return null ;
const details = [ ] ;
for ( const opReport of report ) {
const opName = opNames [ opReport . id ] ;
const diff = opReport . diff ;
if ( diff > 0 ) {
const [ name , hint ] = OP _DETAILS [ opName ] || [ opName , null ] ;
const count = diff ;
let message = ` ${ count } async operation ${
count === 1 ? "" : "s"
} to $ { name } $ {
count === 1 ? "was" : "were"
} started in this test , but never completed . ` ;
if ( hint ) {
message += ` This is often caused by not ${ hint } . ` ;
}
const traces = [ ] ;
for ( const [ id , { opName : traceOpName , stack } ] of postTraces ) {
if ( traceOpName !== opName ) continue ;
if ( MapPrototypeHas ( preTraces , id ) ) continue ;
ArrayPrototypePush ( traces , stack ) ;
2023-09-14 10:38:15 -04:00
}
2023-12-08 02:33:25 -05:00
if ( traces . length === 1 ) {
message += " The operation was started here:\n" ;
message += traces [ 0 ] ;
} else if ( traces . length > 1 ) {
message += " The operations were started here:\n" ;
message += ArrayPrototypeJoin ( traces , "\n\n" ) ;
}
ArrayPrototypePush ( details , message ) ;
} else if ( diff < 0 ) {
const [ name , hint ] = OP _DETAILS [ opName ] || [ opName , null ] ;
const count = - diff ;
let message = ` ${ count } async operation ${
count === 1 ? "" : "s"
} to $ { name } $ {
count === 1 ? "was" : "were"
} started before this test , but $ {
count === 1 ? "was" : "were"
} completed during the test . Async operations should not complete in a test if they were not started in that test . ` ;
if ( hint ) {
message += ` This is often caused by not ${ hint } . ` ;
}
const traces = [ ] ;
for ( const [ id , { opName : traceOpName , stack } ] of preTraces ) {
if ( opName !== traceOpName ) continue ;
if ( MapPrototypeHas ( postTraces , id ) ) continue ;
ArrayPrototypePush ( traces , stack ) ;
}
if ( traces . length === 1 ) {
message += " The operation was started here:\n" ;
message += traces [ 0 ] ;
} else if ( traces . length > 1 ) {
message += " The operations were started here:\n" ;
message += ArrayPrototypeJoin ( traces , "\n\n" ) ;
}
ArrayPrototypePush ( details , message ) ;
} else {
throw new Error ( "unreachable" ) ;
2021-10-11 11:00:33 -04:00
}
2023-12-08 02:33:25 -05:00
}
2023-09-16 01:48:31 -04:00
2023-12-08 02:33:25 -05:00
return {
failed : { leakedOps : [ details , core . isOpCallTracingEnabled ( ) ] } ,
2023-11-24 22:46:16 -05:00
} ;
2023-12-08 02:33:25 -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 "fetchResponse" :
return [ "A fetch response body" , "created" , "consumed" ] ;
case "httpClient" :
return [ "An HTTP client" , "created" , "closed" ] ;
case "dynamicLibrary" :
return [ "A dynamic library" , "loaded" , "unloaded" ] ;
case "httpConn" :
return [ "An inbound HTTP connection" , "accepted" , "closed" ] ;
case "httpStream" :
return [ "An inbound HTTP request" , "accepted" , "closed" ] ;
case "tcpStream" :
return [ "A TCP connection" , "opened/accepted" , "closed" ] ;
case "unixStream" :
return [ "A Unix connection" , "opened/accepted" , "closed" ] ;
case "tlsStream" :
return [ "A TLS connection" , "opened/accepted" , "closed" ] ;
case "tlsListener" :
return [ "A TLS listener" , "opened" , "closed" ] ;
case "unixListener" :
return [ "A Unix listener" , "opened" , "closed" ] ;
case "unixDatagram" :
return [ "A Unix datagram" , "opened" , "closed" ] ;
case "tcpListener" :
return [ "A TCP listener" , "opened" , "closed" ] ;
case "udpSocket" :
return [ "A UDP socket" , "opened" , "closed" ] ;
case "timer" :
return [ "A timer" , "started" , "fired/cleared" ] ;
case "textDecoder" :
return [ "A text decoder" , "created" , "finished" ] ;
case "messagePort" :
return [ "A message port" , "created" , "closed" ] ;
case "webSocketStream" :
return [ "A WebSocket" , "opened" , "closed" ] ;
case "fsEvents" :
return [ "A file system watcher" , "created" , "closed" ] ;
case "childStdin" :
return [ "A child process stdin" , "opened" , "closed" ] ;
case "childStdout" :
return [ "A child process stdout" , "opened" , "closed" ] ;
case "childStderr" :
return [ "A child process stderr" , "opened" , "closed" ] ;
case "child" :
return [ "A child process" , "started" , "closed" ] ;
case "signal" :
return [ "A signal listener" , "created" , "fired/cleared" ] ;
case "stdin" :
return [ "The stdin pipe" , "opened" , "closed" ] ;
case "stdout" :
return [ "The stdout pipe" , "opened" , "closed" ] ;
case "stderr" :
return [ "The stderr pipe" , "opened" , "closed" ] ;
case "compression" :
return [ "A CompressionStream" , "created" , "closed" ] ;
default :
return [ ` A " ${ name } " resource ` , "created" , "cleaned up" ] ;
2022-01-25 11:03:38 -05:00
}
2023-12-08 02:33:25 -05:00
}
function resourceCloseHint ( name ) {
switch ( name ) {
case "fsFile" :
return "Close the file handle by calling `file.close()`." ;
case "fetchRequest" :
return "Await the promise returned from `fetch()` or abort the fetch with an abort signal." ;
case "fetchRequestBody" :
return "Terminate the request body `ReadableStream` by closing or erroring it." ;
case "fetchResponse" :
return "Consume or close the response body `ReadableStream`, e.g `await resp.text()` or `await resp.body.cancel()`." ;
case "httpClient" :
return "Close the HTTP client by calling `httpClient.close()`." ;
case "dynamicLibrary" :
return "Unload the dynamic library by calling `dynamicLibrary.close()`." ;
case "httpConn" :
return "Close the inbound HTTP connection by calling `httpConn.close()`." ;
case "httpStream" :
return "Close the inbound HTTP request by responding with `e.respondWith()` or closing the HTTP connection." ;
case "tcpStream" :
return "Close the TCP connection by calling `tcpConn.close()`." ;
case "unixStream" :
return "Close the Unix socket connection by calling `unixConn.close()`." ;
case "tlsStream" :
return "Close the TLS connection by calling `tlsConn.close()`." ;
case "tlsListener" :
return "Close the TLS listener by calling `tlsListener.close()`." ;
case "unixListener" :
return "Close the Unix socket listener by calling `unixListener.close()`." ;
case "unixDatagram" :
return "Close the Unix datagram socket by calling `unixDatagram.close()`." ;
case "tcpListener" :
return "Close the TCP listener by calling `tcpListener.close()`." ;
case "udpSocket" :
return "Close the UDP socket by calling `udpSocket.close()`." ;
case "timer" :
return "Clear the timer by calling `clearInterval` or `clearTimeout`." ;
case "textDecoder" :
return "Close the text decoder by calling `textDecoder.decode('')` or `await textDecoderStream.readable.cancel()`." ;
case "messagePort" :
return "Close the message port by calling `messagePort.close()`." ;
case "webSocketStream" :
return "Close the WebSocket by calling `webSocket.close()`." ;
case "fsEvents" :
return "Close the file system watcher by calling `watcher.close()`." ;
case "childStdin" :
return "Close the child process stdin by calling `proc.stdin.close()`." ;
case "childStdout" :
return "Close the child process stdout by calling `proc.stdout.close()` or `await child.stdout.cancel()`." ;
case "childStderr" :
return "Close the child process stderr by calling `proc.stderr.close()` or `await child.stderr.cancel()`." ;
case "child" :
return "Close the child process by calling `proc.kill()` or `proc.close()`." ;
case "signal" :
return "Clear the signal listener by calling `Deno.removeSignalListener`." ;
case "stdin" :
return "Close the stdin pipe by calling `Deno.stdin.close()`." ;
case "stdout" :
return "Close the stdout pipe by calling `Deno.stdout.close()`." ;
case "stderr" :
return "Close the stderr pipe by calling `Deno.stderr.close()`." ;
case "compression" :
return "Close the compression stream by calling `await stream.writable.close()`." ;
default :
return "Close the resource before the end of the test." ;
2023-11-24 22:46:16 -05:00
}
2023-12-08 02:33:25 -05:00
}
2023-11-24 22:46:16 -05:00
2023-12-08 02:33:25 -05: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 ) {
/** @param desc {TestDescription | TestStepDescription} */
return async function resourceSanitizer ( desc ) {
const pre = core . resources ( ) ;
const innerResult = await fn ( desc ) ;
if ( innerResult ) return innerResult ;
const post = core . resources ( ) ;
const allResources = new Set ( [
... new SafeArrayIterator ( ObjectKeys ( pre ) ) ,
... new SafeArrayIterator ( ObjectKeys ( post ) ) ,
] ) ;
const details = [ ] ;
for ( const resource of allResources ) {
const preResource = pre [ resource ] ;
const postResource = post [ resource ] ;
if ( preResource === postResource ) continue ;
if ( preResource === undefined ) {
const [ name , action1 , action2 ] = prettyResourceNames ( postResource ) ;
const hint = resourceCloseHint ( postResource ) ;
const detail =
` ${ name } (rid ${ resource } ) was ${ action1 } during the test, but not ${ action2 } during the test. ${ hint } ` ;
ArrayPrototypePush ( details , detail ) ;
} else {
const [ name , action1 , action2 ] = prettyResourceNames ( preResource ) ;
const detail =
` ${ name } (rid ${ resource } ) was ${ action1 } before the test started, but was ${ action2 } during the test. Do not close resources in a test that were not created during that test. ` ;
ArrayPrototypePush ( details , detail ) ;
}
2023-03-25 15:32:11 -04:00
}
2023-12-08 02:33:25 -05:00
if ( details . length == 0 ) {
return null ;
}
return { failed : { leakedResources : details } } ;
} ;
}
// Wrap test function in additional assertion that makes sure
// that the test case does not accidentally exit prematurely.
function assertExit ( fn , isTest ) {
return async function exitSanitizer ( ... params ) {
setExitHandler ( ( exitCode ) => {
throw new Error (
` ${
isTest ? "Test case" : "Bench"
} attempted to exit with exit code : $ { exitCode } ` ,
) ;
} ) ;
2020-07-19 13:49:44 -04:00
2023-12-08 02:33:25 -05:00
try {
const innerResult = await fn ( ... new SafeArrayIterator ( params ) ) ;
2023-03-25 15:32:11 -04:00
if ( innerResult ) return innerResult ;
2023-12-08 02:33:25 -05:00
} finally {
setExitHandler ( null ) ;
}
} ;
}
2023-11-24 22:46:16 -05:00
2023-12-08 02:33:25 -05:00
function wrapOuter ( fn , desc ) {
return async function outerWrapped ( ) {
try {
if ( desc . ignore ) {
return "ignored" ;
2023-04-13 13:43:23 -04:00
}
2023-12-08 02:33:25 -05:00
return await fn ( desc ) ? ? "ok" ;
} catch ( error ) {
return { failed : { jsError : core . destructureError ( error ) } } ;
} finally {
const state = MapPrototypeGet ( testStates , desc . id ) ;
for ( const childDesc of state . children ) {
stepReportResult ( childDesc , { failed : "incomplete" } , 0 ) ;
2023-04-13 13:43:23 -04:00
}
2023-12-08 02:33:25 -05:00
state . completed = true ;
}
} ;
}
2023-04-13 13:43:23 -04:00
2023-12-08 02:33:25 -05:00
function wrapInner ( fn ) {
/** @param desc {TestDescription | TestStepDescription} */
return async function innerWrapped ( desc ) {
function getRunningStepDescs ( ) {
const results = [ ] ;
let childDesc = desc ;
while ( childDesc . parent != null ) {
const state = MapPrototypeGet ( testStates , childDesc . parent . id ) ;
for ( const siblingDesc of state . children ) {
if ( siblingDesc . id == childDesc . id ) {
continue ;
}
const siblingState = MapPrototypeGet ( testStates , siblingDesc . id ) ;
if ( ! siblingState . completed ) {
ArrayPrototypePush ( results , siblingDesc ) ;
2023-02-07 14:22:46 -05:00
}
2023-11-24 22:46:16 -05:00
}
2023-12-08 02:33:25 -05:00
childDesc = childDesc . parent ;
2023-09-10 08:07:00 -04:00
}
2023-12-08 02:33:25 -05:00
return results ;
}
const runningStepDescs = getRunningStepDescs ( ) ;
const runningStepDescsWithSanitizers = ArrayPrototypeFilter (
runningStepDescs ,
( d ) => usesSanitizer ( d ) ,
2023-11-24 22:46:16 -05:00
) ;
2022-06-24 06:00:53 -04:00
2023-12-08 02:33:25 -05:00
if ( runningStepDescsWithSanitizers . length > 0 ) {
return {
failed : {
overlapsWithSanitizers : runningStepDescsWithSanitizers . map (
getFullName ,
) ,
} ,
} ;
}
2023-11-24 22:46:16 -05:00
2023-12-08 02:33:25 -05:00
if ( usesSanitizer ( desc ) && runningStepDescs . length > 0 ) {
return {
failed : {
hasSanitizersAndOverlaps : runningStepDescs . map ( getFullName ) ,
} ,
} ;
}
await fn ( MapPrototypeGet ( testStates , desc . id ) . context ) ;
let failedSteps = 0 ;
for ( const childDesc of MapPrototypeGet ( testStates , desc . id ) . children ) {
const state = MapPrototypeGet ( testStates , childDesc . id ) ;
if ( ! state . completed ) {
return { failed : "incompleteSteps" } ;
2023-11-24 22:46:16 -05:00
}
2023-12-08 02:33:25 -05:00
if ( state . failed ) {
failedSteps ++ ;
2023-11-24 22:46:16 -05:00
}
2023-02-07 14:22:46 -05:00
}
2023-12-08 02:33:25 -05:00
return failedSteps == 0 ? null : { failed : { failedSteps } } ;
} ;
}
const registerTestIdRetBuf = new Uint32Array ( 1 ) ;
const registerTestIdRetBufU8 = new Uint8Array ( registerTestIdRetBuf . buffer ) ;
function testInner (
nameOrFnOrOptions ,
optionsOrFn ,
maybeFn ,
overrides = { } ,
) {
let testDesc ;
const defaults = {
ignore : false ,
only : false ,
sanitizeOps : true ,
sanitizeResources : true ,
sanitizeExit : true ,
permissions : null ,
} ;
2023-11-24 22:46:16 -05:00
2023-12-08 02:33:25 -05:00
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" ) ;
2023-11-24 22:46:16 -05:00
}
2023-12-08 02:33:25 -05:00
if ( optionsOrFn . fn != undefined ) {
throw new TypeError (
"Unexpected 'fn' field in options, test function is already provided as the third argument." ,
) ;
2023-11-24 22:46:16 -05:00
}
2023-12-08 02:33:25 -05:00
if ( optionsOrFn . name != undefined ) {
throw new TypeError (
"Unexpected 'name' field in options, test name is already provided as the first argument." ,
) ;
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
}
2022-07-15 13:09:22 -04:00
testDesc = {
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
... defaults ,
2023-12-08 02:33:25 -05:00
... optionsOrFn ,
fn : maybeFn ,
name : nameOrFnOrOptions ,
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
} ;
2023-12-08 02:33:25 -05:00
}
} 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." ,
) ;
2020-07-19 13:49:44 -04:00
}
2023-12-08 02:33:25 -05:00
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." ,
) ;
2023-11-24 22:46:16 -05:00
}
2023-12-08 02:33:25 -05:00
fn = nameOrFnOrOptions . fn ;
name = nameOrFnOrOptions . name ? ? fn . name ;
2021-07-13 18:11:02 -04:00
}
2023-12-08 02:33:25 -05:00
if ( ! name ) {
throw new TypeError ( "The test name can't be empty" ) ;
}
testDesc = { ... defaults , ... nameOrFnOrOptions , fn , name } ;
2023-02-07 14:22:46 -05:00
}
2022-07-15 13:09:22 -04:00
2023-12-08 02:33:25 -05:00
testDesc = { ... testDesc , ... overrides } ;
// Delete this prop in case the user passed it. It's used to detect steps.
delete testDesc . parent ;
testDesc . location = core . currentUserCallSite ( ) ;
testDesc . fn = wrapTest ( testDesc ) ;
testDesc . name = escapeName ( testDesc . name ) ;
const origin = ops . op _register _test (
testDesc . fn ,
testDesc . name ,
testDesc . ignore ,
testDesc . only ,
testDesc . location . fileName ,
testDesc . location . lineNumber ,
testDesc . location . columnNumber ,
registerTestIdRetBufU8 ,
) ;
testDesc . id = registerTestIdRetBuf [ 0 ] ;
testDesc . origin = origin ;
MapPrototypeSet ( testStates , testDesc . id , {
context : createTestContext ( testDesc ) ,
children : [ ] ,
completed : false ,
} ) ;
}
// Main test function provided by Deno.
function test (
nameOrFnOrOptions ,
optionsOrFn ,
maybeFn ,
) {
return testInner ( nameOrFnOrOptions , optionsOrFn , maybeFn ) ;
}
test . ignore = function ( nameOrFnOrOptions , optionsOrFn , maybeFn ) {
return testInner ( nameOrFnOrOptions , optionsOrFn , maybeFn , { ignore : true } ) ;
} ;
test . only = function (
nameOrFnOrOptions ,
optionsOrFn ,
maybeFn ,
) {
return testInner ( nameOrFnOrOptions , optionsOrFn , maybeFn , { only : true } ) ;
} ;
function getFullName ( desc ) {
if ( "parent" in desc ) {
return ` ${ getFullName ( desc . parent ) } ... ${ desc . name } ` ;
2022-07-15 13:09:22 -04:00
}
2023-12-08 02:33:25 -05:00
return desc . name ;
}
2021-10-11 09:45:02 -04:00
2023-12-08 02:33:25 -05:00
function usesSanitizer ( desc ) {
return desc . sanitizeResources || desc . sanitizeOps || desc . sanitizeExit ;
}
2021-10-11 09:45:02 -04:00
2023-12-08 02:33:25 -05:00
function stepReportResult ( desc , result , elapsed ) {
const state = MapPrototypeGet ( testStates , desc . id ) ;
for ( const childDesc of state . children ) {
stepReportResult ( childDesc , { failed : "incomplete" } , 0 ) ;
2023-02-07 14:22:46 -05:00
}
2023-12-08 02:33:25 -05:00
if ( result === "ok" ) {
ops . op _test _event _step _result _ok ( desc . id , elapsed ) ;
} else if ( result === "ignored" ) {
ops . op _test _event _step _result _ignored ( desc . id , elapsed ) ;
} else {
ops . op _test _event _step _result _failed ( desc . id , result . failed , elapsed ) ;
2023-09-25 22:21:18 -04:00
}
2023-12-08 02:33:25 -05:00
}
/** @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 ;
2021-10-11 09:45:02 -04:00
}
2023-12-08 02:33:25 -05:00
return {
[ SymbolToStringTag ] : "TestContext" ,
/ * *
* The current test name .
* /
name : desc . name ,
/ * *
* Parent test context .
* /
parent ,
/ * *
* File Uri of the test code .
* /
origin : desc . origin ,
/ * *
* @ param nameOrFnOrOptions { string | TestStepDefinition | ( ( t : TestContext ) => void | Promise < void > ) }
* @ param maybeFn { ( ( t : TestContext ) => void | Promise < void > ) | undefined }
* /
async step ( nameOrFnOrOptions , maybeFn ) {
if ( MapPrototypeGet ( testStates , desc . id ) . completed ) {
throw new Error (
"Cannot run test step after parent scope has finished execution. " +
"Ensure any `.step(...)` calls are executed before their parent scope completes execution." ,
) ;
}
2021-10-11 09:45:02 -04:00
2023-12-08 02:33:25 -05:00
let stepDesc ;
if ( typeof nameOrFnOrOptions === "string" ) {
2024-01-03 23:12:38 -05:00
if ( typeof maybeFn !== "function" ) {
2023-12-08 02:33:25 -05:00
throw new TypeError ( "Expected function for second argument." ) ;
2023-02-07 14:22:46 -05:00
}
2023-12-08 02:33:25 -05:00
stepDesc = {
name : nameOrFnOrOptions ,
fn : maybeFn ,
} ;
} else if ( typeof nameOrFnOrOptions === "function" ) {
if ( ! nameOrFnOrOptions . name ) {
throw new TypeError ( "The step function must have a name." ) ;
}
if ( maybeFn != undefined ) {
2022-07-15 13:09:22 -04:00
throw new TypeError (
2023-12-08 02:33:25 -05:00
"Unexpected second argument to TestContext.step()" ,
2022-07-15 13:09:22 -04:00
) ;
}
2023-12-08 02:33:25 -05:00
stepDesc = {
name : nameOrFnOrOptions . name ,
fn : nameOrFnOrOptions ,
2022-07-15 13:09:22 -04:00
} ;
2023-12-08 02:33:25 -05:00
} else if ( typeof nameOrFnOrOptions === "object" ) {
stepDesc = nameOrFnOrOptions ;
} else {
throw new TypeError (
"Expected a test definition or name and function." ,
2022-07-15 13:09:22 -04:00
) ;
2023-12-08 02:33:25 -05:00
}
stepDesc . ignore ? ? = false ;
stepDesc . sanitizeOps ? ? = desc . sanitizeOps ;
stepDesc . sanitizeResources ? ? = desc . sanitizeResources ;
stepDesc . sanitizeExit ? ? = desc . sanitizeExit ;
stepDesc . location = core . currentUserCallSite ( ) ;
stepDesc . level = level + 1 ;
stepDesc . parent = desc ;
stepDesc . rootId = rootId ;
stepDesc . name = escapeName ( stepDesc . name ) ;
stepDesc . rootName = escapeName ( rootName ) ;
stepDesc . fn = wrapTest ( stepDesc ) ;
const id = ops . op _register _test _step (
stepDesc . name ,
stepDesc . location . fileName ,
stepDesc . location . lineNumber ,
stepDesc . location . columnNumber ,
stepDesc . level ,
stepDesc . parent . id ,
stepDesc . rootId ,
stepDesc . rootName ,
) ;
stepDesc . id = id ;
stepDesc . origin = desc . origin ;
const state = {
context : createTestContext ( stepDesc ) ,
children : [ ] ,
failed : false ,
completed : false ,
} ;
MapPrototypeSet ( testStates , stepDesc . id , state ) ;
ArrayPrototypePush (
MapPrototypeGet ( testStates , stepDesc . parent . id ) . children ,
stepDesc ,
) ;
2021-10-11 09:45:02 -04:00
2023-12-08 02:33:25 -05:00
ops . op _test _event _step _wait ( stepDesc . id ) ;
const earlier = DateNow ( ) ;
const result = await stepDesc . fn ( stepDesc ) ;
const elapsed = DateNow ( ) - earlier ;
state . failed = ! ! result . failed ;
stepReportResult ( stepDesc , result , elapsed ) ;
return result == "ok" ;
} ,
} ;
}
/ * *
* Wrap a user test function in one which returns a structured result .
* @ template T { Function }
* @ param testFn { T }
* @ param desc { TestDescription | TestStepDescription }
* @ returns { T }
* /
function wrapTest ( desc ) {
let testFn = wrapInner ( desc . fn ) ;
if ( desc . sanitizeOps ) {
testFn = assertOps ( testFn ) ;
2023-02-07 14:22:46 -05:00
}
2023-12-08 02:33:25 -05:00
if ( desc . sanitizeResources ) {
testFn = assertResources ( testFn ) ;
}
if ( desc . sanitizeExit ) {
testFn = assertExit ( testFn , true ) ;
}
if ( ! ( "parent" in desc ) && desc . permissions ) {
testFn = withPermissions ( testFn , desc . permissions ) ;
2023-04-13 13:43:23 -04:00
}
2023-12-08 02:33:25 -05:00
return wrapOuter ( testFn , desc ) ;
}
2023-02-07 14:22:46 -05:00
2023-12-08 02:33:25 -05:00
globalThis . Deno . test = test ;