summaryrefslogtreecommitdiff
path: root/cli/js
diff options
context:
space:
mode:
authorMatt Mastracci <matthew@mastracci.com>2024-01-25 14:54:35 -0500
committerGitHub <noreply@github.com>2024-01-25 14:54:35 -0500
commit7038074c8583465872b16083f54f2211312f0943 (patch)
tree71cfc7164d40b493cdd4800f130681bdf363ac45 /cli/js
parente06be89143e0cfc85896ee0b89f2e4cce8ba8d44 (diff)
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.
Diffstat (limited to 'cli/js')
-rw-r--r--cli/js/40_bench.js426
-rw-r--r--cli/js/40_test.js (renamed from cli/js/40_testing.js)590
-rw-r--r--cli/js/40_test_common.js60
3 files changed, 547 insertions, 529 deletions
diff --git a/cli/js/40_bench.js b/cli/js/40_bench.js
new file mode 100644
index 000000000..b7a93038c
--- /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: "<warmup>",
+ 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
index 23a8da71b..750cbe7db 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<number, TestState | TestStepState>} */
+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<number, TestState | TestStepState>} */
-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: "<warmup>",
- 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 000000000..7711148f1
--- /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);
+ }
+ };
+}