From c5c5dea90debcc5ec53b4803ca530558df32e43f Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Fri, 8 Dec 2023 13:03:25 +0530 Subject: [PATCH] chore: use primordials in 40_testing.js (#21422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit brings back usage of primordials in "40_testing.js" by turning it back into an ES module and using new "lazy loading" functionality of ES modules coming from "deno_core". The same approach was applied to "40_jupyter.js". Co-authored-by: Bartek IwaƄczuk --- Cargo.lock | 12 +- Cargo.toml | 2 +- cli/js/40_jupyter.js | 713 +++++----- cli/js/40_testing.js | 2370 +++++++++++++++++----------------- cli/worker.rs | 8 +- runtime/js/10_permissions.js | 4 +- runtime/js/30_os.js | 4 +- runtime/web_worker.rs | 7 - runtime/worker.rs | 7 - 9 files changed, 1561 insertions(+), 1566 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdfa4f61f2..a2655f232a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1065,9 +1065,9 @@ dependencies = [ [[package]] name = "deno_core" -version = "0.236.0" +version = "0.237.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea0ab6f78d50bc3c9730f3a7faa3b9b32463b25f4af3dd0f02c6a18d995047e" +checksum = "a2ea708c221abdb5734e3c4b72075379c3046eb0ac54afa0ecb5e58509cce72c" dependencies = [ "anyhow", "bytes", @@ -1493,9 +1493,9 @@ dependencies = [ [[package]] name = "deno_ops" -version = "0.112.0" +version = "0.113.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8050c4964e689fb05cac12df6c52727950b44ce48bdd6b5e4d3c0332f2e7aa76" +checksum = "e5b9c0f6360795fb625774a8b5955c87c470c43159670cf5d2052df5ce9d84bc" dependencies = [ "proc-macro-rules", "proc-macro2", @@ -4638,9 +4638,9 @@ dependencies = [ [[package]] name = "serde_v8" -version = "0.145.0" +version = "0.146.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd782806b3088c7083a142be36ceb734ccfb1da6f82b5eb84a2bff2b4a68efe" +checksum = "78309bd1ec4d14d165f271e203bdc45ad5bf45525da57bb70901f57942f6c0f7" dependencies = [ "bytes", "derive_more", diff --git a/Cargo.toml b/Cargo.toml index 2e8ee9c979..c414203012 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ repository = "https://github.com/denoland/deno" [workspace.dependencies] deno_ast = { version = "0.31.6", features = ["transpiling"] } -deno_core = { version = "0.236.0" } +deno_core = { version = "0.237.0" } deno_runtime = { version = "0.135.0", path = "./runtime" } napi_sym = { version = "0.57.0", path = "./cli/napi/sym" } diff --git a/cli/js/40_jupyter.js b/cli/js/40_jupyter.js index bef7f4056c..272e4c9788 100644 --- a/cli/js/40_jupyter.js +++ b/cli/js/40_jupyter.js @@ -36,215 +36,181 @@ * }, { raw: true }); * ``` */ -(() => { - const internals = Deno[Deno.internal]; - const core = internals.core; +import { core, internals } from "ext:core/mod.js"; - const $display = Symbol.for("Jupyter.display"); +const $display = Symbol.for("Jupyter.display"); - /** Escape copied from https://deno.land/std@0.192.0/html/entities.ts */ - const rawToEntityEntries = [ - ["&", "&"], - ["<", "<"], - [">", ">"], - ['"', """], - ["'", "'"], - ]; +/** Escape copied from https://deno.land/std@0.192.0/html/entities.ts */ +const rawToEntityEntries = [ + ["&", "&"], + ["<", "<"], + [">", ">"], + ['"', """], + ["'", "'"], +]; - const rawToEntity = new Map(rawToEntityEntries); +const rawToEntity = new Map(rawToEntityEntries); - const rawRe = new RegExp(`[${[...rawToEntity.keys()].join("")}]`, "g"); +const rawRe = new RegExp(`[${[...rawToEntity.keys()].join("")}]`, "g"); - function escapeHTML(str) { - return str.replaceAll( - rawRe, - (m) => rawToEntity.has(m) ? rawToEntity.get(m) : m, - ); +function escapeHTML(str) { + return str.replaceAll( + rawRe, + (m) => rawToEntity.has(m) ? rawToEntity.get(m) : m, + ); +} + +/** Duck typing our way to common visualization and tabular libraries */ +/** Vegalite */ +function isVegaLike(obj) { + return obj !== null && typeof obj === "object" && "toSpec" in obj; +} +function extractVega(obj) { + const spec = obj.toSpec(); + if (!("$schema" in spec)) { + return null; } + if (typeof spec !== "object") { + return null; + } + let mediaType = "application/vnd.vega.v5+json"; + if (spec.$schema === "https://vega.github.io/schema/vega-lite/v4.json") { + mediaType = "application/vnd.vegalite.v4+json"; + } else if ( + spec.$schema === "https://vega.github.io/schema/vega-lite/v5.json" + ) { + mediaType = "application/vnd.vegalite.v5+json"; + } + return { + [mediaType]: spec, + }; +} +/** Polars */ +function isDataFrameLike(obj) { + const isObject = obj !== null && typeof obj === "object"; + if (!isObject) { + return false; + } + const df = obj; + return df.schema !== void 0 && typeof df.schema === "object" && + df.head !== void 0 && typeof df.head === "function" && + df.toRecords !== void 0 && typeof df.toRecords === "function"; +} +/** + * Map Polars DataType to JSON Schema data types. + * @param dataType - The Polars DataType. + * @returns The corresponding JSON Schema data type. + */ +function mapPolarsTypeToJSONSchema(colType) { + const typeMapping = { + Null: "null", + Bool: "boolean", + Int8: "integer", + Int16: "integer", + Int32: "integer", + Int64: "integer", + UInt8: "integer", + UInt16: "integer", + UInt32: "integer", + UInt64: "integer", + Float32: "number", + Float64: "number", + Date: "string", + Datetime: "string", + Utf8: "string", + Categorical: "string", + List: "array", + Struct: "object", + }; + // These colTypes are weird. When you console.dir or console.log them + // they show a `DataType` field, however you can't access it directly until you + // convert it to JSON + const dataType = colType.toJSON()["DataType"]; + return typeMapping[dataType] || "string"; +} - /** Duck typing our way to common visualization and tabular libraries */ - /** Vegalite */ - function isVegaLike(obj) { - return obj !== null && typeof obj === "object" && "toSpec" in obj; - } - function extractVega(obj) { - const spec = obj.toSpec(); - if (!("$schema" in spec)) { - return null; - } - if (typeof spec !== "object") { - return null; - } - let mediaType = "application/vnd.vega.v5+json"; - if (spec.$schema === "https://vega.github.io/schema/vega-lite/v4.json") { - mediaType = "application/vnd.vegalite.v4+json"; - } else if ( - spec.$schema === "https://vega.github.io/schema/vega-lite/v5.json" - ) { - mediaType = "application/vnd.vegalite.v5+json"; - } - return { - [mediaType]: spec, - }; - } - /** Polars */ - function isDataFrameLike(obj) { - const isObject = obj !== null && typeof obj === "object"; - if (!isObject) { - return false; - } - const df = obj; - return df.schema !== void 0 && typeof df.schema === "object" && - df.head !== void 0 && typeof df.head === "function" && - df.toRecords !== void 0 && typeof df.toRecords === "function"; - } - /** - * Map Polars DataType to JSON Schema data types. - * @param dataType - The Polars DataType. - * @returns The corresponding JSON Schema data type. - */ - function mapPolarsTypeToJSONSchema(colType) { - const typeMapping = { - Null: "null", - Bool: "boolean", - Int8: "integer", - Int16: "integer", - Int32: "integer", - Int64: "integer", - UInt8: "integer", - UInt16: "integer", - UInt32: "integer", - UInt64: "integer", - Float32: "number", - Float64: "number", - Date: "string", - Datetime: "string", - Utf8: "string", - Categorical: "string", - List: "array", - Struct: "object", - }; - // These colTypes are weird. When you console.dir or console.log them - // they show a `DataType` field, however you can't access it directly until you - // convert it to JSON - const dataType = colType.toJSON()["DataType"]; - return typeMapping[dataType] || "string"; +function extractDataFrame(df) { + const fields = []; + const schema = { + fields, + }; + let data = []; + // Convert DataFrame schema to Tabular DataResource schema + for (const [colName, colType] of Object.entries(df.schema)) { + const dataType = mapPolarsTypeToJSONSchema(colType); + schema.fields.push({ + name: colName, + type: dataType, + }); } + // Convert DataFrame data to row-oriented JSON + // + // TODO(rgbkrk): Determine how to get the polars format max rows + // Since pl.setTblRows just sets env var POLARS_FMT_MAX_ROWS, + // we probably just have to pick a number for now. + // - function extractDataFrame(df) { - const fields = []; - const schema = { - fields, - }; - let data = []; - // Convert DataFrame schema to Tabular DataResource schema - for (const [colName, colType] of Object.entries(df.schema)) { - const dataType = mapPolarsTypeToJSONSchema(colType); - schema.fields.push({ - name: colName, - type: dataType, - }); - } - // Convert DataFrame data to row-oriented JSON - // - // TODO(rgbkrk): Determine how to get the polars format max rows - // Since pl.setTblRows just sets env var POLARS_FMT_MAX_ROWS, - // we probably just have to pick a number for now. - // - - data = df.head(50).toRecords(); - let htmlTable = ""; - htmlTable += ""; + data = df.head(50).toRecords(); + let htmlTable = "
"; + htmlTable += ""; + schema.fields.forEach((field) => { + htmlTable += ``; + }); + htmlTable += ""; + htmlTable += ""; + df.head(10).toRecords().forEach((row) => { + htmlTable += ""; schema.fields.forEach((field) => { - htmlTable += ``; + htmlTable += ``; }); - htmlTable += ""; - htmlTable += ""; - df.head(10).toRecords().forEach((row) => { - htmlTable += ""; - schema.fields.forEach((field) => { - htmlTable += ``; - }); - htmlTable += ""; - }); - htmlTable += "
${escapeHTML(String(field.name))}
${escapeHTML(String(field.name))}${escapeHTML(String(row[field.name]))}
${escapeHTML(String(row[field.name]))}
"; - return { - "application/vnd.dataresource+json": { data, schema }, - "text/html": htmlTable, - }; + htmlTable += ""; + }); + htmlTable += ""; + return { + "application/vnd.dataresource+json": { data, schema }, + "text/html": htmlTable, + }; +} + +/** Canvas */ +function isCanvasLike(obj) { + return obj !== null && typeof obj === "object" && "toDataURL" in obj; +} + +/** Possible HTML and SVG Elements */ +function isSVGElementLike(obj) { + return obj !== null && typeof obj === "object" && "outerHTML" in obj && + typeof obj.outerHTML === "string" && obj.outerHTML.startsWith(" obj, + }; +} + +/** + * Format an object for displaying in Deno + * + * @param obj - The object to be displayed + * @returns MediaBundle + */ +async function format(obj) { + if (hasDisplaySymbol(obj)) { + return await obj[$display](); } - - /** Canvas */ - function isCanvasLike(obj) { - return obj !== null && typeof obj === "object" && "toDataURL" in obj; - } - - /** Possible HTML and SVG Elements */ - function isSVGElementLike(obj) { - return obj !== null && typeof obj === "object" && "outerHTML" in obj && - typeof obj.outerHTML === "string" && obj.outerHTML.startsWith(" obj, - }; - } - - /** - * Format an object for displaying in Deno - * - * @param obj - The object to be displayed - * @returns MediaBundle - */ - async function format(obj) { - if (hasDisplaySymbol(obj)) { - return await obj[$display](); - } - if (typeof obj !== "object") { - return { - "text/plain": Deno[Deno.internal].inspectArgs(["%o", obj], { - colors: !Deno.noColor, - }), - }; - } - - if (isCanvasLike(obj)) { - const dataURL = obj.toDataURL(); - const parts = dataURL.split(","); - const mime = parts[0].split(":")[1].split(";")[0]; - const data = parts[1]; - return { - [mime]: data, - }; - } - if (isVegaLike(obj)) { - return extractVega(obj); - } - if (isDataFrameLike(obj)) { - return extractDataFrame(obj); - } - if (isSVGElementLike(obj)) { - return { - "image/svg+xml": obj.outerHTML, - }; - } - if (isHTMLElementLike(obj)) { - return { - "text/html": obj.outerHTML, - }; - } + if (typeof obj !== "object") { return { "text/plain": Deno[Deno.internal].inspectArgs(["%o", obj], { colors: !Deno.noColor, @@ -252,180 +218,211 @@ }; } - /** - * This function creates a tagged template function for a given media type. - * The tagged template function takes a template string and returns a displayable object. - * - * @param mediatype - The media type for the tagged template function. - * @returns A function that takes a template string and returns a displayable object. - */ - function createTaggedTemplateDisplayable(mediatype) { - return (strings, ...values) => { - const payload = strings.reduce( - (acc, string, i) => - acc + string + (values[i] !== undefined ? values[i] : ""), - "", - ); - return makeDisplayable({ [mediatype]: payload }); + if (isCanvasLike(obj)) { + const dataURL = obj.toDataURL(); + const parts = dataURL.split(","); + const mime = parts[0].split(":")[1].split(";")[0]; + const data = parts[1]; + return { + [mime]: data, }; } + if (isVegaLike(obj)) { + return extractVega(obj); + } + if (isDataFrameLike(obj)) { + return extractDataFrame(obj); + } + if (isSVGElementLike(obj)) { + return { + "image/svg+xml": obj.outerHTML, + }; + } + if (isHTMLElementLike(obj)) { + return { + "text/html": obj.outerHTML, + }; + } + return { + "text/plain": Deno[Deno.internal].inspectArgs(["%o", obj], { + colors: !Deno.noColor, + }), + }; +} - /** - * Show Markdown in Jupyter frontends with a tagged template function. - * - * Takes a template string and returns a displayable object for Jupyter frontends. - * - * @example - * Create a Markdown view. - * - * ```typescript - * md`# Notebooks in TypeScript via Deno ![Deno logo](https://github.com/denoland.png?size=32) - * - * * TypeScript ${Deno.version.typescript} - * * V8 ${Deno.version.v8} - * * Deno ${Deno.version.deno} - * - * Interactive compute with Jupyter _built into Deno_! - * ` - * ``` - */ - const md = createTaggedTemplateDisplayable("text/markdown"); +/** + * This function creates a tagged template function for a given media type. + * The tagged template function takes a template string and returns a displayable object. + * + * @param mediatype - The media type for the tagged template function. + * @returns A function that takes a template string and returns a displayable object. + */ +function createTaggedTemplateDisplayable(mediatype) { + return (strings, ...values) => { + const payload = strings.reduce( + (acc, string, i) => + acc + string + (values[i] !== undefined ? values[i] : ""), + "", + ); + return makeDisplayable({ [mediatype]: payload }); + }; +} - /** - * Show HTML in Jupyter frontends with a tagged template function. - * - * Takes a template string and returns a displayable object for Jupyter frontends. - * - * @example - * Create an HTML view. - * ```typescript - * html`

Hello, world!

` - * ``` - */ - const html = createTaggedTemplateDisplayable("text/html"); - /** - * SVG Tagged Template Function. - * - * Takes a template string and returns a displayable object for Jupyter frontends. - * - * Example usage: - * - * svg` - * - * ` - */ - const svg = createTaggedTemplateDisplayable("image/svg+xml"); +/** + * Show Markdown in Jupyter frontends with a tagged template function. + * + * Takes a template string and returns a displayable object for Jupyter frontends. + * + * @example + * Create a Markdown view. + * + * ```typescript + * md`# Notebooks in TypeScript via Deno ![Deno logo](https://github.com/denoland.png?size=32) + * + * * TypeScript ${Deno.version.typescript} + * * V8 ${Deno.version.v8} + * * Deno ${Deno.version.deno} + * + * Interactive compute with Jupyter _built into Deno_! + * ` + * ``` + */ +const md = createTaggedTemplateDisplayable("text/markdown"); - function isMediaBundle(obj) { - if (obj == null || typeof obj !== "object" || Array.isArray(obj)) { +/** + * Show HTML in Jupyter frontends with a tagged template function. + * + * Takes a template string and returns a displayable object for Jupyter frontends. + * + * @example + * Create an HTML view. + * ```typescript + * html`

Hello, world!

` + * ``` + */ +const html = createTaggedTemplateDisplayable("text/html"); +/** + * SVG Tagged Template Function. + * + * Takes a template string and returns a displayable object for Jupyter frontends. + * + * Example usage: + * + * svg` + * + * ` + */ +const svg = createTaggedTemplateDisplayable("image/svg+xml"); + +function isMediaBundle(obj) { + if (obj == null || typeof obj !== "object" || Array.isArray(obj)) { + return false; + } + for (const key in obj) { + if (typeof key !== "string") { return false; } - for (const key in obj) { - if (typeof key !== "string") { - return false; - } - } - return true; + } + return true; +} + +async function formatInner(obj, raw) { + if (raw && isMediaBundle(obj)) { + return obj; + } else { + return await format(obj); + } +} + +internals.jupyter = { formatInner }; + +function enableJupyter() { + const { + op_jupyter_broadcast, + } = core.ensureFastOps(); + + async function broadcast( + msgType, + content, + { metadata = {}, buffers = [] } = {}, + ) { + await op_jupyter_broadcast(msgType, content, metadata, buffers); } - async function formatInner(obj, raw) { - if (raw && isMediaBundle(obj)) { - return obj; - } else { - return await format(obj); - } - } - - internals.jupyter = { formatInner }; - - function enableJupyter() { - const { - op_jupyter_broadcast, - } = core.ensureFastOps(); - - async function broadcast( - msgType, - content, - { metadata = {}, buffers = [] } = {}, - ) { - await op_jupyter_broadcast(msgType, content, metadata, buffers); - } - - async function broadcastResult(executionCount, result) { - try { - if (result === undefined) { - return; - } - - const data = await format(result); - await broadcast("execute_result", { - execution_count: executionCount, - data, - metadata: {}, - }); - } catch (err) { - if (err instanceof Error) { - const stack = err.stack || ""; - await broadcast("error", { - ename: err.name, - evalue: err.message, - traceback: stack.split("\n"), - }); - } else if (typeof err == "string") { - await broadcast("error", { - ename: "Error", - evalue: err, - traceback: [], - }); - } else { - await broadcast("error", { - ename: "Error", - evalue: - "An error occurred while formatting a result, but it could not be identified", - traceback: [], - }); - } + async function broadcastResult(executionCount, result) { + try { + if (result === undefined) { + return; } - } - internals.jupyter.broadcastResult = broadcastResult; - - /** - * Display function for Jupyter Deno Kernel. - * Mimics the behavior of IPython's `display(obj, raw=True)` function to allow - * asynchronous displaying of objects in Jupyter. - * - * @param obj - The object to be displayed - * @param options - Display options - */ - async function display(obj, options = { raw: false, update: false }) { - const bundle = await formatInner(obj, options.raw); - let messageType = "display_data"; - if (options.update) { - messageType = "update_display_data"; - } - let transient = {}; - if (options.display_id) { - transient = { display_id: options.display_id }; - } - await broadcast(messageType, { - data: bundle, + const data = await format(result); + await broadcast("execute_result", { + execution_count: executionCount, + data, metadata: {}, - transient, }); - return; + } catch (err) { + if (err instanceof Error) { + const stack = err.stack || ""; + await broadcast("error", { + ename: err.name, + evalue: err.message, + traceback: stack.split("\n"), + }); + } else if (typeof err == "string") { + await broadcast("error", { + ename: "Error", + evalue: err, + traceback: [], + }); + } else { + await broadcast("error", { + ename: "Error", + evalue: + "An error occurred while formatting a result, but it could not be identified", + traceback: [], + }); + } } - - globalThis.Deno.jupyter = { - broadcast, - display, - format, - md, - html, - svg, - $display, - }; } - internals.enableJupyter = enableJupyter; -})(); + internals.jupyter.broadcastResult = broadcastResult; + + /** + * Display function for Jupyter Deno Kernel. + * Mimics the behavior of IPython's `display(obj, raw=True)` function to allow + * asynchronous displaying of objects in Jupyter. + * + * @param obj - The object to be displayed + * @param options - Display options + */ + async function display(obj, options = { raw: false, update: false }) { + const bundle = await formatInner(obj, options.raw); + let messageType = "display_data"; + if (options.update) { + messageType = "update_display_data"; + } + let transient = {}; + if (options.display_id) { + transient = { display_id: options.display_id }; + } + await broadcast(messageType, { + data: bundle, + metadata: {}, + transient, + }); + return; + } + + globalThis.Deno.jupyter = { + broadcast, + display, + format, + md, + html, + svg, + $display, + }; +} + +internals.enableJupyter = enableJupyter; diff --git a/cli/js/40_testing.js b/cli/js/40_testing.js index f878d59a43..5b51ac1692 100644 --- a/cli/js/40_testing.js +++ b/cli/js/40_testing.js @@ -1,67 +1,84 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -// Do not use primordials because we do not want to depend on the __bootstrap -// namespace. -// // deno-lint-ignore-file -(() => { - const internals = Deno[Deno.internal]; - const core = internals.core; - const ops = core.ops; - const { - setExitHandler, - Console, - serializePermissions, - } = internals; +import { core, internals, primordials } from "ext:core/mod.js"; +const ops = core.ops; - const opSanitizerDelayResolveQueue = []; - let hasSetOpSanitizerDelayMacrotask = false; +import { setExitHandler } from "ext:runtime/30_os.js"; +import { Console } from "ext:deno_console/01_console.js"; +import { serializePermissions } from "ext:runtime/10_permissions.js"; +import { setTimeout } from "ext:deno_web/02_timers.js"; - // Even if every resource is closed by the end of a test, there can be a delay - // until the pending ops have all finished. This function returns a promise - // that resolves when it's (probably) fine to run the op sanitizer. - // - // This is implemented by adding a macrotask callback that runs after the - // all ready async ops resolve, and the timer macrotask. Using just a macrotask - // callback without delaying is sufficient, because when the macrotask callback - // runs after async op dispatch, we know that all async ops that can currently - // return `Poll::Ready` have done so, and have been dispatched to JS. - // - // Worker ops are an exception to this, because there is no way for the user to - // await shutdown of the worker from the thread calling `worker.terminate()`. - // Because of this, we give extra leeway for worker ops to complete, by waiting - // for a whole millisecond if there are pending worker ops. - function opSanitizerDelay(hasPendingWorkerOps) { - if (!hasSetOpSanitizerDelayMacrotask) { - core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask); - hasSetOpSanitizerDelayMacrotask = true; - } - const p = new Promise((resolve) => { - // Schedule an async op to complete immediately to ensure the macrotask is - // run. We rely on the fact that enqueueing the resolver callback during the - // timeout callback will mean that the resolver gets called in the same - // event loop tick as the timeout callback. - setTimeout(() => { - opSanitizerDelayResolveQueue.push(resolve); - }, hasPendingWorkerOps ? 1 : 0); - }); - return p; +const { + ArrayPrototypeFilter, + ArrayPrototypeJoin, + ArrayPrototypePush, + ArrayPrototypeShift, + DateNow, + Error, + FunctionPrototype, + Map, + MapPrototypeGet, + MapPrototypeHas, + MapPrototypeSet, + MathCeil, + ObjectKeys, + ObjectPrototypeIsPrototypeOf, + Promise, + SafeArrayIterator, + Set, + StringPrototypeReplaceAll, + SymbolToStringTag, + TypeError, +} = primordials; + +const opSanitizerDelayResolveQueue = []; +let hasSetOpSanitizerDelayMacrotask = false; + +// Even if every resource is closed by the end of a test, there can be a delay +// until the pending ops have all finished. This function returns a promise +// that resolves when it's (probably) fine to run the op sanitizer. +// +// This is implemented by adding a macrotask callback that runs after the +// all ready async ops resolve, and the timer macrotask. Using just a macrotask +// callback without delaying is sufficient, because when the macrotask callback +// runs after async op dispatch, we know that all async ops that can currently +// return `Poll::Ready` have done so, and have been dispatched to JS. +// +// Worker ops are an exception to this, because there is no way for the user to +// await shutdown of the worker from the thread calling `worker.terminate()`. +// Because of this, we give extra leeway for worker ops to complete, by waiting +// for a whole millisecond if there are pending worker ops. +function opSanitizerDelay(hasPendingWorkerOps) { + if (!hasSetOpSanitizerDelayMacrotask) { + core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask); + hasSetOpSanitizerDelayMacrotask = true; } + const p = new Promise((resolve) => { + // Schedule an async op to complete immediately to ensure the macrotask is + // run. We rely on the fact that enqueueing the resolver callback during the + // timeout callback will mean that the resolver gets called in the same + // event loop tick as the timeout callback. + setTimeout(() => { + ArrayPrototypePush(opSanitizerDelayResolveQueue, resolve); + }, hasPendingWorkerOps ? 1 : 0); + }); + return p; +} - function handleOpSanitizerDelayMacrotask() { - const resolve = opSanitizerDelayResolveQueue.shift(); - if (resolve) { - resolve(); - return opSanitizerDelayResolveQueue.length === 0; - } - return undefined; // we performed no work, so can skip microtasks checkpoint +function handleOpSanitizerDelayMacrotask() { + const resolve = ArrayPrototypeShift(opSanitizerDelayResolveQueue); + if (resolve) { + resolve(); + return opSanitizerDelayResolveQueue.length === 0; } + return undefined; // we performed no work, so can skip microtasks checkpoint +} - // An async operation to $0 was started in this test, but never completed. This is often caused by not $1. - // An async operation to $0 was started in this test, but never completed. Async operations should not complete in a test if they were not started in that test. - // deno-fmt-ignore - const OP_DETAILS = { +// 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"], @@ -135,840 +152,888 @@ "op_ws_send_pong": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"], }; - let opIdHostRecvMessage = -1; - let opIdHostRecvCtrl = -1; - let opNames = null; +let opIdHostRecvMessage = -1; +let opIdHostRecvCtrl = -1; +let opNames = null; - function populateOpNames() { - opNames = core.ops.op_op_names(); - opIdHostRecvMessage = opNames.indexOf("op_host_recv_message"); - opIdHostRecvCtrl = opNames.indexOf("op_host_recv_ctrl"); - } +function populateOpNames() { + opNames = core.ops.op_op_names(); + opIdHostRecvMessage = opNames.indexOf("op_host_recv_message"); + opIdHostRecvCtrl = opNames.indexOf("op_host_recv_ctrl"); +} - // Wrap test function in additional assertion that makes sure - // the test case does not leak async "ops" - ie. number of async - // completed ops after the test is the same as number of dispatched - // ops. Note that "unref" ops are ignored since in nature that are - // optional. - function assertOps(fn) { - /** @param desc {TestDescription | TestStepDescription} */ - return async function asyncOpSanitizer(desc) { - if (opNames === null) populateOpNames(); - const res = core.ops.op_test_op_sanitizer_collect( +// Wrap test function in additional assertion that makes sure +// the test case does not leak async "ops" - ie. number of async +// completed ops after the test is the same as number of dispatched +// ops. Note that "unref" ops are ignored since in nature that are +// optional. +function assertOps(fn) { + /** @param desc {TestDescription | TestStepDescription} */ + return async function asyncOpSanitizer(desc) { + if (opNames === null) populateOpNames(); + const res = core.ops.op_test_op_sanitizer_collect( + desc.id, + false, + opIdHostRecvMessage, + opIdHostRecvCtrl, + ); + if (res !== 0) { + await opSanitizerDelay(res === 2); + core.ops.op_test_op_sanitizer_collect( + desc.id, + true, + opIdHostRecvMessage, + opIdHostRecvCtrl, + ); + } + const preTraces = new Map(core.opCallTraces); + let postTraces; + let report = null; + + try { + const innerResult = await fn(desc); + if (innerResult) return innerResult; + } finally { + let res = core.ops.op_test_op_sanitizer_finish( desc.id, false, opIdHostRecvMessage, opIdHostRecvCtrl, ); - if (res !== 0) { + if (res === 1 || res === 2) { await opSanitizerDelay(res === 2); - core.ops.op_test_op_sanitizer_collect( + res = core.ops.op_test_op_sanitizer_finish( desc.id, true, opIdHostRecvMessage, opIdHostRecvCtrl, ); } - const preTraces = new Map(core.opCallTraces); - let postTraces; - let report = null; - - try { - const innerResult = await fn(desc); - if (innerResult) return innerResult; - } finally { - let res = core.ops.op_test_op_sanitizer_finish( - desc.id, - false, - opIdHostRecvMessage, - opIdHostRecvCtrl, - ); - if (res === 1 || res === 2) { - await opSanitizerDelay(res === 2); - res = core.ops.op_test_op_sanitizer_finish( - desc.id, - true, - opIdHostRecvMessage, - opIdHostRecvCtrl, - ); - } - postTraces = new Map(core.opCallTraces); - if (res === 3) { - report = core.ops.op_test_op_sanitizer_report(desc.id); - } - } - - if (report === null) return null; - - const details = []; - for (const opReport of report) { - const opName = opNames[opReport.id]; - const diff = opReport.diff; - - if (diff > 0) { - const [name, hint] = OP_DETAILS[opName] || [opName, null]; - const count = diff; - let message = `${count} async operation${ - count === 1 ? "" : "s" - } to ${name} ${ - count === 1 ? "was" : "were" - } started in this test, but never completed.`; - if (hint) { - message += ` This is often caused by not ${hint}.`; - } - const traces = []; - for (const [id, { opName: traceOpName, stack }] of postTraces) { - if (traceOpName !== opName) continue; - if (preTraces.has(id)) continue; - traces.push(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 += traces.join("\n\n"); - } - details.push(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 (postTraces.has(id)) continue; - traces.push(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 += traces.join("\n\n"); - } - details.push(message); - } else { - throw new Error("unreachable"); - } - } - - return { - failed: { leakedOps: [details, core.isOpCallTracingEnabled()] }, - }; - }; - } - - function prettyResourceNames(name) { - switch (name) { - case "fsFile": - return ["A file", "opened", "closed"]; - case "fetchRequest": - return ["A fetch request", "started", "finished"]; - case "fetchRequestBody": - return ["A fetch request body", "created", "closed"]; - case "fetchResponse": - return ["A fetch response body", "created", "consumed"]; - case "httpClient": - return ["An HTTP client", "created", "closed"]; - case "dynamicLibrary": - return ["A dynamic library", "loaded", "unloaded"]; - case "httpConn": - return ["An inbound HTTP connection", "accepted", "closed"]; - case "httpStream": - return ["An inbound HTTP request", "accepted", "closed"]; - case "tcpStream": - return ["A TCP connection", "opened/accepted", "closed"]; - case "unixStream": - return ["A Unix connection", "opened/accepted", "closed"]; - case "tlsStream": - return ["A TLS connection", "opened/accepted", "closed"]; - case "tlsListener": - return ["A TLS listener", "opened", "closed"]; - case "unixListener": - return ["A Unix listener", "opened", "closed"]; - case "unixDatagram": - return ["A Unix datagram", "opened", "closed"]; - case "tcpListener": - return ["A TCP listener", "opened", "closed"]; - case "udpSocket": - return ["A UDP socket", "opened", "closed"]; - case "timer": - return ["A timer", "started", "fired/cleared"]; - case "textDecoder": - return ["A text decoder", "created", "finished"]; - case "messagePort": - return ["A message port", "created", "closed"]; - case "webSocketStream": - return ["A WebSocket", "opened", "closed"]; - case "fsEvents": - return ["A file system watcher", "created", "closed"]; - case "childStdin": - return ["A child process stdin", "opened", "closed"]; - case "childStdout": - return ["A child process stdout", "opened", "closed"]; - case "childStderr": - return ["A child process stderr", "opened", "closed"]; - case "child": - return ["A child process", "started", "closed"]; - case "signal": - return ["A signal listener", "created", "fired/cleared"]; - case "stdin": - return ["The stdin pipe", "opened", "closed"]; - case "stdout": - return ["The stdout pipe", "opened", "closed"]; - case "stderr": - return ["The stderr pipe", "opened", "closed"]; - case "compression": - return ["A CompressionStream", "created", "closed"]; - default: - return [`A "${name}" resource`, "created", "cleaned up"]; - } - } - - function resourceCloseHint(name) { - switch (name) { - case "fsFile": - return "Close the file handle by calling `file.close()`."; - case "fetchRequest": - return "Await the promise returned from `fetch()` or abort the fetch with an abort signal."; - case "fetchRequestBody": - return "Terminate the request body `ReadableStream` by closing or erroring it."; - case "fetchResponse": - return "Consume or close the response body `ReadableStream`, e.g `await resp.text()` or `await resp.body.cancel()`."; - case "httpClient": - return "Close the HTTP client by calling `httpClient.close()`."; - case "dynamicLibrary": - return "Unload the dynamic library by calling `dynamicLibrary.close()`."; - case "httpConn": - return "Close the inbound HTTP connection by calling `httpConn.close()`."; - case "httpStream": - return "Close the inbound HTTP request by responding with `e.respondWith()` or closing the HTTP connection."; - case "tcpStream": - return "Close the TCP connection by calling `tcpConn.close()`."; - case "unixStream": - return "Close the Unix socket connection by calling `unixConn.close()`."; - case "tlsStream": - return "Close the TLS connection by calling `tlsConn.close()`."; - case "tlsListener": - return "Close the TLS listener by calling `tlsListener.close()`."; - case "unixListener": - return "Close the Unix socket listener by calling `unixListener.close()`."; - case "unixDatagram": - return "Close the Unix datagram socket by calling `unixDatagram.close()`."; - case "tcpListener": - return "Close the TCP listener by calling `tcpListener.close()`."; - case "udpSocket": - return "Close the UDP socket by calling `udpSocket.close()`."; - case "timer": - return "Clear the timer by calling `clearInterval` or `clearTimeout`."; - case "textDecoder": - return "Close the text decoder by calling `textDecoder.decode('')` or `await textDecoderStream.readable.cancel()`."; - case "messagePort": - return "Close the message port by calling `messagePort.close()`."; - case "webSocketStream": - return "Close the WebSocket by calling `webSocket.close()`."; - case "fsEvents": - return "Close the file system watcher by calling `watcher.close()`."; - case "childStdin": - return "Close the child process stdin by calling `proc.stdin.close()`."; - case "childStdout": - return "Close the child process stdout by calling `proc.stdout.close()` or `await child.stdout.cancel()`."; - case "childStderr": - return "Close the child process stderr by calling `proc.stderr.close()` or `await child.stderr.cancel()`."; - case "child": - return "Close the child process by calling `proc.kill()` or `proc.close()`."; - case "signal": - return "Clear the signal listener by calling `Deno.removeSignalListener`."; - case "stdin": - return "Close the stdin pipe by calling `Deno.stdin.close()`."; - case "stdout": - return "Close the stdout pipe by calling `Deno.stdout.close()`."; - case "stderr": - return "Close the stderr pipe by calling `Deno.stderr.close()`."; - case "compression": - return "Close the compression stream by calling `await stream.writable.close()`."; - default: - return "Close the resource before the end of the test."; - } - } - - // Wrap test function in additional assertion that makes sure - // the test case does not "leak" resources - ie. resource table after - // the test has exactly the same contents as before the test. - function assertResources(fn) { - /** @param desc {TestDescription | TestStepDescription} */ - return async function resourceSanitizer(desc) { - const pre = core.resources(); - const innerResult = await fn(desc); - if (innerResult) return innerResult; - const post = core.resources(); - - const allResources = new Set([ - ...Object.keys(pre), - ...Object.keys(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}`; - 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); - } - } - if (details.length == 0) { - return null; - } - return { failed: { leakedResources: details } }; - }; - } - - // Wrap test function in additional assertion that makes sure - // that the test case does not accidentally exit prematurely. - function assertExit(fn, isTest) { - return async function exitSanitizer(...params) { - setExitHandler((exitCode) => { - throw new Error( - `${ - isTest ? "Test case" : "Bench" - } attempted to exit with exit code: ${exitCode}`, - ); - }); - - try { - const innerResult = await fn(...params); - if (innerResult) return innerResult; - } finally { - setExitHandler(null); - } - }; - } - - function wrapOuter(fn, desc) { - return async function outerWrapped() { - try { - if (desc.ignore) { - return "ignored"; - } - return await fn(desc) ?? "ok"; - } catch (error) { - return { failed: { jsError: core.destructureError(error) } }; - } finally { - const state = testStates.get(desc.id); - for (const childDesc of state.children) { - stepReportResult(childDesc, { failed: "incomplete" }, 0); - } - state.completed = true; - } - }; - } - - function wrapInner(fn) { - /** @param desc {TestDescription | TestStepDescription} */ - return async function innerWrapped(desc) { - function getRunningStepDescs() { - const results = []; - let childDesc = desc; - while (childDesc.parent != null) { - const state = testStates.get(childDesc.parent.id); - for (const siblingDesc of state.children) { - if (siblingDesc.id == childDesc.id) { - continue; - } - const siblingState = testStates.get(siblingDesc.id); - if (!siblingState.completed) { - results.push(siblingDesc); - } - } - childDesc = childDesc.parent; - } - return results; - } - const runningStepDescs = getRunningStepDescs(); - const runningStepDescsWithSanitizers = runningStepDescs.filter( - (d) => usesSanitizer(d), - ); - - if (runningStepDescsWithSanitizers.length > 0) { - return { - failed: { - overlapsWithSanitizers: runningStepDescsWithSanitizers.map( - getFullName, - ), - }, - }; - } - - if (usesSanitizer(desc) && runningStepDescs.length > 0) { - return { - failed: { - hasSanitizersAndOverlaps: runningStepDescs.map(getFullName), - }, - }; - } - await fn(testStates.get(desc.id).context); - let failedSteps = 0; - for (const childDesc of testStates.get(desc.id).children) { - const state = testStates.get(childDesc.id); - if (!state.completed) { - return { failed: "incompleteSteps" }; - } - if (state.failed) { - failedSteps++; - } - } - return failedSteps == 0 ? null : { failed: { failedSteps } }; - }; - } - - function pledgePermissions(permissions) { - return ops.op_pledge_test_permissions( - serializePermissions(permissions), - ); - } - - function restorePermissions(token) { - ops.op_restore_test_permissions(token); - } - - function withPermissions(fn, permissions) { - return async function applyPermissions(...params) { - const token = pledgePermissions(permissions); - - try { - return await fn(...params); - } finally { - restorePermissions(token); - } - }; - } - - const ESCAPE_ASCII_CHARS = [ - ["\b", "\\b"], - ["\f", "\\f"], - ["\t", "\\t"], - ["\n", "\\n"], - ["\r", "\\r"], - ["\v", "\\v"], - ]; - - /** - * @param {string} name - * @returns {string} - */ - function escapeName(name) { - // Check if we need to escape a character - for (let i = 0; i < name.length; i++) { - const ch = name.charCodeAt(i); - if (ch <= 13 && ch >= 8) { - // Slow path: We do need to escape it - for (const [escape, replaceWith] of ESCAPE_ASCII_CHARS) { - name = name.replaceAll(escape, replaceWith); - } - return name; + postTraces = new Map(core.opCallTraces); + if (res === 3) { + report = core.ops.op_test_op_sanitizer_report(desc.id); } } - // We didn't need to escape anything, return original string - return name; - } + if (report === null) return null; - /** - * @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 - */ + const details = []; + for (const opReport of report) { + const opName = opNames[opReport.id]; + const diff = opReport.diff; - /** @type {Map} */ - const testStates = new Map(); - /** @type {number | null} */ - let currentBenchId = null; - // These local variables are used to track time measurements at - // `BenchContext::{start,end}` calls. They are global instead of using a state - // map to minimise the overhead of assigning them. - /** @type {number | null} */ - let currentBenchUserExplicitStart = null; - /** @type {number | null} */ - let currentBenchUserExplicitEnd = null; - - const registerTestIdRetBuf = new Uint32Array(1); - const registerTestIdRetBufU8 = new Uint8Array(registerTestIdRetBuf.buffer); - - function testInner( - nameOrFnOrOptions, - optionsOrFn, - maybeFn, - overrides = {}, - ) { - if (typeof ops.op_register_test != "function") { - return; - } - - let testDesc; - const defaults = { - ignore: false, - only: false, - sanitizeOps: true, - sanitizeResources: true, - sanitizeExit: true, - permissions: null, - }; - - if (typeof nameOrFnOrOptions === "string") { - if (!nameOrFnOrOptions) { - throw new TypeError("The test name can't be empty"); - } - if (typeof optionsOrFn === "function") { - testDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults }; + if (diff > 0) { + const [name, hint] = OP_DETAILS[opName] || [opName, null]; + const count = diff; + let message = `${count} async operation${ + count === 1 ? "" : "s" + } to ${name} ${ + count === 1 ? "was" : "were" + } started in this test, but never completed.`; + if (hint) { + message += ` This is often caused by not ${hint}.`; + } + const traces = []; + for (const [id, { opName: traceOpName, stack }] of postTraces) { + if (traceOpName !== opName) continue; + if (MapPrototypeHas(preTraces, id)) continue; + ArrayPrototypePush(traces, stack); + } + if (traces.length === 1) { + message += " The operation was started here:\n"; + message += traces[0]; + } else if (traces.length > 1) { + message += " The operations were started here:\n"; + message += ArrayPrototypeJoin(traces, "\n\n"); + } + ArrayPrototypePush(details, message); + } else if (diff < 0) { + const [name, hint] = OP_DETAILS[opName] || [opName, null]; + const count = -diff; + let message = `${count} async operation${ + count === 1 ? "" : "s" + } to ${name} ${ + count === 1 ? "was" : "were" + } started before this test, but ${ + count === 1 ? "was" : "were" + } completed during the test. Async operations should not complete in a test if they were not started in that test.`; + if (hint) { + message += ` This is often caused by not ${hint}.`; + } + const traces = []; + for (const [id, { opName: traceOpName, stack }] of preTraces) { + if (opName !== traceOpName) continue; + if (MapPrototypeHas(postTraces, id)) continue; + ArrayPrototypePush(traces, stack); + } + if (traces.length === 1) { + message += " The operation was started here:\n"; + message += traces[0]; + } else if (traces.length > 1) { + message += " The operations were started here:\n"; + message += ArrayPrototypeJoin(traces, "\n\n"); + } + ArrayPrototypePush(details, message); } else { - if (!maybeFn || typeof maybeFn !== "function") { - throw new TypeError("Missing test function"); - } - if (optionsOrFn.fn != undefined) { - throw new TypeError( - "Unexpected 'fn' field in options, test function is already provided as the third argument.", - ); - } - if (optionsOrFn.name != undefined) { - throw new TypeError( - "Unexpected 'name' field in options, test name is already provided as the first argument.", - ); - } - testDesc = { - ...defaults, - ...optionsOrFn, - fn: maybeFn, - name: nameOrFnOrOptions, - }; + throw new Error("unreachable"); } - } else if (typeof nameOrFnOrOptions === "function") { - if (!nameOrFnOrOptions.name) { - throw new TypeError("The test function must have a name"); + } + + return { + failed: { leakedOps: [details, core.isOpCallTracingEnabled()] }, + }; + }; +} + +function prettyResourceNames(name) { + switch (name) { + case "fsFile": + return ["A file", "opened", "closed"]; + case "fetchRequest": + return ["A fetch request", "started", "finished"]; + case "fetchRequestBody": + return ["A fetch request body", "created", "closed"]; + case "fetchResponse": + return ["A fetch response body", "created", "consumed"]; + case "httpClient": + return ["An HTTP client", "created", "closed"]; + case "dynamicLibrary": + return ["A dynamic library", "loaded", "unloaded"]; + case "httpConn": + return ["An inbound HTTP connection", "accepted", "closed"]; + case "httpStream": + return ["An inbound HTTP request", "accepted", "closed"]; + case "tcpStream": + return ["A TCP connection", "opened/accepted", "closed"]; + case "unixStream": + return ["A Unix connection", "opened/accepted", "closed"]; + case "tlsStream": + return ["A TLS connection", "opened/accepted", "closed"]; + case "tlsListener": + return ["A TLS listener", "opened", "closed"]; + case "unixListener": + return ["A Unix listener", "opened", "closed"]; + case "unixDatagram": + return ["A Unix datagram", "opened", "closed"]; + case "tcpListener": + return ["A TCP listener", "opened", "closed"]; + case "udpSocket": + return ["A UDP socket", "opened", "closed"]; + case "timer": + return ["A timer", "started", "fired/cleared"]; + case "textDecoder": + return ["A text decoder", "created", "finished"]; + case "messagePort": + return ["A message port", "created", "closed"]; + case "webSocketStream": + return ["A WebSocket", "opened", "closed"]; + case "fsEvents": + return ["A file system watcher", "created", "closed"]; + case "childStdin": + return ["A child process stdin", "opened", "closed"]; + case "childStdout": + return ["A child process stdout", "opened", "closed"]; + case "childStderr": + return ["A child process stderr", "opened", "closed"]; + case "child": + return ["A child process", "started", "closed"]; + case "signal": + return ["A signal listener", "created", "fired/cleared"]; + case "stdin": + return ["The stdin pipe", "opened", "closed"]; + case "stdout": + return ["The stdout pipe", "opened", "closed"]; + case "stderr": + return ["The stderr pipe", "opened", "closed"]; + case "compression": + return ["A CompressionStream", "created", "closed"]; + default: + return [`A "${name}" resource`, "created", "cleaned up"]; + } +} + +function resourceCloseHint(name) { + switch (name) { + case "fsFile": + return "Close the file handle by calling `file.close()`."; + case "fetchRequest": + return "Await the promise returned from `fetch()` or abort the fetch with an abort signal."; + case "fetchRequestBody": + return "Terminate the request body `ReadableStream` by closing or erroring it."; + case "fetchResponse": + return "Consume or close the response body `ReadableStream`, e.g `await resp.text()` or `await resp.body.cancel()`."; + case "httpClient": + return "Close the HTTP client by calling `httpClient.close()`."; + case "dynamicLibrary": + return "Unload the dynamic library by calling `dynamicLibrary.close()`."; + case "httpConn": + return "Close the inbound HTTP connection by calling `httpConn.close()`."; + case "httpStream": + return "Close the inbound HTTP request by responding with `e.respondWith()` or closing the HTTP connection."; + case "tcpStream": + return "Close the TCP connection by calling `tcpConn.close()`."; + case "unixStream": + return "Close the Unix socket connection by calling `unixConn.close()`."; + case "tlsStream": + return "Close the TLS connection by calling `tlsConn.close()`."; + case "tlsListener": + return "Close the TLS listener by calling `tlsListener.close()`."; + case "unixListener": + return "Close the Unix socket listener by calling `unixListener.close()`."; + case "unixDatagram": + return "Close the Unix datagram socket by calling `unixDatagram.close()`."; + case "tcpListener": + return "Close the TCP listener by calling `tcpListener.close()`."; + case "udpSocket": + return "Close the UDP socket by calling `udpSocket.close()`."; + case "timer": + return "Clear the timer by calling `clearInterval` or `clearTimeout`."; + case "textDecoder": + return "Close the text decoder by calling `textDecoder.decode('')` or `await textDecoderStream.readable.cancel()`."; + case "messagePort": + return "Close the message port by calling `messagePort.close()`."; + case "webSocketStream": + return "Close the WebSocket by calling `webSocket.close()`."; + case "fsEvents": + return "Close the file system watcher by calling `watcher.close()`."; + case "childStdin": + return "Close the child process stdin by calling `proc.stdin.close()`."; + case "childStdout": + return "Close the child process stdout by calling `proc.stdout.close()` or `await child.stdout.cancel()`."; + case "childStderr": + return "Close the child process stderr by calling `proc.stderr.close()` or `await child.stderr.cancel()`."; + case "child": + return "Close the child process by calling `proc.kill()` or `proc.close()`."; + case "signal": + return "Clear the signal listener by calling `Deno.removeSignalListener`."; + case "stdin": + return "Close the stdin pipe by calling `Deno.stdin.close()`."; + case "stdout": + return "Close the stdout pipe by calling `Deno.stdout.close()`."; + case "stderr": + return "Close the stderr pipe by calling `Deno.stderr.close()`."; + case "compression": + return "Close the compression stream by calling `await stream.writable.close()`."; + default: + return "Close the resource before the end of the test."; + } +} + +// Wrap test function in additional assertion that makes sure +// the test case does not "leak" resources - ie. resource table after +// the test has exactly the same contents as before the test. +function assertResources(fn) { + /** @param desc {TestDescription | TestStepDescription} */ + return async function resourceSanitizer(desc) { + const pre = core.resources(); + const innerResult = await fn(desc); + if (innerResult) return innerResult; + const post = core.resources(); + + const allResources = new Set([ + ...new SafeArrayIterator(ObjectKeys(pre)), + ...new SafeArrayIterator(ObjectKeys(post)), + ]); + + const details = []; + for (const resource of allResources) { + const preResource = pre[resource]; + const postResource = post[resource]; + if (preResource === postResource) continue; + + if (preResource === undefined) { + const [name, action1, action2] = prettyResourceNames(postResource); + const hint = resourceCloseHint(postResource); + const detail = + `${name} (rid ${resource}) was ${action1} during the test, but not ${action2} during the test. ${hint}`; + ArrayPrototypePush(details, detail); + } else { + const [name, action1, action2] = prettyResourceNames(preResource); + const detail = + `${name} (rid ${resource}) was ${action1} before the test started, but was ${action2} during the test. Do not close resources in a test that were not created during that test.`; + ArrayPrototypePush(details, detail); } - if (optionsOrFn != undefined) { - throw new TypeError("Unexpected second argument to Deno.test()"); + } + if (details.length == 0) { + return null; + } + return { failed: { leakedResources: details } }; + }; +} + +// Wrap test function in additional assertion that makes sure +// that the test case does not accidentally exit prematurely. +function assertExit(fn, isTest) { + return async function exitSanitizer(...params) { + setExitHandler((exitCode) => { + throw new Error( + `${ + isTest ? "Test case" : "Bench" + } attempted to exit with exit code: ${exitCode}`, + ); + }); + + try { + const innerResult = await fn(...new SafeArrayIterator(params)); + if (innerResult) return innerResult; + } finally { + setExitHandler(null); + } + }; +} + +function wrapOuter(fn, desc) { + return async function outerWrapped() { + try { + if (desc.ignore) { + return "ignored"; } - if (maybeFn != undefined) { - throw new TypeError("Unexpected third argument to Deno.test()"); + return await fn(desc) ?? "ok"; + } catch (error) { + return { failed: { jsError: core.destructureError(error) } }; + } finally { + const state = MapPrototypeGet(testStates, desc.id); + for (const childDesc of state.children) { + stepReportResult(childDesc, { failed: "incomplete" }, 0); + } + state.completed = true; + } + }; +} + +function wrapInner(fn) { + /** @param desc {TestDescription | TestStepDescription} */ + return async function innerWrapped(desc) { + function getRunningStepDescs() { + const results = []; + let childDesc = desc; + while (childDesc.parent != null) { + const state = MapPrototypeGet(testStates, childDesc.parent.id); + for (const siblingDesc of state.children) { + if (siblingDesc.id == childDesc.id) { + continue; + } + const siblingState = MapPrototypeGet(testStates, siblingDesc.id); + if (!siblingState.completed) { + ArrayPrototypePush(results, siblingDesc); + } + } + childDesc = childDesc.parent; + } + return results; + } + const runningStepDescs = getRunningStepDescs(); + const runningStepDescsWithSanitizers = ArrayPrototypeFilter( + runningStepDescs, + (d) => usesSanitizer(d), + ); + + if (runningStepDescsWithSanitizers.length > 0) { + return { + failed: { + overlapsWithSanitizers: runningStepDescsWithSanitizers.map( + getFullName, + ), + }, + }; + } + + if (usesSanitizer(desc) && runningStepDescs.length > 0) { + return { + failed: { + hasSanitizersAndOverlaps: runningStepDescs.map(getFullName), + }, + }; + } + await fn(MapPrototypeGet(testStates, desc.id).context); + let failedSteps = 0; + for (const childDesc of MapPrototypeGet(testStates, desc.id).children) { + const state = MapPrototypeGet(testStates, childDesc.id); + if (!state.completed) { + return { failed: "incompleteSteps" }; + } + if (state.failed) { + failedSteps++; + } + } + return failedSteps == 0 ? null : { failed: { failedSteps } }; + }; +} + +function pledgePermissions(permissions) { + return ops.op_pledge_test_permissions( + serializePermissions(permissions), + ); +} + +function restorePermissions(token) { + ops.op_restore_test_permissions(token); +} + +function withPermissions(fn, permissions) { + return async function applyPermissions(...params) { + const token = pledgePermissions(permissions); + + try { + return await fn(...new SafeArrayIterator(params)); + } finally { + restorePermissions(token); + } + }; +} + +const ESCAPE_ASCII_CHARS = [ + ["\b", "\\b"], + ["\f", "\\f"], + ["\t", "\\t"], + ["\n", "\\n"], + ["\r", "\\r"], + ["\v", "\\v"], +]; + +/** + * @param {string} name + * @returns {string} + */ +function escapeName(name) { + // Check if we need to escape a character + for (let i = 0; i < name.length; i++) { + const ch = name.charCodeAt(i); + if (ch <= 13 && ch >= 8) { + // Slow path: We do need to escape it + for (const [escape, replaceWith] of ESCAPE_ASCII_CHARS) { + name = StringPrototypeReplaceAll(name, escape, replaceWith); + } + return name; + } + } + + // We didn't need to escape anything, return original string + return name; +} + +/** + * @typedef {{ + * id: number, + * name: string, + * fn: TestFunction + * origin: string, + * location: TestLocation, + * ignore: boolean, + * only: boolean. + * sanitizeOps: boolean, + * sanitizeResources: boolean, + * sanitizeExit: boolean, + * permissions: PermissionOptions, + * }} TestDescription + * + * @typedef {{ + * id: number, + * name: string, + * fn: TestFunction + * origin: string, + * location: TestLocation, + * ignore: boolean, + * level: number, + * parent: TestDescription | TestStepDescription, + * rootId: number, + * rootName: String, + * sanitizeOps: boolean, + * sanitizeResources: boolean, + * sanitizeExit: boolean, + * }} TestStepDescription + * + * @typedef {{ + * context: TestContext, + * children: TestStepDescription[], + * completed: boolean, + * }} TestState + * + * @typedef {{ + * context: TestContext, + * children: TestStepDescription[], + * completed: boolean, + * failed: boolean, + * }} TestStepState + * + * @typedef {{ + * id: number, + * name: string, + * fn: BenchFunction + * origin: string, + * ignore: boolean, + * only: boolean. + * sanitizeExit: boolean, + * permissions: PermissionOptions, + * }} BenchDescription + */ + +/** @type {Map} */ +const testStates = new Map(); +/** @type {number | null} */ +let currentBenchId = null; +// These local variables are used to track time measurements at +// `BenchContext::{start,end}` calls. They are global instead of using a state +// map to minimise the overhead of assigning them. +/** @type {number | null} */ +let currentBenchUserExplicitStart = null; +/** @type {number | null} */ +let currentBenchUserExplicitEnd = null; + +const registerTestIdRetBuf = new Uint32Array(1); +const registerTestIdRetBufU8 = new Uint8Array(registerTestIdRetBuf.buffer); + +function testInner( + nameOrFnOrOptions, + optionsOrFn, + maybeFn, + overrides = {}, +) { + if (typeof ops.op_register_test != "function") { + return; + } + + let testDesc; + const defaults = { + ignore: false, + only: false, + sanitizeOps: true, + sanitizeResources: true, + sanitizeExit: true, + permissions: null, + }; + + if (typeof nameOrFnOrOptions === "string") { + if (!nameOrFnOrOptions) { + throw new TypeError("The test name can't be empty"); + } + if (typeof optionsOrFn === "function") { + testDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults }; + } else { + if (!maybeFn || typeof maybeFn !== "function") { + throw new TypeError("Missing test function"); + } + if (optionsOrFn.fn != undefined) { + throw new TypeError( + "Unexpected 'fn' field in options, test function is already provided as the third argument.", + ); + } + if (optionsOrFn.name != undefined) { + throw new TypeError( + "Unexpected 'name' field in options, test name is already provided as the first argument.", + ); } testDesc = { ...defaults, - fn: nameOrFnOrOptions, - name: nameOrFnOrOptions.name, + ...optionsOrFn, + fn: maybeFn, + name: nameOrFnOrOptions, }; + } + } else if (typeof nameOrFnOrOptions === "function") { + if (!nameOrFnOrOptions.name) { + throw new TypeError("The test function must have a name"); + } + if (optionsOrFn != undefined) { + throw new TypeError("Unexpected second argument to Deno.test()"); + } + if (maybeFn != undefined) { + throw new TypeError("Unexpected third argument to Deno.test()"); + } + testDesc = { + ...defaults, + fn: nameOrFnOrOptions, + name: nameOrFnOrOptions.name, + }; + } else { + let fn; + let name; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + if (nameOrFnOrOptions.fn != undefined) { + throw new TypeError( + "Unexpected 'fn' field in options, test function is already provided as the second argument.", + ); + } + name = nameOrFnOrOptions.name ?? fn.name; } else { - let fn; - let name; - if (typeof optionsOrFn === "function") { - fn = optionsOrFn; - if (nameOrFnOrOptions.fn != undefined) { - throw new TypeError( - "Unexpected 'fn' field in options, test function is already provided as the second argument.", - ); - } - name = nameOrFnOrOptions.name ?? fn.name; - } else { - if ( - !nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function" - ) { - throw new TypeError( - "Expected 'fn' field in the first argument to be a test function.", - ); - } - fn = nameOrFnOrOptions.fn; - name = nameOrFnOrOptions.name ?? fn.name; + if ( + !nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function" + ) { + throw new TypeError( + "Expected 'fn' field in the first argument to be a test function.", + ); } - if (!name) { - throw new TypeError("The test name can't be empty"); - } - testDesc = { ...defaults, ...nameOrFnOrOptions, fn, name }; + fn = nameOrFnOrOptions.fn; + name = nameOrFnOrOptions.name ?? fn.name; } - - testDesc = { ...testDesc, ...overrides }; - - // Delete this prop in case the user passed it. It's used to detect steps. - delete testDesc.parent; - - testDesc.location = core.currentUserCallSite(); - testDesc.fn = wrapTest(testDesc); - testDesc.name = escapeName(testDesc.name); - - const origin = ops.op_register_test( - testDesc.fn, - testDesc.name, - testDesc.ignore, - testDesc.only, - testDesc.location.fileName, - testDesc.location.lineNumber, - testDesc.location.columnNumber, - registerTestIdRetBufU8, - ); - testDesc.id = registerTestIdRetBuf[0]; - testDesc.origin = origin; - testStates.set(testDesc.id, { - context: createTestContext(testDesc), - children: [], - completed: false, - }); + if (!name) { + throw new TypeError("The test name can't be empty"); + } + testDesc = { ...defaults, ...nameOrFnOrOptions, fn, name }; } - // Main test function provided by Deno. - function test( - nameOrFnOrOptions, - optionsOrFn, - maybeFn, - ) { - return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn); + testDesc = { ...testDesc, ...overrides }; + + // Delete this prop in case the user passed it. It's used to detect steps. + delete testDesc.parent; + + testDesc.location = core.currentUserCallSite(); + testDesc.fn = wrapTest(testDesc); + testDesc.name = escapeName(testDesc.name); + + const origin = ops.op_register_test( + testDesc.fn, + testDesc.name, + testDesc.ignore, + testDesc.only, + testDesc.location.fileName, + testDesc.location.lineNumber, + testDesc.location.columnNumber, + registerTestIdRetBufU8, + ); + testDesc.id = registerTestIdRetBuf[0]; + testDesc.origin = origin; + MapPrototypeSet(testStates, testDesc.id, { + context: createTestContext(testDesc), + children: [], + completed: false, + }); +} + +// Main test function provided by Deno. +function test( + nameOrFnOrOptions, + optionsOrFn, + maybeFn, +) { + return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn); +} + +test.ignore = function (nameOrFnOrOptions, optionsOrFn, maybeFn) { + return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn, { ignore: true }); +}; + +test.only = function ( + nameOrFnOrOptions, + optionsOrFn, + maybeFn, +) { + return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn, { only: true }); +}; + +let registeredWarmupBench = false; + +// Main bench function provided by Deno. +function bench( + nameOrFnOrOptions, + optionsOrFn, + maybeFn, +) { + if (typeof ops.op_register_bench != "function") { + return; } - test.ignore = function (nameOrFnOrOptions, optionsOrFn, maybeFn) { - return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn, { ignore: true }); - }; - - test.only = function ( - nameOrFnOrOptions, - optionsOrFn, - maybeFn, - ) { - return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn, { only: true }); - }; - - let registeredWarmupBench = false; - - // Main bench function provided by Deno. - function bench( - nameOrFnOrOptions, - optionsOrFn, - maybeFn, - ) { - if (typeof ops.op_register_bench != "function") { - return; - } - - if (!registeredWarmupBench) { - registeredWarmupBench = true; - const warmupBenchDesc = { - name: "", - fn: function warmup() {}, - async: false, - ignore: false, - baseline: false, - only: false, - sanitizeExit: true, - permissions: null, - warmup: true, - }; - warmupBenchDesc.fn = wrapBenchmark(warmupBenchDesc); - const { id, origin } = ops.op_register_bench(warmupBenchDesc); - warmupBenchDesc.id = id; - warmupBenchDesc.origin = origin; - } - - let benchDesc; - const defaults = { + if (!registeredWarmupBench) { + registeredWarmupBench = true; + const warmupBenchDesc = { + name: "", + fn: function warmup() {}, + async: false, ignore: false, baseline: false, only: false, sanitizeExit: true, permissions: null, + warmup: true, }; + warmupBenchDesc.fn = wrapBenchmark(warmupBenchDesc); + const { id, origin } = ops.op_register_bench(warmupBenchDesc); + warmupBenchDesc.id = id; + warmupBenchDesc.origin = origin; + } - if (typeof nameOrFnOrOptions === "string") { - if (!nameOrFnOrOptions) { - throw new TypeError("The bench name can't be empty"); + let benchDesc; + const defaults = { + ignore: false, + baseline: false, + only: false, + sanitizeExit: true, + permissions: null, + }; + + if (typeof nameOrFnOrOptions === "string") { + if (!nameOrFnOrOptions) { + throw new TypeError("The bench name can't be empty"); + } + if (typeof optionsOrFn === "function") { + benchDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults }; + } else { + if (!maybeFn || typeof maybeFn !== "function") { + throw new TypeError("Missing bench function"); } - if (typeof optionsOrFn === "function") { - benchDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults }; - } else { - if (!maybeFn || typeof maybeFn !== "function") { - throw new TypeError("Missing bench function"); - } - if (optionsOrFn.fn != undefined) { - throw new TypeError( - "Unexpected 'fn' field in options, bench function is already provided as the third argument.", - ); - } - if (optionsOrFn.name != undefined) { - throw new TypeError( - "Unexpected 'name' field in options, bench name is already provided as the first argument.", - ); - } - benchDesc = { - ...defaults, - ...optionsOrFn, - fn: maybeFn, - name: nameOrFnOrOptions, - }; + if (optionsOrFn.fn != undefined) { + throw new TypeError( + "Unexpected 'fn' field in options, bench function is already provided as the third argument.", + ); } - } 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()"); + if (optionsOrFn.name != undefined) { + throw new TypeError( + "Unexpected 'name' field in options, bench name is already provided as the first argument.", + ); } benchDesc = { ...defaults, - fn: nameOrFnOrOptions, - name: nameOrFnOrOptions.name, + ...optionsOrFn, + fn: maybeFn, + name: nameOrFnOrOptions, }; - } else { - let fn; - let name; - if (typeof optionsOrFn === "function") { - fn = optionsOrFn; - if (nameOrFnOrOptions.fn != undefined) { - throw new TypeError( - "Unexpected 'fn' field in options, bench function is already provided as the second argument.", - ); - } - name = nameOrFnOrOptions.name ?? fn.name; - } else { - if ( - !nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function" - ) { - throw new TypeError( - "Expected 'fn' field in the first argument to be a bench function.", - ); - } - fn = nameOrFnOrOptions.fn; - name = nameOrFnOrOptions.name ?? fn.name; - } - if (!name) { - throw new TypeError("The bench name can't be empty"); - } - benchDesc = { ...defaults, ...nameOrFnOrOptions, fn, name }; } - - const AsyncFunction = (async () => {}).constructor; - benchDesc.async = AsyncFunction === benchDesc.fn.constructor; - benchDesc.fn = wrapBenchmark(benchDesc); - benchDesc.warmup = false; - benchDesc.name = escapeName(benchDesc.name); - - const { id, origin } = ops.op_register_bench(benchDesc); - benchDesc.id = id; - benchDesc.origin = origin; + } else if (typeof nameOrFnOrOptions === "function") { + if (!nameOrFnOrOptions.name) { + throw new TypeError("The bench function must have a name"); + } + if (optionsOrFn != undefined) { + throw new TypeError("Unexpected second argument to Deno.bench()"); + } + if (maybeFn != undefined) { + throw new TypeError("Unexpected third argument to Deno.bench()"); + } + benchDesc = { + ...defaults, + fn: nameOrFnOrOptions, + name: nameOrFnOrOptions.name, + }; + } else { + let fn; + let name; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + if (nameOrFnOrOptions.fn != undefined) { + throw new TypeError( + "Unexpected 'fn' field in options, bench function is already provided as the second argument.", + ); + } + name = nameOrFnOrOptions.name ?? fn.name; + } else { + if ( + !nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function" + ) { + throw new TypeError( + "Expected 'fn' field in the first argument to be a bench function.", + ); + } + fn = nameOrFnOrOptions.fn; + name = nameOrFnOrOptions.name ?? fn.name; + } + if (!name) { + throw new TypeError("The bench name can't be empty"); + } + benchDesc = { ...defaults, ...nameOrFnOrOptions, fn, name }; } - function compareMeasurements(a, b) { - if (a > b) return 1; - if (a < b) return -1; + const AsyncFunction = (async () => {}).constructor; + benchDesc.async = AsyncFunction === benchDesc.fn.constructor; + benchDesc.fn = wrapBenchmark(benchDesc); + benchDesc.warmup = false; + benchDesc.name = escapeName(benchDesc.name); - return 0; - } + const { id, origin } = ops.op_register_bench(benchDesc); + benchDesc.id = id; + benchDesc.origin = origin; +} - function benchStats( +function compareMeasurements(a, b) { + if (a > b) return 1; + if (a < b) return -1; + + return 0; +} + +function benchStats( + n, + highPrecision, + usedExplicitTimers, + avg, + min, + max, + all, +) { + return { n, - highPrecision, - usedExplicitTimers, - avg, min, max, - all, - ) { - return { - n, - min, - max, - p75: all[Math.ceil(n * (75 / 100)) - 1], - p99: all[Math.ceil(n * (99 / 100)) - 1], - p995: all[Math.ceil(n * (99.5 / 100)) - 1], - p999: all[Math.ceil(n * (99.9 / 100)) - 1], - avg: !highPrecision ? (avg / n) : Math.ceil(avg / n), - highPrecision, - usedExplicitTimers, - }; + p75: all[MathCeil(n * (75 / 100)) - 1], + p99: all[MathCeil(n * (99 / 100)) - 1], + p995: all[MathCeil(n * (99.5 / 100)) - 1], + p999: all[MathCeil(n * (99.9 / 100)) - 1], + avg: !highPrecision ? (avg / n) : MathCeil(avg / n), + highPrecision, + usedExplicitTimers, + }; +} + +async function benchMeasure(timeBudget, fn, async, context) { + let n = 0; + let avg = 0; + let wavg = 0; + let usedExplicitTimers = false; + const all = []; + let min = Infinity; + let max = -Infinity; + const lowPrecisionThresholdInNs = 1e4; + + // warmup step + let c = 0; + let iterations = 20; + let budget = 10 * 1e6; + + if (!async) { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + fn(context); + const t2 = benchNow(); + const totalTime = t2 - t1; + if (currentBenchUserExplicitStart !== null) { + currentBenchUserExplicitStart = null; + usedExplicitTimers = true; + } + if (currentBenchUserExplicitEnd !== null) { + currentBenchUserExplicitEnd = null; + usedExplicitTimers = true; + } + + c++; + wavg += totalTime; + budget -= totalTime; + } + } else { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + await fn(context); + const t2 = benchNow(); + const totalTime = t2 - t1; + if (currentBenchUserExplicitStart !== null) { + currentBenchUserExplicitStart = null; + usedExplicitTimers = true; + } + if (currentBenchUserExplicitEnd !== null) { + currentBenchUserExplicitEnd = null; + usedExplicitTimers = true; + } + + c++; + wavg += totalTime; + budget -= totalTime; + } } - async function benchMeasure(timeBudget, fn, async, context) { - let n = 0; - let avg = 0; - let wavg = 0; - let usedExplicitTimers = false; - const all = []; - let min = Infinity; - let max = -Infinity; - const lowPrecisionThresholdInNs = 1e4; + wavg /= c; - // warmup step - let c = 0; - let iterations = 20; - let budget = 10 * 1e6; + // measure step + if (wavg > lowPrecisionThresholdInNs) { + let iterations = 10; + let budget = timeBudget * 1e6; if (!async) { while (budget > 0 || iterations-- > 0) { @@ -976,18 +1041,22 @@ fn(context); const t2 = benchNow(); const totalTime = t2 - t1; + let measuredTime = totalTime; if (currentBenchUserExplicitStart !== null) { + measuredTime -= currentBenchUserExplicitStart - t1; currentBenchUserExplicitStart = null; - usedExplicitTimers = true; } if (currentBenchUserExplicitEnd !== null) { + measuredTime -= t2 - currentBenchUserExplicitEnd; currentBenchUserExplicitEnd = null; - usedExplicitTimers = true; } - c++; - wavg += totalTime; + n++; + avg += measuredTime; budget -= totalTime; + ArrayPrototypePush(all, measuredTime); + if (measuredTime < min) min = measuredTime; + if (measuredTime > max) max = measuredTime; } } else { while (budget > 0 || iterations-- > 0) { @@ -995,382 +1064,329 @@ await fn(context); const t2 = benchNow(); const totalTime = t2 - t1; + let measuredTime = totalTime; if (currentBenchUserExplicitStart !== null) { + measuredTime -= currentBenchUserExplicitStart - t1; currentBenchUserExplicitStart = null; - usedExplicitTimers = true; } if (currentBenchUserExplicitEnd !== null) { + measuredTime -= t2 - currentBenchUserExplicitEnd; currentBenchUserExplicitEnd = null; - usedExplicitTimers = true; } - c++; - wavg += totalTime; + n++; + avg += measuredTime; budget -= totalTime; + ArrayPrototypePush(all, measuredTime); + if (measuredTime < min) min = measuredTime; + if (measuredTime > max) max = measuredTime; } } + } else { + context.start = function start() {}; + context.end = function end() {}; + let iterations = 10; + let budget = timeBudget * 1e6; - wavg /= c; - - // measure step - if (wavg > lowPrecisionThresholdInNs) { - let iterations = 10; - let budget = timeBudget * 1e6; - - if (!async) { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); + if (!async) { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + for (let c = 0; c < lowPrecisionThresholdInNs; c++) { fn(context); - const t2 = benchNow(); - const totalTime = t2 - t1; - let measuredTime = totalTime; - if (currentBenchUserExplicitStart !== null) { - measuredTime -= currentBenchUserExplicitStart - t1; - currentBenchUserExplicitStart = null; - } - if (currentBenchUserExplicitEnd !== null) { - measuredTime -= t2 - currentBenchUserExplicitEnd; - currentBenchUserExplicitEnd = null; - } - - n++; - avg += measuredTime; - budget -= totalTime; - all.push(measuredTime); - if (measuredTime < min) min = measuredTime; - if (measuredTime > max) max = measuredTime; } - } else { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); + const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs; + + n++; + avg += iterationTime; + ArrayPrototypePush(all, iterationTime); + if (iterationTime < min) min = iterationTime; + if (iterationTime > max) max = iterationTime; + budget -= iterationTime * lowPrecisionThresholdInNs; + } + } else { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + for (let c = 0; c < lowPrecisionThresholdInNs; c++) { await fn(context); - const t2 = benchNow(); - const totalTime = t2 - t1; - let measuredTime = totalTime; - if (currentBenchUserExplicitStart !== null) { - measuredTime -= currentBenchUserExplicitStart - t1; - currentBenchUserExplicitStart = null; - } - if (currentBenchUserExplicitEnd !== null) { - measuredTime -= t2 - currentBenchUserExplicitEnd; - currentBenchUserExplicitEnd = null; - } - - n++; - avg += measuredTime; - budget -= totalTime; - all.push(measuredTime); - if (measuredTime < min) min = measuredTime; - if (measuredTime > max) max = measuredTime; + currentBenchUserExplicitStart = null; + currentBenchUserExplicitEnd = null; } - } - } else { - context.start = function start() {}; - context.end = function end() {}; - let iterations = 10; - let budget = timeBudget * 1e6; + const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs; - if (!async) { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - for (let c = 0; c < lowPrecisionThresholdInNs; c++) { - fn(context); - } - const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs; - - n++; - avg += iterationTime; - all.push(iterationTime); - if (iterationTime < min) min = iterationTime; - if (iterationTime > max) max = iterationTime; - budget -= iterationTime * lowPrecisionThresholdInNs; - } - } else { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - for (let c = 0; c < lowPrecisionThresholdInNs; c++) { - await fn(context); - currentBenchUserExplicitStart = null; - currentBenchUserExplicitEnd = null; - } - const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs; - - n++; - avg += iterationTime; - all.push(iterationTime); - if (iterationTime < min) min = iterationTime; - if (iterationTime > max) max = iterationTime; - budget -= iterationTime * lowPrecisionThresholdInNs; - } + n++; + avg += iterationTime; + ArrayPrototypePush(all, iterationTime); + if (iterationTime < min) min = iterationTime; + if (iterationTime > max) max = iterationTime; + budget -= iterationTime * lowPrecisionThresholdInNs; } } - - all.sort(compareMeasurements); - return benchStats( - n, - wavg > lowPrecisionThresholdInNs, - usedExplicitTimers, - avg, - min, - max, - all, - ); } - /** @param desc {BenchDescription} */ - function createBenchContext(desc) { - return { - [Symbol.toStringTag]: "BenchContext", - name: desc.name, - origin: desc.origin, - start() { - if (currentBenchId !== desc.id) { - throw new TypeError( - "The benchmark which this context belongs to is not being executed.", - ); - } - if (currentBenchUserExplicitStart != null) { - throw new TypeError( - "BenchContext::start() has already been invoked.", - ); - } - currentBenchUserExplicitStart = benchNow(); - }, - end() { - const end = benchNow(); - if (currentBenchId !== desc.id) { - throw new TypeError( - "The benchmark which this context belongs to is not being executed.", - ); - } - if (currentBenchUserExplicitEnd != null) { - throw new TypeError("BenchContext::end() has already been invoked."); - } - currentBenchUserExplicitEnd = end; - }, - }; - } + all.sort(compareMeasurements); + return benchStats( + n, + wavg > lowPrecisionThresholdInNs, + usedExplicitTimers, + avg, + min, + max, + all, + ); +} - /** Wrap a user benchmark function in one which returns a structured result. */ - function wrapBenchmark(desc) { - const fn = desc.fn; - return async function outerWrapped() { - let token = null; - const originalConsole = globalThis.console; - currentBenchId = desc.id; - - try { - globalThis.console = new Console((s) => { - ops.op_dispatch_bench_event({ output: s }); - }); - - if (desc.permissions) { - token = pledgePermissions(desc.permissions); - } - - if (desc.sanitizeExit) { - setExitHandler((exitCode) => { - throw new Error( - `Bench attempted to exit with exit code: ${exitCode}`, - ); - }); - } - - const benchTimeInMs = 500; - const context = createBenchContext(desc); - const stats = await benchMeasure( - benchTimeInMs, - fn, - desc.async, - context, +/** @param desc {BenchDescription} */ +function createBenchContext(desc) { + return { + [SymbolToStringTag]: "BenchContext", + name: desc.name, + origin: desc.origin, + start() { + if (currentBenchId !== desc.id) { + throw new TypeError( + "The benchmark which this context belongs to is not being executed.", ); - - return { ok: stats }; - } catch (error) { - return { failed: core.destructureError(error) }; - } finally { - globalThis.console = originalConsole; - currentBenchId = null; - currentBenchUserExplicitStart = null; - currentBenchUserExplicitEnd = null; - if (bench.sanitizeExit) setExitHandler(null); - if (token !== null) restorePermissions(token); } - }; - } + if (currentBenchUserExplicitStart != null) { + throw new TypeError( + "BenchContext::start() has already been invoked.", + ); + } + currentBenchUserExplicitStart = benchNow(); + }, + end() { + const end = benchNow(); + if (currentBenchId !== desc.id) { + throw new TypeError( + "The benchmark which this context belongs to is not being executed.", + ); + } + if (currentBenchUserExplicitEnd != null) { + throw new TypeError("BenchContext::end() has already been invoked."); + } + currentBenchUserExplicitEnd = end; + }, + }; +} - function benchNow() { - return ops.op_bench_now(); - } +/** Wrap a user benchmark function in one which returns a structured result. */ +function wrapBenchmark(desc) { + const fn = desc.fn; + return async function outerWrapped() { + let token = null; + const originalConsole = globalThis.console; + currentBenchId = desc.id; - function getFullName(desc) { - if ("parent" in desc) { - return `${getFullName(desc.parent)} ... ${desc.name}`; - } - return desc.name; - } + try { + globalThis.console = new Console((s) => { + ops.op_dispatch_bench_event({ output: s }); + }); - function usesSanitizer(desc) { - return desc.sanitizeResources || desc.sanitizeOps || desc.sanitizeExit; - } + if (desc.permissions) { + token = pledgePermissions(desc.permissions); + } - function stepReportResult(desc, result, elapsed) { - const state = testStates.get(desc.id); - for (const childDesc of state.children) { - stepReportResult(childDesc, { failed: "incomplete" }, 0); - } - if (result === "ok") { - ops.op_test_event_step_result_ok(desc.id, elapsed); - } else if (result === "ignored") { - ops.op_test_event_step_result_ignored(desc.id, elapsed); - } else { - ops.op_test_event_step_result_failed(desc.id, result.failed, elapsed); - } - } - - /** @param desc {TestDescription | TestStepDescription} */ - function createTestContext(desc) { - let parent; - let level; - let rootId; - let rootName; - if ("parent" in desc) { - parent = testStates.get(desc.parent.id).context; - level = desc.level; - rootId = desc.rootId; - rootName = desc.rootName; - } else { - parent = undefined; - level = 0; - rootId = desc.id; - rootName = desc.name; - } - return { - [Symbol.toStringTag]: "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)} - * @param maybeFn {((t: TestContext) => void | Promise) | undefined} - */ - async step(nameOrFnOrOptions, maybeFn) { - if (testStates.get(desc.id).completed) { + if (desc.sanitizeExit) { + setExitHandler((exitCode) => { 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.", + `Bench attempted to exit with exit code: ${exitCode}`, ); - } + }); + } - let stepDesc; - if (typeof nameOrFnOrOptions === "string") { - if ( - !Object.prototype.isPrototypeOf.call(Function.prototype, maybeFn) - ) { - throw new TypeError("Expected function for second argument."); - } - stepDesc = { - name: nameOrFnOrOptions, - fn: maybeFn, - }; - } else if (typeof nameOrFnOrOptions === "function") { - if (!nameOrFnOrOptions.name) { - throw new TypeError("The step function must have a name."); - } - if (maybeFn != undefined) { - throw new TypeError( - "Unexpected second argument to TestContext.step()", - ); - } - stepDesc = { - name: nameOrFnOrOptions.name, - fn: nameOrFnOrOptions, - }; - } else if (typeof nameOrFnOrOptions === "object") { - stepDesc = nameOrFnOrOptions; - } else { - throw new TypeError( - "Expected a test definition or name and function.", - ); - } - stepDesc.ignore ??= false; - stepDesc.sanitizeOps ??= desc.sanitizeOps; - stepDesc.sanitizeResources ??= desc.sanitizeResources; - stepDesc.sanitizeExit ??= desc.sanitizeExit; - stepDesc.location = core.currentUserCallSite(); - stepDesc.level = level + 1; - stepDesc.parent = desc; - stepDesc.rootId = rootId; - stepDesc.name = escapeName(stepDesc.name); - stepDesc.rootName = escapeName(rootName); - stepDesc.fn = wrapTest(stepDesc); - const id = ops.op_register_test_step( - stepDesc.name, - stepDesc.location.fileName, - stepDesc.location.lineNumber, - stepDesc.location.columnNumber, - stepDesc.level, - stepDesc.parent.id, - stepDesc.rootId, - stepDesc.rootName, + const benchTimeInMs = 500; + const context = createBenchContext(desc); + const stats = await benchMeasure( + benchTimeInMs, + fn, + desc.async, + context, + ); + + return { ok: stats }; + } catch (error) { + return { failed: core.destructureError(error) }; + } finally { + globalThis.console = originalConsole; + currentBenchId = null; + currentBenchUserExplicitStart = null; + currentBenchUserExplicitEnd = null; + if (bench.sanitizeExit) setExitHandler(null); + if (token !== null) restorePermissions(token); + } + }; +} + +function benchNow() { + return ops.op_bench_now(); +} + +function getFullName(desc) { + if ("parent" in desc) { + return `${getFullName(desc.parent)} ... ${desc.name}`; + } + return desc.name; +} + +function usesSanitizer(desc) { + return desc.sanitizeResources || desc.sanitizeOps || desc.sanitizeExit; +} + +function stepReportResult(desc, result, elapsed) { + const state = MapPrototypeGet(testStates, desc.id); + for (const childDesc of state.children) { + stepReportResult(childDesc, { failed: "incomplete" }, 0); + } + if (result === "ok") { + ops.op_test_event_step_result_ok(desc.id, elapsed); + } else if (result === "ignored") { + ops.op_test_event_step_result_ignored(desc.id, elapsed); + } else { + ops.op_test_event_step_result_failed(desc.id, result.failed, elapsed); + } +} + +/** @param desc {TestDescription | TestStepDescription} */ +function createTestContext(desc) { + let parent; + let level; + let rootId; + let rootName; + if ("parent" in desc) { + parent = MapPrototypeGet(testStates, desc.parent.id).context; + level = desc.level; + rootId = desc.rootId; + rootName = desc.rootName; + } else { + parent = undefined; + level = 0; + rootId = desc.id; + rootName = desc.name; + } + return { + [SymbolToStringTag]: "TestContext", + /** + * The current test name. + */ + name: desc.name, + /** + * Parent test context. + */ + parent, + /** + * File Uri of the test code. + */ + origin: desc.origin, + /** + * @param nameOrFnOrOptions {string | TestStepDefinition | ((t: TestContext) => void | Promise)} + * @param maybeFn {((t: TestContext) => void | Promise) | 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.", ); - stepDesc.id = id; - stepDesc.origin = desc.origin; - const state = { - context: createTestContext(stepDesc), - children: [], - failed: false, - completed: false, + } + + let stepDesc; + if (typeof nameOrFnOrOptions === "string") { + if (!(ObjectPrototypeIsPrototypeOf(FunctionPrototype, maybeFn))) { + throw new TypeError("Expected function for second argument."); + } + stepDesc = { + name: nameOrFnOrOptions, + fn: maybeFn, }; - testStates.set(stepDesc.id, state); - testStates.get(stepDesc.parent.id).children.push( - stepDesc, + } else if (typeof nameOrFnOrOptions === "function") { + if (!nameOrFnOrOptions.name) { + throw new TypeError("The step function must have a name."); + } + if (maybeFn != undefined) { + throw new TypeError( + "Unexpected second argument to TestContext.step()", + ); + } + stepDesc = { + name: nameOrFnOrOptions.name, + fn: nameOrFnOrOptions, + }; + } else if (typeof nameOrFnOrOptions === "object") { + stepDesc = nameOrFnOrOptions; + } else { + throw new TypeError( + "Expected a test definition or name and function.", ); + } + stepDesc.ignore ??= false; + stepDesc.sanitizeOps ??= desc.sanitizeOps; + stepDesc.sanitizeResources ??= desc.sanitizeResources; + stepDesc.sanitizeExit ??= desc.sanitizeExit; + stepDesc.location = core.currentUserCallSite(); + stepDesc.level = level + 1; + stepDesc.parent = desc; + stepDesc.rootId = rootId; + stepDesc.name = escapeName(stepDesc.name); + stepDesc.rootName = escapeName(rootName); + stepDesc.fn = wrapTest(stepDesc); + const id = ops.op_register_test_step( + stepDesc.name, + stepDesc.location.fileName, + stepDesc.location.lineNumber, + stepDesc.location.columnNumber, + stepDesc.level, + stepDesc.parent.id, + stepDesc.rootId, + stepDesc.rootName, + ); + stepDesc.id = id; + stepDesc.origin = desc.origin; + const state = { + context: createTestContext(stepDesc), + children: [], + failed: false, + completed: false, + }; + MapPrototypeSet(testStates, stepDesc.id, state); + ArrayPrototypePush( + MapPrototypeGet(testStates, stepDesc.parent.id).children, + stepDesc, + ); - ops.op_test_event_step_wait(stepDesc.id); - const earlier = Date.now(); - const result = await stepDesc.fn(stepDesc); - const elapsed = Date.now() - earlier; - state.failed = !!result.failed; - stepReportResult(stepDesc, result, elapsed); - return result == "ok"; - }, - }; + 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); } - - /** - * Wrap a user test function in one which returns a structured result. - * @template T {Function} - * @param testFn {T} - * @param desc {TestDescription | TestStepDescription} - * @returns {T} - */ - function wrapTest(desc) { - let testFn = wrapInner(desc.fn); - if (desc.sanitizeOps) { - testFn = assertOps(testFn); - } - if (desc.sanitizeResources) { - testFn = assertResources(testFn); - } - if (desc.sanitizeExit) { - testFn = assertExit(testFn, true); - } - if (!("parent" in desc) && desc.permissions) { - testFn = withPermissions(testFn, desc.permissions); - } - return wrapOuter(testFn, desc); + if (desc.sanitizeResources) { + testFn = assertResources(testFn); } + if (desc.sanitizeExit) { + testFn = assertExit(testFn, true); + } + if (!("parent" in desc) && desc.permissions) { + testFn = withPermissions(testFn, desc.permissions); + } + return wrapOuter(testFn, desc); +} - globalThis.Deno.bench = bench; - globalThis.Deno.test = test; -})(); +globalThis.Deno.bench = bench; +globalThis.Deno.test = test; diff --git a/cli/worker.rs b/cli/worker.rs index 5e9d279181..ce9c057014 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -637,13 +637,13 @@ impl CliMainWorkerFactory { ); if self.shared.subcommand.needs_test() { - worker.js_runtime.execute_script_static( + worker.js_runtime.lazy_load_es_module_from_code( "ext:cli/40_testing.js", - include_str!("js/40_testing.js"), + deno_core::FastString::StaticAscii(include_str!("js/40_testing.js")), )?; - worker.js_runtime.execute_script_static( + worker.js_runtime.lazy_load_es_module_from_code( "ext:cli/40_jupyter.js", - include_str!("js/40_jupyter.js"), + deno_core::FastString::StaticAscii(include_str!("js/40_jupyter.js")), )?; } diff --git a/runtime/js/10_permissions.js b/runtime/js/10_permissions.js index 2d3295f854..4519f14a2f 100644 --- a/runtime/js/10_permissions.js +++ b/runtime/js/10_permissions.js @@ -1,6 +1,6 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -import { core, internals, primordials } from "ext:core/mod.js"; +import { core, primordials } from "ext:core/mod.js"; const ops = core.ops; import { pathFromURL } from "ext:deno_web/00_infra.js"; import { Event, EventTarget } from "ext:deno_web/02_event.js"; @@ -290,6 +290,4 @@ function serializePermissions(permissions) { return permissions; } -internals.serializePermissions = serializePermissions; - export { Permissions, permissions, PermissionStatus, serializePermissions }; diff --git a/runtime/js/30_os.js b/runtime/js/30_os.js index 5bc96e54b2..f6d6556338 100644 --- a/runtime/js/30_os.js +++ b/runtime/js/30_os.js @@ -1,6 +1,6 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -import { core, internals, primordials } from "ext:core/mod.js"; +import { core, primordials } from "ext:core/mod.js"; const ops = core.ops; import { Event, EventTarget } from "ext:deno_web/02_event.js"; const { @@ -105,8 +105,6 @@ function execPath() { return ops.op_exec_path(); } -internals.setExitHandler = setExitHandler; - export { env, execPath, diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index 5b8c1944aa..4244f1045c 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -43,7 +43,6 @@ use deno_fs::FileSystem; use deno_http::DefaultHttpPropertyExtractor; use deno_io::Stdio; use deno_kv::dynamic::MultiBackendDbHandler; -use deno_node::SUPPORTED_BUILTIN_NODE_MODULES_WITH_PREFIX; use deno_tls::RootCertStoreProvider; use deno_web::create_entangled_message_port; use deno_web::BlobStore; @@ -523,11 +522,6 @@ impl WebWorker { (None, None) }; - // Clear extension modules from the module map, except preserve `node:*` - // modules as `node:` specifiers. - let preserve_snapshotted_modules = - Some(SUPPORTED_BUILTIN_NODE_MODULES_WITH_PREFIX); - let mut js_runtime = JsRuntime::new(RuntimeOptions { module_loader: Some(options.module_loader.clone()), startup_snapshot: options @@ -539,7 +533,6 @@ impl WebWorker { compiled_wasm_module_store: options.compiled_wasm_module_store.clone(), extensions, inspector: options.maybe_inspector_server.is_some(), - preserve_snapshotted_modules, feature_checker: Some(options.feature_checker.clone()), op_metrics_factory_fn, ..Default::default() diff --git a/runtime/worker.rs b/runtime/worker.rs index 3267bd78d1..2a7e82c545 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -39,7 +39,6 @@ use deno_fs::FileSystem; use deno_http::DefaultHttpPropertyExtractor; use deno_io::Stdio; use deno_kv::dynamic::MultiBackendDbHandler; -use deno_node::SUPPORTED_BUILTIN_NODE_MODULES_WITH_PREFIX; use deno_tls::RootCertStoreProvider; use deno_web::BlobStore; use log::debug; @@ -420,11 +419,6 @@ impl MainWorker { #[cfg(all(feature = "include_js_files_for_snapshotting", feature = "dont_create_runtime_snapshot", not(feature = "__runtime_js_sources")))] options.startup_snapshot.as_ref().expect("Sources are not embedded, snapshotting was disabled and a user snapshot was not provided."); - // Clear extension modules from the module map, except preserve `node:*` - // modules. - let preserve_snapshotted_modules = - Some(SUPPORTED_BUILTIN_NODE_MODULES_WITH_PREFIX); - let mut js_runtime = JsRuntime::new(RuntimeOptions { module_loader: Some(options.module_loader.clone()), startup_snapshot: options @@ -437,7 +431,6 @@ impl MainWorker { shared_array_buffer_store: options.shared_array_buffer_store.clone(), compiled_wasm_module_store: options.compiled_wasm_module_store.clone(), extensions, - preserve_snapshotted_modules, inspector: options.maybe_inspector_server.is_some(), is_main: true, feature_checker: Some(options.feature_checker.clone()),