diff --git a/cli/js/40_test.js b/cli/js/40_test.js index 2c5e0a2285..4e9ab61ab1 100644 --- a/cli/js/40_test.js +++ b/cli/js/40_test.js @@ -10,6 +10,7 @@ const { op_register_test_group, op_test_group_pop, op_register_test_group_lifecycle, + op_register_test_run_fn, op_test_event_step_result_failed, op_test_event_step_result_ignored, op_test_event_step_result_ok, @@ -501,12 +502,6 @@ function createTestContext(desc) { }; } -/** @type { only: boolean[], ignore: boolean[] } */ -const bddStack = { - only: [], - ignore: [], -}; - /** * Wrap a user test function in one which returns a structured result. * @template T {Function} @@ -527,6 +522,29 @@ function wrapTest(desc) { globalThis.Deno.test = test; +/** @typedef {{ name: string, fn: () => any, only: boolean, ignore: boolean }} BddTest */ + +/** @typedef {() => unknown | Promise} TestLifecycleFn */ + +/** @typedef {{ name: string, ignore: boolean, only: boolean, children: Array, beforeAll: TestLifecycleFn | null, afterAll: TestLifecycleFn | null, beforeEach: TestLifecycleFn | null, afterEach: TestLifecycleFn | null}} TestGroup */ + +const ROOT_TEST_GROUP = { + name: "____", + ignore: false, + only: false, + children: [], + beforeAll: null, + beforeEach: null, + afterAll: null, + afterEach: null, +}; +/** @type {{ hasOnly: boolean, stack: TestGroup[], total: number }} */ +const BDD_CONTEXT = { + hasOnly: false, + stack: [ROOT_TEST_GROUP], + total: 0, +}; + /** * @param {string} name * @param {fn: () => any} fn @@ -557,6 +575,16 @@ function itInner(name, fn, ignore, only) { } }; + /** @type {BddTest} */ + const testDef = { + name, + fn: testFn, + ignore, + only, + }; + BDD_CONTEXT.stack.at(-1).children.push(testDef); + BDD_CONTEXT.total++; + op_register_test( testFn, escapeName(name), @@ -583,6 +611,7 @@ function it(name, fn) { * @param {() => any} fn */ it.only = (name, fn) => { + BDD_CONTEXT.hasOnly = true; itInner(name, fn, false, true); }; /** @@ -606,11 +635,25 @@ function describeInner(name, fn, ignore, only) { return; } - op_register_test_group(name, ignore, only); + const parent = BDD_CONTEXT.stack.at(-1); + /** @type {TestGroup} */ + const group = { + name, + ignore, + only, + children: [], + beforeAll: null, + beforeEach: null, + afterAll: null, + afterEach: null, + }; + parent.children.push(group); + BDD_CONTEXT.stack.push(group); + try { fn(); } finally { - op_test_group_pop(); + BDD_CONTEXT.stack.pop(); } } @@ -626,6 +669,7 @@ function describe(name, fn) { * @param {() => void} fn */ describe.only = (name, fn) => { + BDD_CONTEXT.hasOnly = true; describeInner(name, fn, false, true); }; /** @@ -637,65 +681,31 @@ describe.ignore = (name, fn) => { }; describe.skip = describe.ignore; -// Keep in sync on the rust side -const BEFORE_ALL = 1; -const BEFORE_EACH = 2; -const AFTER_ALL = 3; -const AFTER_EACH = 4; - /** * @param {() => any} fn */ function beforeAll(fn) { - const location = core.currentUserCallSite(); - op_register_test_group_lifecycle( - BEFORE_ALL, - fn, - location.fileName, - location.lineNumber, - location.columnNumber, - ); + BDD_CONTEXT.stack.at(-1).beforeAll = fn; } /** * @param {() => any} fn */ function afterAll(fn) { - const location = core.currentUserCallSite(); - op_register_test_group_lifecycle( - AFTER_ALL, - fn, - location.fileName, - location.lineNumber, - location.columnNumber, - ); + BDD_CONTEXT.stack.at(-1).afterAll = fn; } /** * @param {() => any} fn */ function beforeEach(fn) { - const location = core.currentUserCallSite(); - op_register_test_group_lifecycle( - BEFORE_EACH, - fn, - location.fileName, - location.lineNumber, - location.columnNumber, - ); + BDD_CONTEXT.stack.at(-1).beforeEach = fn; } /** * @param {() => any} fn */ function afterEach(fn) { - const location = core.currentUserCallSite(); - op_register_test_group_lifecycle( - AFTER_EACH, - fn, - location.fileName, - location.lineNumber, - location.columnNumber, - ); + BDD_CONTEXT.stack.at(-1).afterEach = fn; } globalThis.before = beforeAll; @@ -706,3 +716,86 @@ globalThis.beforeEach = beforeEach; globalThis.afterEach = afterEach; globalThis.it = it; globalThis.describe = describe; + +/** + * This function is called from Rust. + * @param {bigint} seed + * @param {...any} rest + */ +async function runTests(seed, ...rest) { + console.log("RUN TESTS", seed, rest, ROOT_TEST_GROUP); + + // Filter tests + + await runGroup(seed, ROOT_TEST_GROUP); +} + +/** + * @param {bigint} seed + * @param {TestGroup} group + */ +async function runGroup(seed, group) { + // Bail out if group has no tests or sub groups + + /** @type {BddTest[]} */ + const tests = []; + /** @type {TestGroup[]} */ + const groups = []; + + for (let i = 0; i < group.children[i]; i++) { + const child = group.children[i]; + if ("beforeAll" in child) { + groups.push(child); + } else { + tests.push(child); + } + } + + if (seed > 0) { + shuffle(tests, seed); + shuffle(groups, seed); + } + + await group.beforeAll?.(); + + for (let i = 0; i < tests.length; i++) { + const test = tests[i]; + + await group.beforeEach?.(); + await test.fn(); + await group.afterEach?.(); + } + + for (let i = 0; i < groups.length; i++) { + const childGroup = groups[i]; + + await group.beforeEach?.(); + await runGroup(seed, childGroup); + await group.afterEach?.(); + } + + await group.afterAll?.(); +} + +/** + * @template T + * @param {T[]} arr + * @param {bigint} seed + */ +function shuffle(arr, seed) { + let m = arr.length; + let t; + let i; + + while (m) { + i = Math.floor(seed * m--); + t = arr[m]; + arr[m] = arr[i]; + arr[i] = t; + } +} + +// No-op if we're not running in `deno test` subcommand. +if (typeof op_register_test === "function") { + op_register_test_run_fn(runTests); +} diff --git a/cli/ops/testing.rs b/cli/ops/testing.rs index f12a0c0c3e..61b29584a7 100644 --- a/cli/ops/testing.rs +++ b/cli/ops/testing.rs @@ -33,6 +33,7 @@ deno_core::extension!(deno_test, op_register_test_group, op_test_group_pop, op_register_test_group_lifecycle, + op_register_test_run_fn, op_test_get_origin, op_test_event_step_wait, op_test_event_step_result_ok, @@ -171,6 +172,16 @@ fn op_test_group_pop(state: &mut OpState) -> Result<(), AnyError> { Ok(()) } +#[op2] +fn op_register_test_run_fn( + state: &mut OpState, + #[global] function: v8::Global, +) -> Result<(), AnyError> { + let container = state.borrow_mut::(); + container.run_fn = Some(function); + Ok(()) +} + #[allow(clippy::too_many_arguments)] #[op2] fn op_register_test_group_lifecycle( diff --git a/cli/tools/test/mod.rs b/cli/tools/test/mod.rs index f11d9e20f8..01d6a93888 100644 --- a/cli/tools/test/mod.rs +++ b/cli/tools/test/mod.rs @@ -224,6 +224,7 @@ pub struct TestLocation { #[derive(Default)] pub(crate) struct TestContainer { has_tests: bool, + pub run_fn: Option>, pub has_only: bool, pub groups: Vec, pub stack: Vec, @@ -249,6 +250,7 @@ impl TestContainer { Self { has_tests: false, + run_fn: None, groups: vec![root], has_only: false, stack, @@ -899,6 +901,25 @@ pub async fn run_tests_for_worker( eprintln!("{:#?}", tc.stack); let to_run: Vec = vec![]; + if let Some(function) = &tc.run_fn { + let seed = if let Some(seed) = options.shuffle { + seed + } else { + 0 + }; + + let args = { + let scope = &mut worker.js_runtime.handle_scope(); + let seed_value: v8::Local = + v8::BigInt::new_from_u64(scope, seed as u64).into(); + [v8::Global::new(scope, seed_value)] + }; + let call = worker.js_runtime.call_with_args(&function, &args); + let result = worker + .js_runtime + .with_event_loop_promise(call, PollEventLoopOptions::default()) + .await; + } if let Some(seed) = options.shuffle { // tests_to_run.shuffle(&mut SmallRng::seed_from_u64(seed)); diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 8ee4d49d91..a207b5d252 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -486,6 +486,7 @@ const NOT_IMPORTED_OPS = [ "op_register_test_group", "op_test_group_pop", "op_register_test_group_lifecycle", + "op_register_test_run_fn", "op_test_get_origin", "op_pledge_test_permissions",