refactor(cli): move runTests() and runBenchmarks() to rust (#18563)

Stores the test/bench functions in rust op state during registration.
The functions are wrapped in JS first so that they return a directly
convertible `TestResult`/`BenchResult`. Test steps are still mostly
handled in JS since they are pretty much invoked by the user. Allows
removing a bunch of infrastructure for communicating between JS and
rust. Allows using rust utilities for things like shuffling tests
(`Vec::shuffle`). We can progressively move op and resource sanitization
to rust as well.

Fixes #17122.
Fixes #17312.
This commit is contained in:
Nayeem Rahman 2023-04-13 18:43:23 +01:00 committed by GitHub
parent 4e53bc5a94
commit 6e8618ae0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 450 additions and 705 deletions

View file

@ -2,21 +2,16 @@
const core = globalThis.Deno.core; const core = globalThis.Deno.core;
const ops = core.ops; const ops = core.ops;
const internals = globalThis.__bootstrap.internals;
import { setExitHandler } from "ext:runtime/30_os.js"; import { setExitHandler } from "ext:runtime/30_os.js";
import { Console } from "ext:deno_console/02_console.js"; import { Console } from "ext:deno_console/02_console.js";
import { serializePermissions } from "ext:runtime/10_permissions.js"; import { serializePermissions } from "ext:runtime/10_permissions.js";
import { assert } from "ext:deno_web/00_infra.js"; import { assert } from "ext:deno_web/00_infra.js";
const primordials = globalThis.__bootstrap.primordials; const primordials = globalThis.__bootstrap.primordials;
const { const {
ArrayFrom,
ArrayPrototypeFilter, ArrayPrototypeFilter,
ArrayPrototypeJoin, ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush, ArrayPrototypePush,
ArrayPrototypeShift, ArrayPrototypeShift,
ArrayPrototypeSort,
BigInt,
DateNow, DateNow,
Error, Error,
FunctionPrototype, FunctionPrototype,
@ -36,6 +31,7 @@ const {
} = primordials; } = primordials;
const opSanitizerDelayResolveQueue = []; const opSanitizerDelayResolveQueue = [];
let hasSetOpSanitizerDelayMacrotask = false;
// Even if every resource is closed by the end of a test, there can be a delay // 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 // until the pending ops have all finished. This function returns a promise
@ -47,6 +43,10 @@ const opSanitizerDelayResolveQueue = [];
// before that, though, in order to give time for worker message ops to finish // before that, though, in order to give time for worker message ops to finish
// (since timeouts of 0 don't queue tasks in the timer queue immediately). // (since timeouts of 0 don't queue tasks in the timer queue immediately).
function opSanitizerDelay() { function opSanitizerDelay() {
if (!hasSetOpSanitizerDelayMacrotask) {
core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask);
hasSetOpSanitizerDelayMacrotask = true;
}
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
ArrayPrototypePush(opSanitizerDelayResolveQueue, resolve); ArrayPrototypePush(opSanitizerDelayResolveQueue, resolve);
@ -415,9 +415,28 @@ function assertExit(fn, isTest) {
}; };
} }
function assertTestStepScopes(fn) { 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 = 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} */ /** @param desc {TestDescription | TestStepDescription} */
return async function testStepSanitizer(desc) { return async function innerWrapped(desc) {
function getRunningStepDescs() { function getRunningStepDescs() {
const results = []; const results = [];
let childDesc = desc; let childDesc = desc;
@ -458,11 +477,17 @@ function assertTestStepScopes(fn) {
}; };
} }
await fn(MapPrototypeGet(testStates, desc.id).context); await fn(MapPrototypeGet(testStates, desc.id).context);
let failedSteps = 0;
for (const childDesc of MapPrototypeGet(testStates, desc.id).children) { for (const childDesc of MapPrototypeGet(testStates, desc.id).children) {
if (!MapPrototypeGet(testStates, childDesc.id).completed) { const state = MapPrototypeGet(testStates, childDesc.id);
if (!state.completed) {
return { failed: "incompleteSteps" }; return { failed: "incompleteSteps" };
} }
if (state.failed) {
failedSteps++;
}
} }
return failedSteps == 0 ? null : { failed: { failedSteps } };
}; };
} }
@ -495,7 +520,6 @@ function withPermissions(fn, permissions) {
* fn: TestFunction * fn: TestFunction
* origin: string, * origin: string,
* location: TestLocation, * location: TestLocation,
* filteredOut: boolean,
* ignore: boolean, * ignore: boolean,
* only: boolean. * only: boolean.
* sanitizeOps: boolean, * sanitizeOps: boolean,
@ -538,7 +562,6 @@ function withPermissions(fn, permissions) {
* name: string, * name: string,
* fn: BenchFunction * fn: BenchFunction
* origin: string, * origin: string,
* filteredOut: boolean,
* ignore: boolean, * ignore: boolean,
* only: boolean. * only: boolean.
* sanitizeExit: boolean, * sanitizeExit: boolean,
@ -546,14 +569,8 @@ function withPermissions(fn, permissions) {
* }} BenchDescription * }} BenchDescription
*/ */
/** @type {TestDescription[]} */
const testDescs = [];
/** @type {Map<number, TestState | TestStepState>} */ /** @type {Map<number, TestState | TestStepState>} */
const testStates = new Map(); const testStates = new Map();
/** @type {BenchDescription[]} */
const benchDescs = [];
let isTestSubcommand = false;
let isBenchSubcommand = false;
// Main test function provided by Deno. // Main test function provided by Deno.
function test( function test(
@ -561,7 +578,7 @@ function test(
optionsOrFn, optionsOrFn,
maybeFn, maybeFn,
) { ) {
if (!isTestSubcommand) { if (typeof ops.op_register_test != "function") {
return; return;
} }
@ -647,19 +664,17 @@ function test(
// Delete this prop in case the user passed it. It's used to detect steps. // Delete this prop in case the user passed it. It's used to detect steps.
delete testDesc.parent; delete testDesc.parent;
testDesc.origin = getTestOrigin();
const jsError = core.destructureError(new Error()); const jsError = core.destructureError(new Error());
testDesc.location = { testDesc.location = {
fileName: jsError.frames[1].fileName, fileName: jsError.frames[1].fileName,
lineNumber: jsError.frames[1].lineNumber, lineNumber: jsError.frames[1].lineNumber,
columnNumber: jsError.frames[1].columnNumber, columnNumber: jsError.frames[1].columnNumber,
}; };
testDesc.fn = wrapTest(testDesc);
const { id, filteredOut } = ops.op_register_test(testDesc); const { id, origin } = ops.op_register_test(testDesc);
testDesc.id = id; testDesc.id = id;
testDesc.filteredOut = filteredOut; testDesc.origin = origin;
ArrayPrototypePush(testDescs, testDesc);
MapPrototypeSet(testStates, testDesc.id, { MapPrototypeSet(testStates, testDesc.id, {
context: createTestContext(testDesc), context: createTestContext(testDesc),
children: [], children: [],
@ -673,7 +688,7 @@ function bench(
optionsOrFn, optionsOrFn,
maybeFn, maybeFn,
) { ) {
if (!isBenchSubcommand) { if (typeof ops.op_register_bench != "function") {
return; return;
} }
@ -756,36 +771,13 @@ function bench(
benchDesc = { ...defaults, ...nameOrFnOrOptions, fn, name }; benchDesc = { ...defaults, ...nameOrFnOrOptions, fn, name };
} }
benchDesc.origin = getBenchOrigin();
const AsyncFunction = (async () => {}).constructor; const AsyncFunction = (async () => {}).constructor;
benchDesc.async = AsyncFunction === benchDesc.fn.constructor; benchDesc.async = AsyncFunction === benchDesc.fn.constructor;
benchDesc.fn = wrapBenchmark(benchDesc);
const { id, filteredOut } = ops.op_register_bench(benchDesc); const { id, origin } = ops.op_register_bench(benchDesc);
benchDesc.id = id; benchDesc.id = id;
benchDesc.filteredOut = filteredOut; benchDesc.origin = origin;
ArrayPrototypePush(benchDescs, benchDesc);
}
async function runTest(desc) {
if (desc.ignore) {
return "ignored";
}
let testFn = wrapTestFnWithSanitizers(desc.fn, desc);
if (!("parent" in desc) && desc.permissions) {
testFn = withPermissions(
testFn,
desc.permissions,
);
}
try {
const result = await testFn(desc);
if (result) return result;
const failedSteps = failedChildStepsCount(desc);
return failedSteps === 0 ? "ok" : { failed: { failedSteps } };
} catch (error) {
return { failed: { jsError: core.destructureError(error) } };
}
} }
function compareMeasurements(a, b) { function compareMeasurements(a, b) {
@ -808,8 +800,7 @@ function benchStats(n, highPrecision, avg, min, max, all) {
}; };
} }
async function benchMeasure(timeBudget, desc) { async function benchMeasure(timeBudget, fn, async) {
const fn = desc.fn;
let n = 0; let n = 0;
let avg = 0; let avg = 0;
let wavg = 0; let wavg = 0;
@ -823,7 +814,7 @@ async function benchMeasure(timeBudget, desc) {
let iterations = 20; let iterations = 20;
let budget = 10 * 1e6; let budget = 10 * 1e6;
if (!desc.async) { if (!async) {
while (budget > 0 || iterations-- > 0) { while (budget > 0 || iterations-- > 0) {
const t1 = benchNow(); const t1 = benchNow();
@ -854,7 +845,7 @@ async function benchMeasure(timeBudget, desc) {
let iterations = 10; let iterations = 10;
let budget = timeBudget * 1e6; let budget = timeBudget * 1e6;
if (!desc.async) { if (!async) {
while (budget > 0 || iterations-- > 0) { while (budget > 0 || iterations-- > 0) {
const t1 = benchNow(); const t1 = benchNow();
@ -887,7 +878,7 @@ async function benchMeasure(timeBudget, desc) {
let iterations = 10; let iterations = 10;
let budget = timeBudget * 1e6; let budget = timeBudget * 1e6;
if (!desc.async) { if (!async) {
while (budget > 0 || iterations-- > 0) { while (budget > 0 || iterations-- > 0) {
const t1 = benchNow(); const t1 = benchNow();
for (let c = 0; c < lowPrecisionThresholdInNs; c++) fn(); for (let c = 0; c < lowPrecisionThresholdInNs; c++) fn();
@ -920,173 +911,49 @@ async function benchMeasure(timeBudget, desc) {
return benchStats(n, wavg > lowPrecisionThresholdInNs, avg, min, max, all); return benchStats(n, wavg > lowPrecisionThresholdInNs, avg, min, max, all);
} }
async function runBench(desc) { /** Wrap a user benchmark function in one which returns a structured result. */
let token = null; function wrapBenchmark(desc) {
const fn = desc.fn;
return async function outerWrapped() {
let token = null;
const originalConsole = globalThis.console;
try { try {
if (desc.permissions) { globalThis.console = new Console((s) => {
token = pledgePermissions(desc.permissions); ops.op_dispatch_bench_event({ output: s });
}
if (desc.sanitizeExit) {
setExitHandler((exitCode) => {
assert(
false,
`Bench attempted to exit with exit code: ${exitCode}`,
);
}); });
if (desc.permissions) {
token = pledgePermissions(desc.permissions);
}
if (desc.sanitizeExit) {
setExitHandler((exitCode) => {
assert(
false,
`Bench attempted to exit with exit code: ${exitCode}`,
);
});
}
const benchTimeInMs = 500;
const stats = await benchMeasure(benchTimeInMs, fn, desc.async);
return { ok: stats };
} catch (error) {
return { failed: core.destructureError(error) };
} finally {
globalThis.console = originalConsole;
if (bench.sanitizeExit) setExitHandler(null);
if (token !== null) restorePermissions(token);
} }
};
const benchTimeInMs = 500;
const stats = await benchMeasure(benchTimeInMs, desc);
return { ok: stats };
} catch (error) {
return { failed: core.destructureError(error) };
} finally {
if (bench.sanitizeExit) setExitHandler(null);
if (token !== null) restorePermissions(token);
}
}
let origin = null;
function getTestOrigin() {
if (origin == null) {
origin = ops.op_get_test_origin();
}
return origin;
}
function getBenchOrigin() {
if (origin == null) {
origin = ops.op_get_bench_origin();
}
return origin;
} }
function benchNow() { function benchNow() {
return ops.op_bench_now(); return ops.op_bench_now();
} }
function enableTest() {
isTestSubcommand = true;
}
function enableBench() {
isBenchSubcommand = true;
}
async function runTests({
shuffle = null,
} = {}) {
core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask);
const origin = getTestOrigin();
const only = ArrayPrototypeFilter(testDescs, (test) => test.only);
const filtered = ArrayPrototypeFilter(
only.length > 0 ? only : testDescs,
(desc) => !desc.filteredOut,
);
ops.op_dispatch_test_event({
plan: {
origin,
total: filtered.length,
filteredOut: testDescs.length - filtered.length,
usedOnly: only.length > 0,
},
});
if (shuffle !== null) {
// http://en.wikipedia.org/wiki/Linear_congruential_generator
// Use BigInt for everything because the random seed is u64.
const nextInt = function (state) {
const m = 0x80000000n;
const a = 1103515245n;
const c = 12345n;
return function (max) {
return state = ((a * state + c) % m) % BigInt(max);
};
}(BigInt(shuffle));
for (let i = filtered.length - 1; i > 0; i--) {
const j = nextInt(i);
[filtered[i], filtered[j]] = [filtered[j], filtered[i]];
}
}
for (const desc of filtered) {
if (ops.op_tests_should_stop()) {
break;
}
ops.op_dispatch_test_event({ wait: desc.id });
const earlier = DateNow();
const result = await runTest(desc);
const elapsed = DateNow() - earlier;
const state = MapPrototypeGet(testStates, desc.id);
state.completed = true;
for (const childDesc of state.children) {
stepReportResult(childDesc, { failed: "incomplete" }, 0);
}
ops.op_dispatch_test_event({
result: [desc.id, result, elapsed],
});
}
}
async function runBenchmarks() {
core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask);
const origin = getBenchOrigin();
const originalConsole = globalThis.console;
globalThis.console = new Console((s) => {
ops.op_dispatch_bench_event({ output: s });
});
const only = ArrayPrototypeFilter(benchDescs, (bench) => bench.only);
const filtered = ArrayPrototypeFilter(
only.length > 0 ? only : benchDescs,
(desc) => !desc.filteredOut && !desc.ignore,
);
let groups = new Set();
// make sure ungrouped benchmarks are placed above grouped
groups.add(undefined);
for (const desc of filtered) {
desc.group ||= undefined;
groups.add(desc.group);
}
groups = ArrayFrom(groups);
ArrayPrototypeSort(
filtered,
(a, b) => groups.indexOf(a.group) - groups.indexOf(b.group),
);
ops.op_dispatch_bench_event({
plan: {
origin,
total: filtered.length,
usedOnly: only.length > 0,
names: ArrayPrototypeMap(filtered, (desc) => desc.name),
},
});
for (const desc of filtered) {
desc.baseline = !!desc.baseline;
ops.op_dispatch_bench_event({ wait: desc.id });
ops.op_dispatch_bench_event({
result: [desc.id, await runBench(desc)],
});
}
globalThis.console = originalConsole;
}
function getFullName(desc) { function getFullName(desc) {
if ("parent" in desc) { if ("parent" in desc) {
return `${getFullName(desc.parent)} ... ${desc.name}`; return `${getFullName(desc.parent)} ... ${desc.name}`;
@ -1108,13 +975,6 @@ function stepReportResult(desc, result, elapsed) {
}); });
} }
function failedChildStepsCount(desc) {
return ArrayPrototypeFilter(
MapPrototypeGet(testStates, desc.id).children,
(d) => MapPrototypeGet(testStates, d.id).failed,
).length;
}
/** @param desc {TestDescription | TestStepDescription} */ /** @param desc {TestDescription | TestStepDescription} */
function createTestContext(desc) { function createTestContext(desc) {
let parent; let parent;
@ -1191,7 +1051,6 @@ function createTestContext(desc) {
stepDesc.sanitizeOps ??= desc.sanitizeOps; stepDesc.sanitizeOps ??= desc.sanitizeOps;
stepDesc.sanitizeResources ??= desc.sanitizeResources; stepDesc.sanitizeResources ??= desc.sanitizeResources;
stepDesc.sanitizeExit ??= desc.sanitizeExit; stepDesc.sanitizeExit ??= desc.sanitizeExit;
stepDesc.origin = getTestOrigin();
const jsError = core.destructureError(new Error()); const jsError = core.destructureError(new Error());
stepDesc.location = { stepDesc.location = {
fileName: jsError.frames[1].fileName, fileName: jsError.frames[1].fileName,
@ -1202,8 +1061,10 @@ function createTestContext(desc) {
stepDesc.parent = desc; stepDesc.parent = desc;
stepDesc.rootId = rootId; stepDesc.rootId = rootId;
stepDesc.rootName = rootName; stepDesc.rootName = rootName;
const { id } = ops.op_register_test_step(stepDesc); stepDesc.fn = wrapTest(stepDesc);
const { id, origin } = ops.op_register_test_step(stepDesc);
stepDesc.id = id; stepDesc.id = id;
stepDesc.origin = origin;
const state = { const state = {
context: createTestContext(stepDesc), context: createTestContext(stepDesc),
children: [], children: [],
@ -1218,10 +1079,9 @@ function createTestContext(desc) {
ops.op_dispatch_test_event({ stepWait: stepDesc.id }); ops.op_dispatch_test_event({ stepWait: stepDesc.id });
const earlier = DateNow(); const earlier = DateNow();
const result = await runTest(stepDesc); const result = await stepDesc.fn(stepDesc);
const elapsed = DateNow() - earlier; const elapsed = DateNow() - earlier;
state.failed = !!result.failed; state.failed = !!result.failed;
state.completed = true;
stepReportResult(stepDesc, result, elapsed); stepReportResult(stepDesc, result, elapsed);
return result == "ok"; return result == "ok";
}, },
@ -1229,37 +1089,29 @@ function createTestContext(desc) {
} }
/** /**
* Wrap a user test function in one which returns a structured result.
* @template T {Function} * @template T {Function}
* @param testFn {T} * @param testFn {T}
* @param opts {{ * @param desc {TestDescription | TestStepDescription}
* sanitizeOps: boolean,
* sanitizeResources: boolean,
* sanitizeExit: boolean,
* }}
* @returns {T} * @returns {T}
*/ */
function wrapTestFnWithSanitizers(testFn, opts) { function wrapTest(desc) {
testFn = assertTestStepScopes(testFn); let testFn = wrapInner(desc.fn);
if (desc.sanitizeOps) {
if (opts.sanitizeOps) {
testFn = assertOps(testFn); testFn = assertOps(testFn);
} }
if (opts.sanitizeResources) { if (desc.sanitizeResources) {
testFn = assertResources(testFn); testFn = assertResources(testFn);
} }
if (opts.sanitizeExit) { if (desc.sanitizeExit) {
testFn = assertExit(testFn, true); testFn = assertExit(testFn, true);
} }
return testFn; if (!("parent" in desc) && desc.permissions) {
testFn = withPermissions(testFn, desc.permissions);
}
return wrapOuter(testFn, desc);
} }
internals.testing = {
runTests,
runBenchmarks,
enableTest,
enableBench,
};
import { denoNs } from "ext:runtime/90_deno_ns.js"; import { denoNs } from "ext:runtime/90_deno_ns.js";
denoNs.bench = bench; denoNs.bench = bench;
denoNs.test = test; denoNs.test = test;

View file

@ -10,13 +10,11 @@ use crate::lsp::client::Client;
use crate::lsp::client::TestingNotification; use crate::lsp::client::TestingNotification;
use crate::lsp::config; use crate::lsp::config;
use crate::lsp::logging::lsp_log; use crate::lsp::logging::lsp_log;
use crate::ops;
use crate::proc_state; use crate::proc_state;
use crate::tools::test; use crate::tools::test;
use crate::tools::test::FailFastTracker; use crate::tools::test::FailFastTracker;
use crate::tools::test::TestEventSender; use crate::tools::test::TestEventSender;
use crate::util::checksum; use crate::util::checksum;
use crate::worker::create_main_worker_for_test_or_bench;
use deno_core::anyhow::anyhow; use deno_core::anyhow::anyhow;
use deno_core::error::AnyError; use deno_core::error::AnyError;
@ -27,10 +25,7 @@ use deno_core::futures::StreamExt;
use deno_core::parking_lot::Mutex; use deno_core::parking_lot::Mutex;
use deno_core::parking_lot::RwLock; use deno_core::parking_lot::RwLock;
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
use deno_runtime::deno_io::Stdio;
use deno_runtime::deno_io::StdioPipe;
use deno_runtime::permissions::Permissions; use deno_runtime::permissions::Permissions;
use deno_runtime::permissions::PermissionsContainer;
use deno_runtime::tokio_util::run_local; use deno_runtime::tokio_util::run_local;
use indexmap::IndexMap; use indexmap::IndexMap;
use std::collections::HashMap; use std::collections::HashMap;
@ -147,42 +142,6 @@ impl LspTestFilter {
} }
} }
#[allow(clippy::too_many_arguments)]
async fn test_specifier(
ps: proc_state::ProcState,
permissions: Permissions,
specifier: ModuleSpecifier,
mode: test::TestMode,
sender: TestEventSender,
fail_fast_tracker: FailFastTracker,
token: CancellationToken,
filter: test::TestFilter,
) -> Result<(), AnyError> {
if !token.is_cancelled() {
let stdout = StdioPipe::File(sender.stdout());
let stderr = StdioPipe::File(sender.stderr());
let mut worker = create_main_worker_for_test_or_bench(
&ps,
specifier.clone(),
PermissionsContainer::new(permissions),
vec![ops::testing::deno_test::init_ops(
sender,
fail_fast_tracker,
filter,
)],
Stdio {
stdin: StdioPipe::Inherit,
stdout,
stderr,
},
)
.await?;
worker.run_lsp_test_specifier(mode).await?;
}
Ok(())
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TestRun { pub struct TestRun {
id: u32, id: u32,
@ -300,7 +259,6 @@ impl TestRun {
Arc::new(RwLock::new(IndexMap::new())); Arc::new(RwLock::new(IndexMap::new()));
let mut test_steps = IndexMap::new(); let mut test_steps = IndexMap::new();
let tests_ = tests.clone();
let join_handles = queue.into_iter().map(move |specifier| { let join_handles = queue.into_iter().map(move |specifier| {
let specifier = specifier.clone(); let specifier = specifier.clone();
let ps = ps.clone(); let ps = ps.clone();
@ -321,38 +279,30 @@ impl TestRun {
.unwrap_or_default(), .unwrap_or_default(),
}; };
let token = self.token.clone(); let token = self.token.clone();
let tests = tests_.clone();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
if fail_fast_tracker.should_stop() { if fail_fast_tracker.should_stop() {
return Ok(()); return Ok(());
} }
let origin = specifier.to_string(); let origin = specifier.to_string();
let file_result = run_local(test_specifier( let file_result = if token.is_cancelled() {
ps, Ok(())
permissions, } else {
specifier, run_local(test::test_specifier(
test::TestMode::Executable, &ps,
sender.clone(), permissions,
fail_fast_tracker, specifier,
token, sender.clone(),
filter, fail_fast_tracker,
)); filter,
))
};
if let Err(error) = file_result { if let Err(error) = file_result {
if error.is::<JsError>() { if error.is::<JsError>() {
sender.send(test::TestEvent::UncaughtError( sender.send(test::TestEvent::UncaughtError(
origin.clone(), origin,
Box::new(error.downcast::<JsError>().unwrap()), Box::new(error.downcast::<JsError>().unwrap()),
))?; ))?;
for desc in tests.read().values() {
if desc.origin == origin {
sender.send(test::TestEvent::Result(
desc.id,
test::TestResult::Cancelled,
0,
))?
}
}
} else { } else {
return Err(error); return Err(error);
} }
@ -489,6 +439,7 @@ impl TestRun {
.iter() .iter()
.map(|s| s.as_str()), .map(|s| s.as_str()),
); );
args.push("--trace-ops");
if self.workspace_settings.unstable && !args.contains(&"--unstable") { if self.workspace_settings.unstable && !args.contains(&"--unstable") {
args.push("--unstable"); args.push("--unstable");
} }

View file

@ -7,6 +7,8 @@ use std::time;
use deno_core::error::generic_error; use deno_core::error::generic_error;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::op; use deno_core::op;
use deno_core::serde_v8;
use deno_core::v8;
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
use deno_core::OpState; use deno_core::OpState;
use deno_runtime::permissions::create_child_permissions; use deno_runtime::permissions::create_child_permissions;
@ -19,24 +21,26 @@ use uuid::Uuid;
use crate::tools::bench::BenchDescription; use crate::tools::bench::BenchDescription;
use crate::tools::bench::BenchEvent; use crate::tools::bench::BenchEvent;
use crate::tools::test::TestFilter;
#[derive(Default)]
pub(crate) struct BenchContainer(
pub Vec<(BenchDescription, v8::Global<v8::Function>)>,
);
deno_core::extension!(deno_bench, deno_core::extension!(deno_bench,
ops = [ ops = [
op_pledge_test_permissions, op_pledge_test_permissions,
op_restore_test_permissions, op_restore_test_permissions,
op_get_bench_origin,
op_register_bench, op_register_bench,
op_dispatch_bench_event, op_dispatch_bench_event,
op_bench_now, op_bench_now,
], ],
options = { options = {
sender: UnboundedSender<BenchEvent>, sender: UnboundedSender<BenchEvent>,
filter: TestFilter,
}, },
state = |state, options| { state = |state, options| {
state.put(options.sender); state.put(options.sender);
state.put(options.filter); state.put(BenchContainer::default());
}, },
customizer = |ext: &mut deno_core::ExtensionBuilder| { customizer = |ext: &mut deno_core::ExtensionBuilder| {
ext.force_op_registration(); ext.force_op_registration();
@ -90,51 +94,61 @@ pub fn op_restore_test_permissions(
} }
} }
#[op] #[derive(Deserialize)]
fn op_get_bench_origin(state: &mut OpState) -> String {
state.borrow::<ModuleSpecifier>().to_string()
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct BenchInfo { struct BenchInfo<'s> {
#[serde(rename = "fn")]
function: serde_v8::Value<'s>,
name: String, name: String,
origin: String,
baseline: bool, baseline: bool,
group: Option<String>, group: Option<String>,
ignore: bool,
only: bool,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct BenchRegisterResult { struct BenchRegisterResult {
id: usize, id: usize,
filtered_out: bool, origin: String,
} }
static NEXT_ID: AtomicUsize = AtomicUsize::new(0); static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
#[op] #[op(v8)]
fn op_register_bench( fn op_register_bench<'a>(
scope: &mut v8::HandleScope<'a>,
state: &mut OpState, state: &mut OpState,
info: BenchInfo, info: BenchInfo<'a>,
) -> Result<BenchRegisterResult, AnyError> { ) -> Result<BenchRegisterResult, AnyError> {
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
let filter = state.borrow::<TestFilter>().clone(); let origin = state.borrow::<ModuleSpecifier>().to_string();
let filtered_out = !filter.includes(&info.name);
let description = BenchDescription { let description = BenchDescription {
id, id,
name: info.name, name: info.name,
origin: info.origin, origin: origin.clone(),
baseline: info.baseline, baseline: info.baseline,
group: info.group, group: info.group,
ignore: info.ignore,
only: info.only,
}; };
let function: v8::Local<v8::Function> = info.function.v8_value.try_into()?;
let function = v8::Global::new(scope, function);
state
.borrow_mut::<BenchContainer>()
.0
.push((description.clone(), function));
let sender = state.borrow::<UnboundedSender<BenchEvent>>().clone(); let sender = state.borrow::<UnboundedSender<BenchEvent>>().clone();
sender.send(BenchEvent::Register(description)).ok(); sender.send(BenchEvent::Register(description)).ok();
Ok(BenchRegisterResult { id, filtered_out }) Ok(BenchRegisterResult { id, origin })
} }
#[op] #[op]
fn op_dispatch_bench_event(state: &mut OpState, event: BenchEvent) { fn op_dispatch_bench_event(state: &mut OpState, event: BenchEvent) {
assert!(
matches!(event, BenchEvent::Output(_)),
"Only output events are expected from JS."
);
let sender = state.borrow::<UnboundedSender<BenchEvent>>().clone(); let sender = state.borrow::<UnboundedSender<BenchEvent>>().clone();
sender.send(event).ok(); sender.send(event).ok();
} }

View file

@ -1,17 +1,16 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
use crate::tools::test::FailFastTracker;
use crate::tools::test::TestDescription; use crate::tools::test::TestDescription;
use crate::tools::test::TestEvent; use crate::tools::test::TestEvent;
use crate::tools::test::TestEventSender; use crate::tools::test::TestEventSender;
use crate::tools::test::TestFilter;
use crate::tools::test::TestLocation; use crate::tools::test::TestLocation;
use crate::tools::test::TestResult;
use crate::tools::test::TestStepDescription; use crate::tools::test::TestStepDescription;
use deno_core::error::generic_error; use deno_core::error::generic_error;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::op; use deno_core::op;
use deno_core::serde_v8;
use deno_core::v8;
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
use deno_core::OpState; use deno_core::OpState;
use deno_runtime::permissions::create_child_permissions; use deno_runtime::permissions::create_child_permissions;
@ -24,25 +23,25 @@ use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use uuid::Uuid; use uuid::Uuid;
#[derive(Default)]
pub(crate) struct TestContainer(
pub Vec<(TestDescription, v8::Global<v8::Function>)>,
);
deno_core::extension!(deno_test, deno_core::extension!(deno_test,
ops = [ ops = [
op_pledge_test_permissions, op_pledge_test_permissions,
op_restore_test_permissions, op_restore_test_permissions,
op_get_test_origin,
op_register_test, op_register_test,
op_register_test_step, op_register_test_step,
op_dispatch_test_event, op_dispatch_test_event,
op_tests_should_stop,
], ],
options = { options = {
sender: TestEventSender, sender: TestEventSender,
fail_fast_tracker: FailFastTracker,
filter: TestFilter,
}, },
state = |state, options| { state = |state, options| {
state.put(options.sender); state.put(options.sender);
state.put(options.fail_fast_tracker); state.put(TestContainer::default());
state.put(options.filter);
}, },
customizer = |ext: &mut deno_core::ExtensionBuilder| { customizer = |ext: &mut deno_core::ExtensionBuilder| {
ext.force_op_registration(); ext.force_op_registration();
@ -95,16 +94,14 @@ pub fn op_restore_test_permissions(
} }
} }
#[op] #[derive(Deserialize)]
fn op_get_test_origin(state: &mut OpState) -> Result<String, AnyError> {
Ok(state.borrow::<ModuleSpecifier>().to_string())
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct TestInfo { struct TestInfo<'s> {
#[serde(rename = "fn")]
function: serde_v8::Value<'s>,
name: String, name: String,
origin: String, ignore: bool,
only: bool,
location: TestLocation, location: TestLocation,
} }
@ -112,28 +109,36 @@ struct TestInfo {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct TestRegisterResult { struct TestRegisterResult {
id: usize, id: usize,
filtered_out: bool, origin: String,
} }
static NEXT_ID: AtomicUsize = AtomicUsize::new(0); static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
#[op] #[op(v8)]
fn op_register_test( fn op_register_test<'a>(
scope: &mut v8::HandleScope<'a>,
state: &mut OpState, state: &mut OpState,
info: TestInfo, info: TestInfo<'a>,
) -> Result<TestRegisterResult, AnyError> { ) -> Result<TestRegisterResult, AnyError> {
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
let filter = state.borrow::<TestFilter>().clone(); let origin = state.borrow::<ModuleSpecifier>().to_string();
let filtered_out = !filter.includes(&info.name);
let description = TestDescription { let description = TestDescription {
id, id,
name: info.name, name: info.name,
origin: info.origin, ignore: info.ignore,
only: info.only,
origin: origin.clone(),
location: info.location, location: info.location,
}; };
let function: v8::Local<v8::Function> = info.function.v8_value.try_into()?;
let function = v8::Global::new(scope, function);
state
.borrow_mut::<TestContainer>()
.0
.push((description.clone(), function));
let mut sender = state.borrow::<TestEventSender>().clone(); let mut sender = state.borrow::<TestEventSender>().clone();
sender.send(TestEvent::Register(description)).ok(); sender.send(TestEvent::Register(description)).ok();
Ok(TestRegisterResult { id, filtered_out }) Ok(TestRegisterResult { id, origin })
} }
fn deserialize_parent<'de, D>(deserializer: D) -> Result<usize, D::Error> fn deserialize_parent<'de, D>(deserializer: D) -> Result<usize, D::Error>
@ -151,7 +156,6 @@ where
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct TestStepInfo { struct TestStepInfo {
name: String, name: String,
origin: String,
location: TestLocation, location: TestLocation,
level: usize, level: usize,
#[serde(rename = "parent")] #[serde(rename = "parent")]
@ -167,10 +171,11 @@ fn op_register_test_step(
info: TestStepInfo, info: TestStepInfo,
) -> Result<TestRegisterResult, AnyError> { ) -> Result<TestRegisterResult, AnyError> {
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
let origin = state.borrow::<ModuleSpecifier>().to_string();
let description = TestStepDescription { let description = TestStepDescription {
id, id,
name: info.name, name: info.name,
origin: info.origin, origin: origin.clone(),
location: info.location, location: info.location,
level: info.level, level: info.level,
parent_id: info.parent_id, parent_id: info.parent_id,
@ -179,10 +184,7 @@ fn op_register_test_step(
}; };
let mut sender = state.borrow::<TestEventSender>().clone(); let mut sender = state.borrow::<TestEventSender>().clone();
sender.send(TestEvent::StepRegister(description)).ok(); sender.send(TestEvent::StepRegister(description)).ok();
Ok(TestRegisterResult { Ok(TestRegisterResult { id, origin })
id,
filtered_out: false,
})
} }
#[op] #[op]
@ -190,18 +192,11 @@ fn op_dispatch_test_event(
state: &mut OpState, state: &mut OpState,
event: TestEvent, event: TestEvent,
) -> Result<(), AnyError> { ) -> Result<(), AnyError> {
if matches!( assert!(
event, matches!(event, TestEvent::StepWait(_) | TestEvent::StepResult(..)),
TestEvent::Result(_, TestResult::Cancelled | TestResult::Failed(_), _) "Only step wait/result events are expected from JS."
) { );
state.borrow::<FailFastTracker>().add_failure();
}
let mut sender = state.borrow::<TestEventSender>().clone(); let mut sender = state.borrow::<TestEventSender>().clone();
sender.send(event).ok(); sender.send(event).ok();
Ok(()) Ok(())
} }
#[op]
fn op_tests_should_stop(state: &mut OpState) -> bool {
state.borrow::<FailFastTracker>().should_stop()
}

View file

@ -1,5 +1,4 @@
Check [WILDCARD]/test/doc_only/mod.ts$2-5.ts Check [WILDCARD]/test/doc_only/mod.ts$2-5.ts
running 0 tests from ./test/doc_only/mod.ts
ok | 0 passed | 0 failed ([WILDCARD]) ok | 0 passed | 0 failed ([WILDCARD])

View file

@ -2,38 +2,38 @@ Check [WILDCARD]/test/shuffle/bar_test.ts
Check [WILDCARD]/test/shuffle/baz_test.ts Check [WILDCARD]/test/shuffle/baz_test.ts
Check [WILDCARD]/test/shuffle/foo_test.ts Check [WILDCARD]/test/shuffle/foo_test.ts
running 10 tests from ./test/shuffle/foo_test.ts running 10 tests from ./test/shuffle/foo_test.ts
test 2 ... ok ([WILDCARD])
test 3 ... ok ([WILDCARD]) test 3 ... ok ([WILDCARD])
test 6 ... ok ([WILDCARD]) test 2 ... ok ([WILDCARD])
test 9 ... ok ([WILDCARD])
test 8 ... ok ([WILDCARD])
test 7 ... ok ([WILDCARD]) test 7 ... ok ([WILDCARD])
test 5 ... ok ([WILDCARD]) test 5 ... ok ([WILDCARD])
test 4 ... ok ([WILDCARD]) test 8 ... ok ([WILDCARD])
test 1 ... ok ([WILDCARD])
test 0 ... ok ([WILDCARD]) test 0 ... ok ([WILDCARD])
test 9 ... ok ([WILDCARD])
test 4 ... ok ([WILDCARD])
test 6 ... ok ([WILDCARD])
test 1 ... ok ([WILDCARD])
running 10 tests from ./test/shuffle/baz_test.ts running 10 tests from ./test/shuffle/baz_test.ts
test 2 ... ok ([WILDCARD])
test 3 ... ok ([WILDCARD]) test 3 ... ok ([WILDCARD])
test 6 ... ok ([WILDCARD]) test 2 ... ok ([WILDCARD])
test 9 ... ok ([WILDCARD])
test 8 ... ok ([WILDCARD])
test 7 ... ok ([WILDCARD]) test 7 ... ok ([WILDCARD])
test 5 ... ok ([WILDCARD]) test 5 ... ok ([WILDCARD])
test 4 ... ok ([WILDCARD]) test 8 ... ok ([WILDCARD])
test 1 ... ok ([WILDCARD])
test 0 ... ok ([WILDCARD]) test 0 ... ok ([WILDCARD])
test 9 ... ok ([WILDCARD])
test 4 ... ok ([WILDCARD])
test 6 ... ok ([WILDCARD])
test 1 ... ok ([WILDCARD])
running 10 tests from ./test/shuffle/bar_test.ts running 10 tests from ./test/shuffle/bar_test.ts
test 2 ... ok ([WILDCARD])
test 3 ... ok ([WILDCARD]) test 3 ... ok ([WILDCARD])
test 6 ... ok ([WILDCARD]) test 2 ... ok ([WILDCARD])
test 9 ... ok ([WILDCARD])
test 8 ... ok ([WILDCARD])
test 7 ... ok ([WILDCARD]) test 7 ... ok ([WILDCARD])
test 5 ... ok ([WILDCARD]) test 5 ... ok ([WILDCARD])
test 4 ... ok ([WILDCARD]) test 8 ... ok ([WILDCARD])
test 1 ... ok ([WILDCARD])
test 0 ... ok ([WILDCARD]) test 0 ... ok ([WILDCARD])
test 9 ... ok ([WILDCARD])
test 4 ... ok ([WILDCARD])
test 6 ... ok ([WILDCARD])
test 1 ... ok ([WILDCARD])
ok | 30 passed | 0 failed ([WILDCARD]) ok | 30 passed | 0 failed ([WILDCARD])

View file

@ -1,4 +1,3 @@
running 0 tests from ./test/text.md
ok | 0 passed | 0 failed ([WILDCARD]) ok | 0 passed | 0 failed ([WILDCARD])

View file

@ -15,7 +15,7 @@ use crate::util::file_watcher::ResolutionResult;
use crate::util::fs::collect_specifiers; use crate::util::fs::collect_specifiers;
use crate::util::path::is_supported_ext; use crate::util::path::is_supported_ext;
use crate::version::get_user_agent; use crate::version::get_user_agent;
use crate::worker::create_main_worker_for_test_or_bench; use crate::worker::create_custom_worker;
use deno_core::error::generic_error; use deno_core::error::generic_error;
use deno_core::error::AnyError; use deno_core::error::AnyError;
@ -24,11 +24,15 @@ use deno_core::futures::future;
use deno_core::futures::stream; use deno_core::futures::stream;
use deno_core::futures::FutureExt; use deno_core::futures::FutureExt;
use deno_core::futures::StreamExt; use deno_core::futures::StreamExt;
use deno_core::located_script_name;
use deno_core::serde_v8;
use deno_core::v8;
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
use deno_runtime::permissions::Permissions; use deno_runtime::permissions::Permissions;
use deno_runtime::permissions::PermissionsContainer; use deno_runtime::permissions::PermissionsContainer;
use deno_runtime::tokio_util::run_local; use deno_runtime::tokio_util::run_local;
use indexmap::IndexMap; use indexmap::IndexMap;
use indexmap::IndexSet;
use log::Level; use log::Level;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
@ -87,6 +91,8 @@ pub struct BenchDescription {
pub origin: String, pub origin: String,
pub baseline: bool, pub baseline: bool,
pub group: Option<String>, pub group: Option<String>,
pub ignore: bool,
pub only: bool,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -433,20 +439,80 @@ async fn bench_specifier(
ps: ProcState, ps: ProcState,
permissions: Permissions, permissions: Permissions,
specifier: ModuleSpecifier, specifier: ModuleSpecifier,
channel: UnboundedSender<BenchEvent>, sender: UnboundedSender<BenchEvent>,
options: BenchSpecifierOptions, filter: TestFilter,
) -> Result<(), AnyError> { ) -> Result<(), AnyError> {
let filter = options.filter; let mut worker = create_custom_worker(
let mut worker = create_main_worker_for_test_or_bench(
&ps, &ps,
specifier, specifier.clone(),
PermissionsContainer::new(permissions), PermissionsContainer::new(permissions),
vec![ops::bench::deno_bench::init_ops(channel, filter)], vec![ops::bench::deno_bench::init_ops(sender.clone())],
Default::default(), Default::default(),
) )
.await?; .await?;
worker.run_bench_specifier().await // We execute the main module as a side module so that import.meta.main is not set.
worker.execute_side_module_possibly_with_npm().await?;
let mut worker = worker.into_main_worker();
worker.dispatch_load_event(located_script_name!())?;
let benchmarks = {
let state_rc = worker.js_runtime.op_state();
let mut state = state_rc.borrow_mut();
std::mem::take(&mut state.borrow_mut::<ops::bench::BenchContainer>().0)
};
let (only, no_only): (Vec<_>, Vec<_>) =
benchmarks.into_iter().partition(|(d, _)| d.only);
let used_only = !only.is_empty();
let benchmarks = if used_only { only } else { no_only };
let mut benchmarks = benchmarks
.into_iter()
.filter(|(d, _)| filter.includes(&d.name) && !d.ignore)
.collect::<Vec<_>>();
let mut groups = IndexSet::<Option<String>>::new();
// make sure ungrouped benchmarks are placed above grouped
groups.insert(None);
for (desc, _) in &benchmarks {
groups.insert(desc.group.clone());
}
benchmarks.sort_by(|(d1, _), (d2, _)| {
groups
.get_index_of(&d1.group)
.unwrap()
.partial_cmp(&groups.get_index_of(&d2.group).unwrap())
.unwrap()
});
sender.send(BenchEvent::Plan(BenchPlan {
origin: specifier.to_string(),
total: benchmarks.len(),
used_only,
names: benchmarks.iter().map(|(d, _)| d.name.clone()).collect(),
}))?;
for (desc, function) in benchmarks {
sender.send(BenchEvent::Wait(desc.id))?;
let promise = {
let scope = &mut worker.js_runtime.handle_scope();
let cb = function.open(scope);
let this = v8::undefined(scope).into();
let promise = cb.call(scope, this, &[]).unwrap();
v8::Global::new(scope, promise)
};
let result = worker.js_runtime.resolve_value(promise).await?;
let scope = &mut worker.js_runtime.handle_scope();
let result = v8::Local::new(scope, result);
let result = serde_v8::from_v8::<BenchResult>(scope, result)?;
sender.send(BenchEvent::Result(desc.id, result))?;
}
loop {
if !worker.dispatch_beforeunload_event(located_script_name!())? {
break;
}
worker.run_event_loop(false).await?;
}
worker.dispatch_unload_event(located_script_name!())?;
Ok(())
} }
/// Test a collection of specifiers with test modes concurrently. /// Test a collection of specifiers with test modes concurrently.
@ -468,10 +534,9 @@ async fn bench_specifiers(
let specifier = specifier; let specifier = specifier;
let sender = sender.clone(); let sender = sender.clone();
let options = option_for_handles.clone(); let options = option_for_handles.clone();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let future = bench_specifier(ps, permissions, specifier, sender, options); let future =
bench_specifier(ps, permissions, specifier, sender, options.filter);
run_local(future) run_local(future)
}) })
}); });

View file

@ -17,7 +17,7 @@ use crate::util::fs::collect_specifiers;
use crate::util::path::get_extension; use crate::util::path::get_extension;
use crate::util::path::is_supported_ext; use crate::util::path::is_supported_ext;
use crate::util::path::mapped_specifier_for_tsc; use crate::util::path::mapped_specifier_for_tsc;
use crate::worker::create_main_worker_for_test_or_bench; use crate::worker::create_custom_worker;
use deno_ast::swc::common::comments::CommentKind; use deno_ast::swc::common::comments::CommentKind;
use deno_ast::MediaType; use deno_ast::MediaType;
@ -29,8 +29,11 @@ use deno_core::futures::future;
use deno_core::futures::stream; use deno_core::futures::stream;
use deno_core::futures::FutureExt; use deno_core::futures::FutureExt;
use deno_core::futures::StreamExt; use deno_core::futures::StreamExt;
use deno_core::located_script_name;
use deno_core::parking_lot::Mutex; use deno_core::parking_lot::Mutex;
use deno_core::serde_v8;
use deno_core::url::Url; use deno_core::url::Url;
use deno_core::v8;
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
use deno_runtime::deno_io::Stdio; use deno_runtime::deno_io::Stdio;
use deno_runtime::deno_io::StdioPipe; use deno_runtime::deno_io::StdioPipe;
@ -63,6 +66,7 @@ use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use std::time::Instant; use std::time::Instant;
use std::time::SystemTime;
use tokio::signal; use tokio::signal;
use tokio::sync::mpsc::unbounded_channel; use tokio::sync::mpsc::unbounded_channel;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
@ -144,6 +148,8 @@ pub struct TestLocation {
pub struct TestDescription { pub struct TestDescription {
pub id: usize, pub id: usize,
pub name: String, pub name: String,
pub ignore: bool,
pub only: bool,
pub origin: String, pub origin: String,
pub location: TestLocation, pub location: TestLocation,
} }
@ -900,26 +906,24 @@ pub fn format_test_error(js_error: &JsError) -> String {
/// Test a single specifier as documentation containing test programs, an executable test module or /// Test a single specifier as documentation containing test programs, an executable test module or
/// both. /// both.
async fn test_specifier( pub async fn test_specifier(
ps: &ProcState, ps: &ProcState,
permissions: Permissions, permissions: Permissions,
specifier: ModuleSpecifier, specifier: ModuleSpecifier,
mode: TestMode, mut sender: TestEventSender,
sender: TestEventSender,
fail_fast_tracker: FailFastTracker, fail_fast_tracker: FailFastTracker,
options: TestSpecifierOptions, filter: TestFilter,
) -> Result<(), AnyError> { ) -> Result<(), AnyError> {
if fail_fast_tracker.should_stop() {
return Ok(());
}
let stdout = StdioPipe::File(sender.stdout()); let stdout = StdioPipe::File(sender.stdout());
let stderr = StdioPipe::File(sender.stderr()); let stderr = StdioPipe::File(sender.stderr());
let mut worker = create_main_worker_for_test_or_bench( let mut worker = create_custom_worker(
ps, ps,
specifier, specifier.clone(),
PermissionsContainer::new(permissions), PermissionsContainer::new(permissions),
vec![ops::testing::deno_test::init_ops( vec![ops::testing::deno_test::init_ops(sender.clone())],
sender,
fail_fast_tracker,
options.filter,
)],
Stdio { Stdio {
stdin: StdioPipe::Inherit, stdin: StdioPipe::Inherit,
stdout, stdout,
@ -928,7 +932,119 @@ async fn test_specifier(
) )
.await?; .await?;
worker.run_test_specifier(mode).await let mut coverage_collector = worker.maybe_setup_coverage_collector().await?;
// We execute the main module as a side module so that import.meta.main is not set.
match worker.execute_side_module_possibly_with_npm().await {
Ok(()) => {}
Err(error) => {
if error.is::<JsError>() {
sender.send(TestEvent::UncaughtError(
specifier.to_string(),
Box::new(error.downcast::<JsError>().unwrap()),
))?;
return Ok(());
} else {
return Err(error);
}
}
}
let mut worker = worker.into_main_worker();
if ps.options.trace_ops() {
worker.js_runtime.execute_script_static(
located_script_name!(),
"Deno[Deno.internal].core.enableOpCallTracing();",
)?;
}
worker.dispatch_load_event(located_script_name!())?;
let tests = {
let state_rc = worker.js_runtime.op_state();
let mut state = state_rc.borrow_mut();
std::mem::take(&mut state.borrow_mut::<ops::testing::TestContainer>().0)
};
let unfiltered = tests.len();
let (only, no_only): (Vec<_>, Vec<_>) =
tests.into_iter().partition(|(d, _)| d.only);
let used_only = !only.is_empty();
let tests = if used_only { only } else { no_only };
let mut tests = tests
.into_iter()
.filter(|(d, _)| filter.includes(&d.name))
.collect::<Vec<_>>();
if let Some(seed) = ps.options.shuffle_tests() {
tests.shuffle(&mut SmallRng::seed_from_u64(seed));
}
sender.send(TestEvent::Plan(TestPlan {
origin: specifier.to_string(),
total: tests.len(),
filtered_out: unfiltered - tests.len(),
used_only,
}))?;
let mut had_uncaught_error = false;
for (desc, function) in tests {
if fail_fast_tracker.should_stop() {
break;
}
if desc.ignore {
sender.send(TestEvent::Result(desc.id, TestResult::Ignored, 0))?;
continue;
}
if had_uncaught_error {
sender.send(TestEvent::Result(desc.id, TestResult::Cancelled, 0))?;
continue;
}
sender.send(TestEvent::Wait(desc.id))?;
let earlier = SystemTime::now();
let promise = {
let scope = &mut worker.js_runtime.handle_scope();
let cb = function.open(scope);
let this = v8::undefined(scope).into();
let promise = cb.call(scope, this, &[]).unwrap();
v8::Global::new(scope, promise)
};
let result = match worker.js_runtime.resolve_value(promise).await {
Ok(r) => r,
Err(error) => {
if error.is::<JsError>() {
sender.send(TestEvent::UncaughtError(
specifier.to_string(),
Box::new(error.downcast::<JsError>().unwrap()),
))?;
fail_fast_tracker.add_failure();
sender.send(TestEvent::Result(desc.id, TestResult::Cancelled, 0))?;
had_uncaught_error = true;
continue;
} else {
return Err(error);
}
}
};
let scope = &mut worker.js_runtime.handle_scope();
let result = v8::Local::new(scope, result);
let result = serde_v8::from_v8::<TestResult>(scope, result)?;
if matches!(result, TestResult::Failed(_)) {
fail_fast_tracker.add_failure();
}
let elapsed = SystemTime::now().duration_since(earlier)?.as_millis();
sender.send(TestEvent::Result(desc.id, result, elapsed as u64))?;
}
loop {
if !worker.dispatch_beforeunload_event(located_script_name!())? {
break;
}
worker.run_event_loop(false).await?;
}
worker.dispatch_unload_event(located_script_name!())?;
if let Some(coverage_collector) = coverage_collector.as_mut() {
worker
.with_event_loop(coverage_collector.stop_collecting().boxed_local())
.await?;
}
Ok(())
} }
fn extract_files_from_regex_blocks( fn extract_files_from_regex_blocks(
@ -1182,18 +1298,18 @@ static HAS_TEST_RUN_SIGINT_HANDLER: AtomicBool = AtomicBool::new(false);
async fn test_specifiers( async fn test_specifiers(
ps: &ProcState, ps: &ProcState,
permissions: &Permissions, permissions: &Permissions,
specifiers_with_mode: Vec<(ModuleSpecifier, TestMode)>, specifiers: Vec<ModuleSpecifier>,
options: TestSpecifierOptions, options: TestSpecifierOptions,
) -> Result<(), AnyError> { ) -> Result<(), AnyError> {
let log_level = ps.options.log_level(); let log_level = ps.options.log_level();
let specifiers_with_mode = if let Some(seed) = ps.options.shuffle_tests() { let specifiers = if let Some(seed) = ps.options.shuffle_tests() {
let mut rng = SmallRng::seed_from_u64(seed); let mut rng = SmallRng::seed_from_u64(seed);
let mut specifiers_with_mode = specifiers_with_mode; let mut specifiers = specifiers;
specifiers_with_mode.sort_by_key(|(specifier, _)| specifier.clone()); specifiers.sort();
specifiers_with_mode.shuffle(&mut rng); specifiers.shuffle(&mut rng);
specifiers_with_mode specifiers
} else { } else {
specifiers_with_mode specifiers
}; };
let (sender, mut receiver) = unbounded_channel::<TestEvent>(); let (sender, mut receiver) = unbounded_channel::<TestEvent>();
@ -1207,44 +1323,23 @@ async fn test_specifiers(
}); });
HAS_TEST_RUN_SIGINT_HANDLER.store(true, Ordering::Relaxed); HAS_TEST_RUN_SIGINT_HANDLER.store(true, Ordering::Relaxed);
let join_handles = let join_handles = specifiers.into_iter().map(move |specifier| {
specifiers_with_mode let ps = ps.clone();
.into_iter() let permissions = permissions.clone();
.map(move |(specifier, mode)| { let sender = sender.clone();
let ps = ps.clone(); let options = options.clone();
let permissions = permissions.clone(); let fail_fast_tracker = FailFastTracker::new(options.fail_fast);
let mut sender = sender.clone(); tokio::task::spawn_blocking(move || {
let options = options.clone(); run_local(test_specifier(
let fail_fast_tracker = FailFastTracker::new(options.fail_fast); &ps,
permissions,
tokio::task::spawn_blocking(move || { specifier,
if fail_fast_tracker.should_stop() { sender.clone(),
return Ok(()); fail_fast_tracker,
} options.filter,
))
let origin = specifier.to_string(); })
let file_result = run_local(test_specifier( });
&ps,
permissions,
specifier,
mode,
sender.clone(),
fail_fast_tracker,
options,
));
if let Err(error) = file_result {
if error.is::<JsError>() {
sender.send(TestEvent::UncaughtError(
origin,
Box::new(error.downcast::<JsError>().unwrap()),
))?;
} else {
return Err(error);
}
}
Ok(())
})
});
let join_stream = stream::iter(join_handles) let join_stream = stream::iter(join_handles)
.buffer_unordered(concurrent_jobs.get()) .buffer_unordered(concurrent_jobs.get())
@ -1310,7 +1405,7 @@ async fn test_specifiers(
.push((description.clone(), failure.clone())); .push((description.clone(), failure.clone()));
} }
TestResult::Cancelled => { TestResult::Cancelled => {
unreachable!("should be handled in TestEvent::UncaughtError"); summary.failed += 1;
} }
} }
reporter.report_result(description, &result, elapsed); reporter.report_result(description, &result, elapsed);
@ -1321,12 +1416,6 @@ async fn test_specifiers(
reporter.report_uncaught_error(&origin, &error); reporter.report_uncaught_error(&origin, &error);
summary.failed += 1; summary.failed += 1;
summary.uncaught_errors.push((origin.clone(), error)); summary.uncaught_errors.push((origin.clone(), error));
for desc in tests.values() {
if desc.origin == origin && tests_with_result.insert(desc.id) {
summary.failed += 1;
reporter.report_result(desc, &TestResult::Cancelled, 0);
}
}
} }
TestEvent::StepRegister(description) => { TestEvent::StepRegister(description) => {
@ -1360,6 +1449,8 @@ async fn test_specifiers(
&tests, &tests,
&test_steps, &test_steps,
), ),
ignore: false,
only: false,
origin: description.origin.clone(), origin: description.origin.clone(),
location: description.location.clone(), location: description.location.clone(),
}, },
@ -1565,7 +1656,13 @@ pub async fn run_tests(
test_specifiers( test_specifiers(
&ps, &ps,
&permissions, &permissions,
specifiers_with_mode, specifiers_with_mode
.into_iter()
.filter_map(|(s, m)| match m {
TestMode::Documentation => None,
_ => Some(s),
})
.collect(),
TestSpecifierOptions { TestSpecifierOptions {
concurrent_jobs: test_options.concurrent_jobs, concurrent_jobs: test_options.concurrent_jobs,
fail_fast: test_options.fail_fast, fail_fast: test_options.fail_fast,
@ -1729,7 +1826,13 @@ pub async fn run_tests_with_watch(
test_specifiers( test_specifiers(
&ps, &ps,
permissions, permissions,
specifiers_with_mode, specifiers_with_mode
.into_iter()
.filter_map(|(s, m)| match m {
TestMode::Documentation => None,
_ => Some(s),
})
.collect(),
TestSpecifierOptions { TestSpecifierOptions {
concurrent_jobs: test_options.concurrent_jobs, concurrent_jobs: test_options.concurrent_jobs,
fail_fast: test_options.fail_fast, fail_fast: test_options.fail_fast,

View file

@ -5,14 +5,10 @@ use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use deno_ast::ModuleSpecifier; use deno_ast::ModuleSpecifier;
use deno_core::ascii_str;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::futures::task::LocalFutureObj; use deno_core::futures::task::LocalFutureObj;
use deno_core::futures::FutureExt; use deno_core::futures::FutureExt;
use deno_core::located_script_name; use deno_core::located_script_name;
use deno_core::serde_json::json;
use deno_core::serde_v8;
use deno_core::v8;
use deno_core::Extension; use deno_core::Extension;
use deno_core::ModuleId; use deno_core::ModuleId;
use deno_runtime::colors; use deno_runtime::colors;
@ -36,7 +32,6 @@ use crate::ops;
use crate::proc_state::ProcState; use crate::proc_state::ProcState;
use crate::tools; use crate::tools;
use crate::tools::coverage::CoverageCollector; use crate::tools::coverage::CoverageCollector;
use crate::tools::test::TestMode;
use crate::util::checksum; use crate::util::checksum;
use crate::version; use crate::version;
@ -45,11 +40,6 @@ pub struct CliMainWorker {
is_main_cjs: bool, is_main_cjs: bool,
worker: MainWorker, worker: MainWorker,
ps: ProcState, ps: ProcState,
js_run_tests_callback: Option<v8::Global<v8::Function>>,
js_run_benchmarks_callback: Option<v8::Global<v8::Function>>,
js_enable_test_callback: Option<v8::Global<v8::Function>>,
js_enable_bench_callback: Option<v8::Global<v8::Function>>,
} }
impl CliMainWorker { impl CliMainWorker {
@ -176,114 +166,14 @@ impl CliMainWorker {
executor.execute().await executor.execute().await
} }
pub async fn run_test_specifier( pub async fn execute_main_module_possibly_with_npm(
&mut self,
mode: TestMode,
) -> Result<(), AnyError> {
self.enable_test();
// Enable op call tracing in core to enable better debugging of op sanitizer
// failures.
if self.ps.options.trace_ops() {
self.worker.js_runtime.execute_script_static(
located_script_name!(),
"Deno[Deno.internal].core.enableOpCallTracing();",
)?;
}
let mut maybe_coverage_collector =
self.maybe_setup_coverage_collector().await?;
// We only execute the specifier as a module if it is tagged with TestMode::Module or
// TestMode::Both.
if mode != TestMode::Documentation {
// We execute the module module as a side module so that import.meta.main is not set.
self.execute_side_module_possibly_with_npm().await?;
}
self.worker.dispatch_load_event(located_script_name!())?;
self.run_tests(&self.ps.options.shuffle_tests()).await?;
loop {
if !self
.worker
.dispatch_beforeunload_event(located_script_name!())?
{
break;
}
self.worker.run_event_loop(false).await?;
}
self.worker.dispatch_unload_event(located_script_name!())?;
if let Some(coverage_collector) = maybe_coverage_collector.as_mut() {
self
.worker
.with_event_loop(coverage_collector.stop_collecting().boxed_local())
.await?;
}
Ok(())
}
pub async fn run_lsp_test_specifier(
&mut self,
mode: TestMode,
) -> Result<(), AnyError> {
self.enable_test();
self.worker.execute_script(
located_script_name!(),
ascii_str!("Deno[Deno.internal].core.enableOpCallTracing();"),
)?;
if mode != TestMode::Documentation {
// We execute the module module as a side module so that import.meta.main is not set.
self.execute_side_module_possibly_with_npm().await?;
}
self.worker.dispatch_load_event(located_script_name!())?;
self.run_tests(&None).await?;
loop {
if !self
.worker
.dispatch_beforeunload_event(located_script_name!())?
{
break;
}
self.worker.run_event_loop(false).await?;
}
self.worker.dispatch_unload_event(located_script_name!())?;
Ok(())
}
pub async fn run_bench_specifier(&mut self) -> Result<(), AnyError> {
self.enable_bench();
// We execute the module module as a side module so that import.meta.main is not set.
self.execute_side_module_possibly_with_npm().await?;
self.worker.dispatch_load_event(located_script_name!())?;
self.run_benchmarks().await?;
loop {
if !self
.worker
.dispatch_beforeunload_event(located_script_name!())?
{
break;
}
self.worker.run_event_loop(false).await?;
}
self.worker.dispatch_unload_event(located_script_name!())?;
Ok(())
}
async fn execute_main_module_possibly_with_npm(
&mut self, &mut self,
) -> Result<(), AnyError> { ) -> Result<(), AnyError> {
let id = self.worker.preload_main_module(&self.main_module).await?; let id = self.worker.preload_main_module(&self.main_module).await?;
self.evaluate_module_possibly_with_npm(id).await self.evaluate_module_possibly_with_npm(id).await
} }
async fn execute_side_module_possibly_with_npm( pub async fn execute_side_module_possibly_with_npm(
&mut self, &mut self,
) -> Result<(), AnyError> { ) -> Result<(), AnyError> {
let id = self.worker.preload_side_module(&self.main_module).await?; let id = self.worker.preload_side_module(&self.main_module).await?;
@ -325,7 +215,7 @@ impl CliMainWorker {
Ok(()) Ok(())
} }
async fn maybe_setup_coverage_collector( pub async fn maybe_setup_coverage_collector(
&mut self, &mut self,
) -> Result<Option<CoverageCollector>, AnyError> { ) -> Result<Option<CoverageCollector>, AnyError> {
if let Some(ref coverage_dir) = self.ps.options.coverage_dir() { if let Some(ref coverage_dir) = self.ps.options.coverage_dir() {
@ -343,61 +233,6 @@ impl CliMainWorker {
Ok(None) Ok(None)
} }
} }
/// Run tests declared with `Deno.test()`. Test events will be dispatched
/// by calling ops which are currently only implemented in the CLI crate.
pub async fn run_tests(
&mut self,
shuffle: &Option<u64>,
) -> Result<(), AnyError> {
let promise = {
let scope = &mut self.worker.js_runtime.handle_scope();
let cb = self.js_run_tests_callback.as_ref().unwrap().open(scope);
let this = v8::undefined(scope).into();
let options =
serde_v8::to_v8(scope, json!({ "shuffle": shuffle })).unwrap();
let promise = cb.call(scope, this, &[options]).unwrap();
v8::Global::new(scope, promise)
};
self.worker.js_runtime.resolve_value(promise).await?;
Ok(())
}
/// Run benches declared with `Deno.bench()`. Bench events will be dispatched
/// by calling ops which are currently only implemented in the CLI crate.
pub async fn run_benchmarks(&mut self) -> Result<(), AnyError> {
let promise = {
let scope = &mut self.worker.js_runtime.handle_scope();
let cb = self
.js_run_benchmarks_callback
.as_ref()
.unwrap()
.open(scope);
let this = v8::undefined(scope).into();
let promise = cb.call(scope, this, &[]).unwrap();
v8::Global::new(scope, promise)
};
self.worker.js_runtime.resolve_value(promise).await?;
Ok(())
}
/// Enable `Deno.test()`. If this isn't called before executing user code,
/// `Deno.test()` calls will noop.
pub fn enable_test(&mut self) {
let scope = &mut self.worker.js_runtime.handle_scope();
let cb = self.js_enable_test_callback.as_ref().unwrap().open(scope);
let this = v8::undefined(scope).into();
cb.call(scope, this, &[]).unwrap();
}
/// Enable `Deno.bench()`. If this isn't called before executing user code,
/// `Deno.bench()` calls will noop.
pub fn enable_bench(&mut self) {
let scope = &mut self.worker.js_runtime.handle_scope();
let cb = self.js_enable_bench_callback.as_ref().unwrap().open(scope);
let this = v8::undefined(scope).into();
cb.call(scope, this, &[]).unwrap();
}
} }
pub async fn create_main_worker( pub async fn create_main_worker(
@ -405,42 +240,16 @@ pub async fn create_main_worker(
main_module: ModuleSpecifier, main_module: ModuleSpecifier,
permissions: PermissionsContainer, permissions: PermissionsContainer,
) -> Result<CliMainWorker, AnyError> { ) -> Result<CliMainWorker, AnyError> {
create_main_worker_internal( create_custom_worker(ps, main_module, permissions, vec![], Default::default())
ps, .await
main_module,
permissions,
vec![],
Default::default(),
false,
)
.await
} }
pub async fn create_main_worker_for_test_or_bench( pub async fn create_custom_worker(
ps: &ProcState,
main_module: ModuleSpecifier,
permissions: PermissionsContainer,
custom_extensions: Vec<Extension>,
stdio: deno_runtime::deno_io::Stdio,
) -> Result<CliMainWorker, AnyError> {
create_main_worker_internal(
ps,
main_module,
permissions,
custom_extensions,
stdio,
true,
)
.await
}
async fn create_main_worker_internal(
ps: &ProcState, ps: &ProcState,
main_module: ModuleSpecifier, main_module: ModuleSpecifier,
permissions: PermissionsContainer, permissions: PermissionsContainer,
mut custom_extensions: Vec<Extension>, mut custom_extensions: Vec<Extension>,
stdio: deno_runtime::deno_io::Stdio, stdio: deno_runtime::deno_io::Stdio,
bench_or_test: bool,
) -> Result<CliMainWorker, AnyError> { ) -> Result<CliMainWorker, AnyError> {
let (main_module, is_main_cjs) = if let Ok(package_ref) = let (main_module, is_main_cjs) = if let Ok(package_ref) =
NpmPackageReqReference::from_specifier(&main_module) NpmPackageReqReference::from_specifier(&main_module)
@ -552,59 +361,17 @@ async fn create_main_worker_internal(
stdio, stdio,
}; };
let mut worker = MainWorker::bootstrap_from_options( let worker = MainWorker::bootstrap_from_options(
main_module.clone(), main_module.clone(),
permissions, permissions,
options, options,
); );
let (
js_run_tests_callback,
js_run_benchmarks_callback,
js_enable_test_callback,
js_enable_bench_callback,
) = if bench_or_test {
let scope = &mut worker.js_runtime.handle_scope();
let js_run_tests_callback = deno_core::JsRuntime::eval::<v8::Function>(
scope,
"Deno[Deno.internal].testing.runTests",
)
.unwrap();
let js_run_benchmarks_callback =
deno_core::JsRuntime::eval::<v8::Function>(
scope,
"Deno[Deno.internal].testing.runBenchmarks",
)
.unwrap();
let js_enable_tests_callback = deno_core::JsRuntime::eval::<v8::Function>(
scope,
"Deno[Deno.internal].testing.enableTest",
)
.unwrap();
let js_enable_bench_callback = deno_core::JsRuntime::eval::<v8::Function>(
scope,
"Deno[Deno.internal].testing.enableBench",
)
.unwrap();
(
Some(v8::Global::new(scope, js_run_tests_callback)),
Some(v8::Global::new(scope, js_run_benchmarks_callback)),
Some(v8::Global::new(scope, js_enable_tests_callback)),
Some(v8::Global::new(scope, js_enable_bench_callback)),
)
} else {
(None, None, None, None)
};
Ok(CliMainWorker { Ok(CliMainWorker {
main_module, main_module,
is_main_cjs, is_main_cjs,
worker, worker,
ps: ps.clone(), ps: ps.clone(),
js_run_tests_callback,
js_run_benchmarks_callback,
js_enable_test_callback,
js_enable_bench_callback,
}) })
} }