From 7038074c8583465872b16083f54f2211312f0943 Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Thu, 25 Jan 2024 14:54:35 -0500 Subject: [PATCH] chore(cli): split 40_testing (#22112) No code changes -- just splitting 40_testing into three files and removing a couple of unused lines of code. --- cli/js/40_bench.js | 426 +++++++++++++++++++ cli/js/{40_testing.js => 40_test.js} | 590 +++------------------------ cli/js/40_test_common.js | 60 +++ cli/worker.rs | 22 +- 4 files changed, 561 insertions(+), 537 deletions(-) create mode 100644 cli/js/40_bench.js rename cli/js/{40_testing.js => 40_test.js} (73%) create mode 100644 cli/js/40_test_common.js diff --git a/cli/js/40_bench.js b/cli/js/40_bench.js new file mode 100644 index 0000000000..b7a93038c0 --- /dev/null +++ b/cli/js/40_bench.js @@ -0,0 +1,426 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file + +import { core, primordials } from "ext:core/mod.js"; +import { + escapeName, + pledgePermissions, + restorePermissions, +} from "ext:cli/40_test_common.js"; +import { Console } from "ext:deno_console/01_console.js"; +import { setExitHandler } from "ext:runtime/30_os.js"; +const ops = core.ops; +const { + ArrayPrototypePush, + Error, + MathCeil, + SymbolToStringTag, + TypeError, +} = primordials; + +/** @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; + +let registeredWarmupBench = false; + +// Main bench function provided by Deno. +function bench( + nameOrFnOrOptions, + optionsOrFn, + maybeFn, +) { + 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 = { + ignore: false, + baseline: false, + only: false, + sanitizeExit: true, + permissions: null, + }; + + if (typeof nameOrFnOrOptions === "string") { + if (!nameOrFnOrOptions) { + throw new TypeError("The bench name can't be empty"); + } + if (typeof optionsOrFn === "function") { + benchDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults }; + } else { + if (!maybeFn || typeof maybeFn !== "function") { + throw new TypeError("Missing bench function"); + } + if (optionsOrFn.fn != undefined) { + throw new TypeError( + "Unexpected 'fn' field in options, bench function is already provided as the third argument.", + ); + } + if (optionsOrFn.name != undefined) { + throw new TypeError( + "Unexpected 'name' field in options, bench name is already provided as the first argument.", + ); + } + benchDesc = { + ...defaults, + ...optionsOrFn, + fn: maybeFn, + name: nameOrFnOrOptions, + }; + } + } else if (typeof nameOrFnOrOptions === "function") { + if (!nameOrFnOrOptions.name) { + throw new TypeError("The bench function must have a name"); + } + if (optionsOrFn != undefined) { + throw new TypeError("Unexpected second argument to Deno.bench()"); + } + if (maybeFn != undefined) { + throw new TypeError("Unexpected third argument to Deno.bench()"); + } + benchDesc = { + ...defaults, + fn: nameOrFnOrOptions, + name: nameOrFnOrOptions.name, + }; + } else { + let fn; + let name; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + if (nameOrFnOrOptions.fn != undefined) { + throw new TypeError( + "Unexpected 'fn' field in options, bench function is already provided as the second argument.", + ); + } + name = nameOrFnOrOptions.name ?? fn.name; + } else { + if ( + !nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function" + ) { + throw new TypeError( + "Expected 'fn' field in the first argument to be a bench function.", + ); + } + fn = nameOrFnOrOptions.fn; + name = nameOrFnOrOptions.name ?? fn.name; + } + if (!name) { + throw new TypeError("The bench name can't be empty"); + } + benchDesc = { ...defaults, ...nameOrFnOrOptions, fn, name }; + } + + const AsyncFunction = (async () => {}).constructor; + benchDesc.async = AsyncFunction === benchDesc.fn.constructor; + benchDesc.fn = wrapBenchmark(benchDesc); + benchDesc.warmup = false; + benchDesc.name = escapeName(benchDesc.name); + + const { id, origin } = ops.op_register_bench(benchDesc); + benchDesc.id = id; + benchDesc.origin = origin; +} + +function compareMeasurements(a, b) { + if (a > b) return 1; + if (a < b) return -1; + + return 0; +} + +function benchStats( + n, + highPrecision, + usedExplicitTimers, + avg, + min, + max, + all, +) { + return { + n, + min, + max, + p75: all[MathCeil(n * (75 / 100)) - 1], + p99: all[MathCeil(n * (99 / 100)) - 1], + p995: all[MathCeil(n * (99.5 / 100)) - 1], + p999: all[MathCeil(n * (99.9 / 100)) - 1], + avg: !highPrecision ? (avg / n) : MathCeil(avg / n), + highPrecision, + usedExplicitTimers, + }; +} + +async function benchMeasure(timeBudget, fn, async, context) { + let n = 0; + let avg = 0; + let wavg = 0; + let usedExplicitTimers = false; + const all = []; + let min = Infinity; + let max = -Infinity; + const lowPrecisionThresholdInNs = 1e4; + + // warmup step + let c = 0; + let iterations = 20; + let budget = 10 * 1e6; + + if (!async) { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + fn(context); + const t2 = benchNow(); + const totalTime = t2 - t1; + if (currentBenchUserExplicitStart !== null) { + currentBenchUserExplicitStart = null; + usedExplicitTimers = true; + } + if (currentBenchUserExplicitEnd !== null) { + currentBenchUserExplicitEnd = null; + usedExplicitTimers = true; + } + + c++; + wavg += totalTime; + budget -= totalTime; + } + } else { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + await fn(context); + const t2 = benchNow(); + const totalTime = t2 - t1; + if (currentBenchUserExplicitStart !== null) { + currentBenchUserExplicitStart = null; + usedExplicitTimers = true; + } + if (currentBenchUserExplicitEnd !== null) { + currentBenchUserExplicitEnd = null; + usedExplicitTimers = true; + } + + c++; + wavg += totalTime; + budget -= totalTime; + } + } + + wavg /= c; + + // measure step + if (wavg > lowPrecisionThresholdInNs) { + let iterations = 10; + let budget = timeBudget * 1e6; + + if (!async) { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + fn(context); + const t2 = benchNow(); + const totalTime = t2 - t1; + let measuredTime = totalTime; + if (currentBenchUserExplicitStart !== null) { + measuredTime -= currentBenchUserExplicitStart - t1; + currentBenchUserExplicitStart = null; + } + if (currentBenchUserExplicitEnd !== null) { + measuredTime -= t2 - currentBenchUserExplicitEnd; + currentBenchUserExplicitEnd = null; + } + + n++; + avg += measuredTime; + budget -= totalTime; + ArrayPrototypePush(all, measuredTime); + if (measuredTime < min) min = measuredTime; + if (measuredTime > max) max = measuredTime; + } + } else { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + await fn(context); + const t2 = benchNow(); + const totalTime = t2 - t1; + let measuredTime = totalTime; + if (currentBenchUserExplicitStart !== null) { + measuredTime -= currentBenchUserExplicitStart - t1; + currentBenchUserExplicitStart = null; + } + if (currentBenchUserExplicitEnd !== null) { + measuredTime -= t2 - currentBenchUserExplicitEnd; + currentBenchUserExplicitEnd = null; + } + + n++; + avg += measuredTime; + budget -= totalTime; + ArrayPrototypePush(all, measuredTime); + if (measuredTime < min) min = measuredTime; + if (measuredTime > max) max = measuredTime; + } + } + } else { + context.start = function start() {}; + context.end = function end() {}; + let iterations = 10; + let budget = timeBudget * 1e6; + + if (!async) { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + for (let c = 0; c < lowPrecisionThresholdInNs; c++) { + fn(context); + } + const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs; + + n++; + avg += iterationTime; + ArrayPrototypePush(all, iterationTime); + if (iterationTime < min) min = iterationTime; + if (iterationTime > max) max = iterationTime; + budget -= iterationTime * lowPrecisionThresholdInNs; + } + } else { + while (budget > 0 || iterations-- > 0) { + const t1 = benchNow(); + for (let c = 0; c < lowPrecisionThresholdInNs; c++) { + await fn(context); + currentBenchUserExplicitStart = null; + currentBenchUserExplicitEnd = null; + } + const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs; + + n++; + avg += iterationTime; + ArrayPrototypePush(all, iterationTime); + if (iterationTime < min) min = iterationTime; + if (iterationTime > max) max = iterationTime; + budget -= iterationTime * lowPrecisionThresholdInNs; + } + } + } + + all.sort(compareMeasurements); + return benchStats( + n, + wavg > lowPrecisionThresholdInNs, + usedExplicitTimers, + avg, + min, + max, + all, + ); +} + +/** @param desc {BenchDescription} */ +function createBenchContext(desc) { + return { + [SymbolToStringTag]: "BenchContext", + name: desc.name, + origin: desc.origin, + start() { + if (currentBenchId !== desc.id) { + throw new TypeError( + "The benchmark which this context belongs to is not being executed.", + ); + } + if (currentBenchUserExplicitStart != null) { + throw new TypeError( + "BenchContext::start() has already been invoked.", + ); + } + currentBenchUserExplicitStart = benchNow(); + }, + end() { + const end = benchNow(); + if (currentBenchId !== desc.id) { + throw new TypeError( + "The benchmark which this context belongs to is not being executed.", + ); + } + if (currentBenchUserExplicitEnd != null) { + throw new TypeError("BenchContext::end() has already been invoked."); + } + currentBenchUserExplicitEnd = end; + }, + }; +} + +/** Wrap a user benchmark function in one which returns a structured result. */ +function wrapBenchmark(desc) { + const fn = desc.fn; + return async function outerWrapped() { + let token = null; + const originalConsole = globalThis.console; + currentBenchId = desc.id; + + try { + globalThis.console = new Console((s) => { + ops.op_dispatch_bench_event({ output: s }); + }); + + if (desc.permissions) { + token = pledgePermissions(desc.permissions); + } + + if (desc.sanitizeExit) { + setExitHandler((exitCode) => { + throw new Error( + `Bench attempted to exit with exit code: ${exitCode}`, + ); + }); + } + + const benchTimeInMs = 500; + const context = createBenchContext(desc); + const stats = await benchMeasure( + benchTimeInMs, + fn, + desc.async, + context, + ); + + return { ok: stats }; + } catch (error) { + return { failed: core.destructureError(error) }; + } finally { + globalThis.console = originalConsole; + currentBenchId = null; + currentBenchUserExplicitStart = null; + currentBenchUserExplicitEnd = null; + if (bench.sanitizeExit) setExitHandler(null); + if (token !== null) restorePermissions(token); + } + }; +} + +function benchNow() { + return ops.op_bench_now(); +} + +globalThis.Deno.bench = bench; diff --git a/cli/js/40_testing.js b/cli/js/40_test.js similarity index 73% rename from cli/js/40_testing.js rename to cli/js/40_test.js index 23a8da71ba..750cbe7db0 100644 --- a/cli/js/40_testing.js +++ b/cli/js/40_test.js @@ -1,7 +1,8 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// deno-lint-ignore-file import { core, primordials } from "ext:core/mod.js"; +import { escapeName, withPermissions } from "ext:cli/40_test_common.js"; + const ops = core.ops; const { ArrayPrototypeFilter, @@ -14,21 +15,76 @@ const { MapPrototypeGet, MapPrototypeHas, MapPrototypeSet, - MathCeil, ObjectKeys, Promise, SafeArrayIterator, Set, - StringPrototypeReplaceAll, SymbolToStringTag, TypeError, } = primordials; import { setExitHandler } from "ext:runtime/30_os.js"; -import { Console } from "ext:deno_console/01_console.js"; -import { serializePermissions } from "ext:runtime/10_permissions.js"; import { setTimeout } from "ext:deno_web/02_timers.js"; +/** + * @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(); + const opSanitizerDelayResolveQueue = []; let hasSetOpSanitizerDelayMacrotask = false; @@ -560,126 +616,6 @@ function wrapInner(fn) { }; } -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); @@ -689,10 +625,6 @@ function testInner( maybeFn, overrides = {}, ) { - if (typeof ops.op_register_test != "function") { - return; - } - let testDesc; const defaults = { ignore: false, @@ -822,405 +754,6 @@ test.only = function ( 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 = { - ignore: false, - baseline: false, - only: false, - sanitizeExit: true, - permissions: null, - }; - - if (typeof nameOrFnOrOptions === "string") { - if (!nameOrFnOrOptions) { - throw new TypeError("The bench name can't be empty"); - } - if (typeof optionsOrFn === "function") { - benchDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults }; - } else { - if (!maybeFn || typeof maybeFn !== "function") { - throw new TypeError("Missing bench function"); - } - if (optionsOrFn.fn != undefined) { - throw new TypeError( - "Unexpected 'fn' field in options, bench function is already provided as the third argument.", - ); - } - if (optionsOrFn.name != undefined) { - throw new TypeError( - "Unexpected 'name' field in options, bench name is already provided as the first argument.", - ); - } - benchDesc = { - ...defaults, - ...optionsOrFn, - fn: maybeFn, - name: nameOrFnOrOptions, - }; - } - } else if (typeof nameOrFnOrOptions === "function") { - if (!nameOrFnOrOptions.name) { - throw new TypeError("The bench function must have a name"); - } - if (optionsOrFn != undefined) { - throw new TypeError("Unexpected second argument to Deno.bench()"); - } - if (maybeFn != undefined) { - throw new TypeError("Unexpected third argument to Deno.bench()"); - } - benchDesc = { - ...defaults, - fn: nameOrFnOrOptions, - name: nameOrFnOrOptions.name, - }; - } else { - let fn; - let name; - if (typeof optionsOrFn === "function") { - fn = optionsOrFn; - if (nameOrFnOrOptions.fn != undefined) { - throw new TypeError( - "Unexpected 'fn' field in options, bench function is already provided as the second argument.", - ); - } - name = nameOrFnOrOptions.name ?? fn.name; - } else { - if ( - !nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function" - ) { - throw new TypeError( - "Expected 'fn' field in the first argument to be a bench function.", - ); - } - fn = nameOrFnOrOptions.fn; - name = nameOrFnOrOptions.name ?? fn.name; - } - if (!name) { - throw new TypeError("The bench name can't be empty"); - } - benchDesc = { ...defaults, ...nameOrFnOrOptions, fn, name }; - } - - const AsyncFunction = (async () => {}).constructor; - benchDesc.async = AsyncFunction === benchDesc.fn.constructor; - benchDesc.fn = wrapBenchmark(benchDesc); - benchDesc.warmup = false; - benchDesc.name = escapeName(benchDesc.name); - - const { id, origin } = ops.op_register_bench(benchDesc); - benchDesc.id = id; - benchDesc.origin = origin; -} - -function compareMeasurements(a, b) { - if (a > b) return 1; - if (a < b) return -1; - - return 0; -} - -function benchStats( - n, - highPrecision, - usedExplicitTimers, - avg, - min, - max, - all, -) { - return { - n, - min, - max, - p75: all[MathCeil(n * (75 / 100)) - 1], - p99: all[MathCeil(n * (99 / 100)) - 1], - p995: all[MathCeil(n * (99.5 / 100)) - 1], - p999: all[MathCeil(n * (99.9 / 100)) - 1], - avg: !highPrecision ? (avg / n) : MathCeil(avg / n), - highPrecision, - usedExplicitTimers, - }; -} - -async function benchMeasure(timeBudget, fn, async, context) { - let n = 0; - let avg = 0; - let wavg = 0; - let usedExplicitTimers = false; - const all = []; - let min = Infinity; - let max = -Infinity; - const lowPrecisionThresholdInNs = 1e4; - - // warmup step - let c = 0; - let iterations = 20; - let budget = 10 * 1e6; - - if (!async) { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - fn(context); - const t2 = benchNow(); - const totalTime = t2 - t1; - if (currentBenchUserExplicitStart !== null) { - currentBenchUserExplicitStart = null; - usedExplicitTimers = true; - } - if (currentBenchUserExplicitEnd !== null) { - currentBenchUserExplicitEnd = null; - usedExplicitTimers = true; - } - - c++; - wavg += totalTime; - budget -= totalTime; - } - } else { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - await fn(context); - const t2 = benchNow(); - const totalTime = t2 - t1; - if (currentBenchUserExplicitStart !== null) { - currentBenchUserExplicitStart = null; - usedExplicitTimers = true; - } - if (currentBenchUserExplicitEnd !== null) { - currentBenchUserExplicitEnd = null; - usedExplicitTimers = true; - } - - c++; - wavg += totalTime; - budget -= totalTime; - } - } - - wavg /= c; - - // measure step - if (wavg > lowPrecisionThresholdInNs) { - let iterations = 10; - let budget = timeBudget * 1e6; - - if (!async) { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - fn(context); - const t2 = benchNow(); - const totalTime = t2 - t1; - let measuredTime = totalTime; - if (currentBenchUserExplicitStart !== null) { - measuredTime -= currentBenchUserExplicitStart - t1; - currentBenchUserExplicitStart = null; - } - if (currentBenchUserExplicitEnd !== null) { - measuredTime -= t2 - currentBenchUserExplicitEnd; - currentBenchUserExplicitEnd = null; - } - - n++; - avg += measuredTime; - budget -= totalTime; - ArrayPrototypePush(all, measuredTime); - if (measuredTime < min) min = measuredTime; - if (measuredTime > max) max = measuredTime; - } - } else { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - await fn(context); - const t2 = benchNow(); - const totalTime = t2 - t1; - let measuredTime = totalTime; - if (currentBenchUserExplicitStart !== null) { - measuredTime -= currentBenchUserExplicitStart - t1; - currentBenchUserExplicitStart = null; - } - if (currentBenchUserExplicitEnd !== null) { - measuredTime -= t2 - currentBenchUserExplicitEnd; - currentBenchUserExplicitEnd = null; - } - - n++; - avg += measuredTime; - budget -= totalTime; - ArrayPrototypePush(all, measuredTime); - if (measuredTime < min) min = measuredTime; - if (measuredTime > max) max = measuredTime; - } - } - } else { - context.start = function start() {}; - context.end = function end() {}; - let iterations = 10; - let budget = timeBudget * 1e6; - - if (!async) { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - for (let c = 0; c < lowPrecisionThresholdInNs; c++) { - fn(context); - } - const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs; - - n++; - avg += iterationTime; - ArrayPrototypePush(all, iterationTime); - if (iterationTime < min) min = iterationTime; - if (iterationTime > max) max = iterationTime; - budget -= iterationTime * lowPrecisionThresholdInNs; - } - } else { - while (budget > 0 || iterations-- > 0) { - const t1 = benchNow(); - for (let c = 0; c < lowPrecisionThresholdInNs; c++) { - await fn(context); - currentBenchUserExplicitStart = null; - currentBenchUserExplicitEnd = null; - } - const iterationTime = (benchNow() - t1) / lowPrecisionThresholdInNs; - - n++; - avg += iterationTime; - ArrayPrototypePush(all, iterationTime); - if (iterationTime < min) min = iterationTime; - if (iterationTime > max) max = iterationTime; - budget -= iterationTime * lowPrecisionThresholdInNs; - } - } - } - - all.sort(compareMeasurements); - return benchStats( - n, - wavg > lowPrecisionThresholdInNs, - usedExplicitTimers, - avg, - min, - max, - all, - ); -} - -/** @param desc {BenchDescription} */ -function createBenchContext(desc) { - return { - [SymbolToStringTag]: "BenchContext", - name: desc.name, - origin: desc.origin, - start() { - if (currentBenchId !== desc.id) { - throw new TypeError( - "The benchmark which this context belongs to is not being executed.", - ); - } - if (currentBenchUserExplicitStart != null) { - throw new TypeError( - "BenchContext::start() has already been invoked.", - ); - } - currentBenchUserExplicitStart = benchNow(); - }, - end() { - const end = benchNow(); - if (currentBenchId !== desc.id) { - throw new TypeError( - "The benchmark which this context belongs to is not being executed.", - ); - } - if (currentBenchUserExplicitEnd != null) { - throw new TypeError("BenchContext::end() has already been invoked."); - } - currentBenchUserExplicitEnd = end; - }, - }; -} - -/** Wrap a user benchmark function in one which returns a structured result. */ -function wrapBenchmark(desc) { - const fn = desc.fn; - return async function outerWrapped() { - let token = null; - const originalConsole = globalThis.console; - currentBenchId = desc.id; - - try { - globalThis.console = new Console((s) => { - ops.op_dispatch_bench_event({ output: s }); - }); - - if (desc.permissions) { - token = pledgePermissions(desc.permissions); - } - - if (desc.sanitizeExit) { - setExitHandler((exitCode) => { - throw new Error( - `Bench attempted to exit with exit code: ${exitCode}`, - ); - }); - } - - const benchTimeInMs = 500; - const context = createBenchContext(desc); - const stats = await benchMeasure( - benchTimeInMs, - fn, - desc.async, - context, - ); - - return { ok: stats }; - } catch (error) { - return { failed: core.destructureError(error) }; - } finally { - globalThis.console = originalConsole; - currentBenchId = null; - currentBenchUserExplicitStart = null; - currentBenchUserExplicitEnd = null; - if (bench.sanitizeExit) setExitHandler(null); - if (token !== null) restorePermissions(token); - } - }; -} - -function benchNow() { - return ops.op_bench_now(); -} - function getFullName(desc) { if ("parent" in desc) { return `${getFullName(desc.parent)} ... ${desc.name}`; @@ -1388,5 +921,4 @@ function wrapTest(desc) { return wrapOuter(testFn, desc); } -globalThis.Deno.bench = bench; globalThis.Deno.test = test; diff --git a/cli/js/40_test_common.js b/cli/js/40_test_common.js new file mode 100644 index 0000000000..7711148f1e --- /dev/null +++ b/cli/js/40_test_common.js @@ -0,0 +1,60 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { core, primordials } from "ext:core/mod.js"; +import { serializePermissions } from "ext:runtime/10_permissions.js"; +const ops = core.ops; +const { + StringPrototypeReplaceAll, + SafeArrayIterator, +} = primordials; + +const ESCAPE_ASCII_CHARS = [ + ["\b", "\\b"], + ["\f", "\\f"], + ["\t", "\\t"], + ["\n", "\\n"], + ["\r", "\\r"], + ["\v", "\\v"], +]; + +/** + * @param {string} name + * @returns {string} + */ +export 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; +} + +export function pledgePermissions(permissions) { + return ops.op_pledge_test_permissions( + serializePermissions(permissions), + ); +} + +export function restorePermissions(token) { + ops.op_restore_test_permissions(token); +} + +export function withPermissions(fn, permissions) { + return async function applyPermissions(...params) { + const token = pledgePermissions(permissions); + + try { + return await fn(...new SafeArrayIterator(params)); + } finally { + restorePermissions(token); + } + }; +} diff --git a/cli/worker.rs b/cli/worker.rs index 4807e2699c..e0f76716b2 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -633,14 +633,20 @@ impl CliMainWorkerFactory { ); if self.shared.subcommand.needs_test() { - worker.js_runtime.lazy_load_es_module_from_code( - "ext:cli/40_testing.js", - deno_core::FastString::StaticAscii(include_str!("js/40_testing.js")), - )?; - worker.js_runtime.lazy_load_es_module_from_code( - "ext:cli/40_jupyter.js", - deno_core::FastString::StaticAscii(include_str!("js/40_jupyter.js")), - )?; + macro_rules! test_file { + ($($file:literal),*) => { + $(worker.js_runtime.lazy_load_es_module_from_code( + concat!("ext:cli/", $file), + deno_core::FastString::StaticAscii(include_str!(concat!("js/", $file))), + )?;)* + } + } + test_file!( + "40_test_common.js", + "40_test.js", + "40_bench.js", + "40_jupyter.js" + ); } Ok(CliMainWorker {