1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-24 15:19:26 -05:00
denoland-deno/cli/js/40_test.js
Marvin Hagemeister fd35d4b688 WIP
2024-10-14 11:43:39 +02:00

1146 lines
28 KiB
JavaScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// @ts-check
import { core, primordials } from "ext:core/mod.js";
import { escapeName, withPermissions } from "ext:cli/40_test_common.js";
// TODO(mmastrac): We cannot import these from "ext:core/ops" yet
const {
op_register_test_step,
op_register_test,
op_test_group_register,
op_test_group_event_start,
op_test_group_event_end,
op_register_test_run_fn,
op_test_event_step_result_failed,
op_test_event_step_result_ignored,
op_test_event_step_result_ok,
op_test_event_step_wait,
op_test_event_start,
op_test_event_result_ok,
op_test_event_result_ignored,
op_test_event_result_failed,
op_test_get_origin,
} = core.ops;
const {
ArrayPrototypeFilter,
ArrayPrototypePush,
DateNow,
Error,
Map,
MapPrototypeGet,
MapPrototypeSet,
SafeArrayIterator,
SymbolToStringTag,
TypeError,
} = primordials;
import { setExitHandler } from "ext:runtime/30_os.js";
// Capture `Deno` global so that users deleting or mangling it, won't
// have impact on our sanitizers.
const DenoNs = globalThis.Deno;
/**
* @typedef {{
* id: number,
* name: string,
* fn: TestFunction
* origin: string,
* location: TestLocation,
* ignore: boolean,
* only: boolean,
* sanitizeOps: boolean,
* sanitizeResources: boolean,
* sanitizeExit: boolean,
* permissions: Deno.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: Deno.PermissionOptions,
* }} BenchDescription
*/
/** @type {Map<number, TestState | TestStepState>} */
const testStates = new Map();
// Wrap test function in additional assertion that makes sure
// that the test case does not accidentally exit prematurely.
function assertExit(fn, isTest) {
return async function exitSanitizer(...params) {
setExitHandler((exitCode) => {
throw new Error(
`${
isTest ? "Test case" : "Bench"
} attempted to exit with exit code: ${exitCode}`,
);
});
try {
const innerResult = await fn(...new SafeArrayIterator(params));
const exitCode = DenoNs.exitCode;
if (exitCode !== 0) {
// Reset the code to allow other tests to run...
DenoNs.exitCode = 0;
// ...and fail the current test.
throw new Error(
`${
isTest ? "Test case" : "Bench"
} finished with exit code set to ${exitCode}`,
);
}
if (innerResult) {
return innerResult;
}
} finally {
setExitHandler(null);
}
};
}
function wrapOuter(fn, desc) {
return async function outerWrapped() {
try {
if (desc.ignore) {
return "ignored";
}
return await fn(desc) ?? "ok";
} catch (error) {
return { failed: { jsError: core.destructureError(error) } };
} finally {
const state = MapPrototypeGet(testStates, desc.id);
for (const childDesc of state.children) {
stepReportResult(childDesc, { failed: "incomplete" }, 0);
}
state.completed = true;
}
};
}
function wrapInner(fn) {
/** @param {TestDescription | TestStepDescription} desc */
return async function innerWrapped(desc) {
function getRunningStepDescs() {
const results = [];
let childDesc = desc;
while (childDesc.parent != null) {
const state = MapPrototypeGet(testStates, childDesc.parent.id);
for (const siblingDesc of state.children) {
if (siblingDesc.id == childDesc.id) {
continue;
}
const siblingState = MapPrototypeGet(testStates, siblingDesc.id);
if (!siblingState.completed) {
ArrayPrototypePush(results, siblingDesc);
}
}
childDesc = childDesc.parent;
}
return results;
}
const runningStepDescs = getRunningStepDescs();
const runningStepDescsWithSanitizers = ArrayPrototypeFilter(
runningStepDescs,
(d) => usesSanitizer(d),
);
if (runningStepDescsWithSanitizers.length > 0) {
return {
failed: {
overlapsWithSanitizers: runningStepDescsWithSanitizers.map(
getFullName,
),
},
};
}
if (usesSanitizer(desc) && runningStepDescs.length > 0) {
return {
failed: {
hasSanitizersAndOverlaps: runningStepDescs.map(getFullName),
},
};
}
await fn(MapPrototypeGet(testStates, desc.id).context);
let failedSteps = 0;
for (const childDesc of MapPrototypeGet(testStates, desc.id).children) {
const state = MapPrototypeGet(testStates, childDesc.id);
if (!state.completed) {
return { failed: "incompleteSteps" };
}
if (state.failed) {
failedSteps++;
}
}
return failedSteps == 0 ? null : { failed: { failedSteps } };
};
}
const registerTestIdRetBuf = new Uint32Array(1);
const registerTestIdRetBufU8 = new Uint8Array(registerTestIdRetBuf.buffer);
const registerTestGroupIdRetBuf = new Uint32Array(1);
const registerTestGroupIdRetBufU8 = new Uint8Array(
registerTestGroupIdRetBuf.buffer,
);
/**
* As long as we're using one isolate per test, we can cache the origin
* since it won't change.
* @type {string | undefined}
*/
let cachedOrigin = undefined;
function testInner(
nameOrFnOrOptions,
optionsOrFn,
maybeFn,
overrides = { __proto__: null },
) {
// No-op if we're not running in `deno test` subcommand.
if (typeof op_register_test !== "function") {
return;
}
let testDesc;
const defaults = {
ignore: false,
only: false,
sanitizeOps: true,
sanitizeResources: true,
sanitizeExit: true,
permissions: null,
};
if (typeof nameOrFnOrOptions === "string") {
if (!nameOrFnOrOptions) {
throw new TypeError("The test name can't be empty");
}
if (typeof optionsOrFn === "function") {
testDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults };
} else {
if (!maybeFn || typeof maybeFn !== "function") {
throw new TypeError("Missing test function");
}
if (optionsOrFn.fn != undefined) {
throw new TypeError(
"Unexpected 'fn' field in options, test function is already provided as the third argument",
);
}
if (optionsOrFn.name != undefined) {
throw new TypeError(
"Unexpected 'name' field in options, test name is already provided as the first argument",
);
}
testDesc = {
...defaults,
...optionsOrFn,
fn: maybeFn,
name: nameOrFnOrOptions,
};
}
} else if (typeof nameOrFnOrOptions === "function") {
if (!nameOrFnOrOptions.name) {
throw new TypeError("The test function must have a name");
}
if (optionsOrFn != undefined) {
throw new TypeError("Unexpected second argument to Deno.test()");
}
if (maybeFn != undefined) {
throw new TypeError("Unexpected third argument to Deno.test()");
}
testDesc = {
...defaults,
fn: nameOrFnOrOptions,
name: nameOrFnOrOptions.name,
};
} else {
let fn;
let name;
if (typeof optionsOrFn === "function") {
fn = optionsOrFn;
if (nameOrFnOrOptions.fn != undefined) {
throw new TypeError(
"Unexpected 'fn' field in options, test function is already provided as the second argument",
);
}
name = nameOrFnOrOptions.name ?? fn.name;
} else {
if (
!nameOrFnOrOptions.fn || typeof nameOrFnOrOptions.fn !== "function"
) {
throw new TypeError(
"Expected 'fn' field in the first argument to be a test function",
);
}
fn = nameOrFnOrOptions.fn;
name = nameOrFnOrOptions.name ?? fn.name;
}
if (!name) {
throw new TypeError("The test name can't be empty");
}
testDesc = { ...defaults, ...nameOrFnOrOptions, fn, name };
}
testDesc = { ...testDesc, ...overrides };
// Delete this prop in case the user passed it. It's used to detect steps.
delete testDesc.parent;
if (cachedOrigin == undefined) {
cachedOrigin = op_test_get_origin();
}
testDesc.location = core.currentUserCallSite();
testDesc.fn = wrapTest(testDesc);
testDesc.name = escapeName(testDesc.name);
op_register_test(
ROOT_TEST_GROUP.id,
testDesc.fn,
testDesc.name,
testDesc.ignore,
testDesc.only,
testDesc.sanitizeOps,
testDesc.sanitizeResources,
testDesc.location.fileName,
testDesc.location.lineNumber,
testDesc.location.columnNumber,
registerTestIdRetBufU8,
);
testDesc.id = registerTestIdRetBuf[0];
testDesc.origin = cachedOrigin;
MapPrototypeSet(testStates, testDesc.id, {
context: createTestContext(testDesc),
children: [],
completed: false,
});
}
// Main test function provided by Deno.
function test(
nameOrFnOrOptions,
optionsOrFn,
maybeFn,
) {
return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn);
}
test.ignore = function (nameOrFnOrOptions, optionsOrFn, maybeFn) {
return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn, { ignore: true });
};
test.only = function (
nameOrFnOrOptions,
optionsOrFn,
maybeFn,
) {
return testInner(nameOrFnOrOptions, optionsOrFn, maybeFn, { only: true });
};
function getFullName(desc) {
if ("parent" in desc) {
return `${getFullName(desc.parent)} ... ${desc.name}`;
}
return desc.name;
}
function usesSanitizer(desc) {
return desc.sanitizeResources || desc.sanitizeOps || desc.sanitizeExit;
}
function stepReportResult(desc, result, elapsed) {
const state = MapPrototypeGet(testStates, desc.id);
for (const childDesc of state.children) {
stepReportResult(childDesc, { failed: "incomplete" }, 0);
}
if (result === "ok") {
op_test_event_step_result_ok(desc.id, elapsed);
} else if (result === "ignored") {
op_test_event_step_result_ignored(desc.id, elapsed);
} else {
op_test_event_step_result_failed(desc.id, result.failed, elapsed);
}
}
/** @param {TestDescription | TestStepDescription} desc */
function createTestContext(desc) {
let parent;
let level;
let rootId;
let rootName;
if ("parent" in desc) {
parent = MapPrototypeGet(testStates, desc.parent.id).context;
level = desc.level;
rootId = desc.rootId;
rootName = desc.rootName;
} else {
parent = undefined;
level = 0;
rootId = desc.id;
rootName = desc.name;
}
return {
[SymbolToStringTag]: "TestContext",
/**
* The current test name.
*/
name: desc.name,
/**
* Parent test context.
*/
parent,
/**
* File Uri of the test code.
*/
origin: desc.origin,
/**
* @param {string | TestStepDescription | ((t: TestContext) => void | Promise<void>)} nameOrFnOrOptions
* @param {((t: TestContext) => void | Promise<void>) | undefined} maybeFn
*/
async step(nameOrFnOrOptions, maybeFn) {
if (MapPrototypeGet(testStates, desc.id).completed) {
throw new Error(
"Cannot run test step after parent scope has finished execution. " +
"Ensure any `.step(...)` calls are executed before their parent scope completes execution.",
);
}
let stepDesc;
if (typeof nameOrFnOrOptions === "string") {
if (typeof maybeFn !== "function") {
throw new TypeError("Expected function for second argument");
}
stepDesc = {
name: nameOrFnOrOptions,
fn: maybeFn,
};
} else if (typeof nameOrFnOrOptions === "function") {
if (!nameOrFnOrOptions.name) {
throw new TypeError("The step function must have a name");
}
if (maybeFn != undefined) {
throw new TypeError(
"Unexpected second argument to TestContext.step()",
);
}
stepDesc = {
name: nameOrFnOrOptions.name,
fn: nameOrFnOrOptions,
};
} else if (typeof nameOrFnOrOptions === "object") {
stepDesc = nameOrFnOrOptions;
} else {
throw new TypeError(
"Expected a test definition or name and function",
);
}
stepDesc.ignore ??= false;
stepDesc.sanitizeOps ??= desc.sanitizeOps;
stepDesc.sanitizeResources ??= desc.sanitizeResources;
stepDesc.sanitizeExit ??= desc.sanitizeExit;
stepDesc.location = core.currentUserCallSite();
stepDesc.level = level + 1;
stepDesc.parent = desc;
stepDesc.rootId = rootId;
stepDesc.name = escapeName(stepDesc.name);
stepDesc.rootName = escapeName(rootName);
stepDesc.fn = wrapTest(stepDesc);
const id = op_register_test_step(
stepDesc.name,
stepDesc.location.fileName,
stepDesc.location.lineNumber,
stepDesc.location.columnNumber,
stepDesc.level,
stepDesc.parent.id,
stepDesc.rootId,
stepDesc.rootName,
);
stepDesc.id = id;
stepDesc.origin = desc.origin;
const state = {
context: createTestContext(stepDesc),
children: [],
failed: false,
completed: false,
};
MapPrototypeSet(testStates, stepDesc.id, state);
ArrayPrototypePush(
MapPrototypeGet(testStates, stepDesc.parent.id).children,
stepDesc,
);
op_test_event_step_wait(stepDesc.id);
const earlier = DateNow();
const result = await stepDesc.fn(stepDesc);
const elapsed = DateNow() - earlier;
state.failed = !!result.failed;
stepReportResult(stepDesc, result, elapsed);
return result == "ok";
},
};
}
/**
* Wrap a user test function in one which returns a structured result.
* @template {Function} T
* @param {TestDescription | TestStepDescription} desc
* @returns {T}
*/
function wrapTest(desc) {
let testFn = wrapInner(desc.fn);
if (desc.sanitizeExit) {
testFn = assertExit(testFn, true);
}
if (!("parent" in desc) && desc.permissions) {
testFn = withPermissions(testFn, desc.permissions);
}
return wrapOuter(testFn, desc);
}
globalThis.Deno.test = test;
/**
* @typedef {{
* id: number,
* parentId: number,
* name: string,
* fn: () => any,
* only: boolean,
* ignore: boolean,
* location: TestLocationInfo,
* sanitizeOps: boolean,
* sanitizeResources: boolean,
* sanitizeExit: boolean,
* permissions?: Deno.PermissionOptions,
* }} BddTest
*
* @typedef {() => unknown | Promise<unknown>} TestLifecycleFn
*
* @typedef {{
* id: number,
* parentId: number,
* name: string,
* ignore: boolean,
* only: boolean,
* children: Array<TestGroup | BddTest>,
* beforeAll: TestLifecycleFn | null,
* afterAll: TestLifecycleFn | null,
* beforeEach: TestLifecycleFn | null,
* afterEach: TestLifecycleFn | null
* sanitizeOps: boolean,
* sanitizeResources: boolean,
* sanitizeExit: boolean,
* permissions?: Deno.PermissionOptions,
* }} TestGroup
*
* @typedef {{
* only: boolean,
* ignore: boolean,
* name: string,
* fn: () => any,
* sanitizeOps: boolean,
* sanitizeResources: boolean,
* sanitizeExit: boolean,
* permissions?: Deno.PermissionOptions,
* }} BddArgs
*/
/** @type {TestGroup} */
const ROOT_TEST_GROUP = {
id: 0,
parentId: 0,
name: "__DENO_TEST_ROOT__",
ignore: false,
only: false,
children: [],
beforeAll: null,
beforeEach: null,
afterAll: null,
afterEach: null,
sanitizeExit: false,
sanitizeOps: false,
sanitizeResources: false,
permissions: undefined,
};
// No-op if we're not running in `deno test` subcommand.
if (typeof op_register_test === "function") {
op_test_group_register(
registerTestGroupIdRetBufU8,
ROOT_TEST_GROUP.name,
ROOT_TEST_GROUP.parentId,
);
ROOT_TEST_GROUP.id = registerTestGroupIdRetBuf[0];
}
/** @type {{ hasOnly: boolean, stack: TestGroup[], total: number }} */
const BDD_CONTEXT = {
hasOnly: false,
stack: [ROOT_TEST_GROUP],
total: 0,
};
/**
* @overload
* @param {() => any} nameOrFnOrOptions
* @returns {BddArgs}
*/
/**
* @overload
* @param {BddArgs} nameOrFnOrOptions
* @returns {BddArgs}
*/
/**
* @overload
* @param {string} nameOrFnOrOptions
* @param {() => any} fnOrOptions
* @returns {BddArgs}
*/
/**
* @overload
* @param {string} nameOrFnOrOptions
* @param {BddArgs} fnOrOptions
* @param {() => any} maybeFn
* @returns {BddArgs}
*/
/**
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
* @returns {BddArgs}
*/
function normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn) {
let name = "";
let fn;
let only = false;
let ignore = false;
let sanitizeExit = false;
let sanitizeOps = false;
let sanitizeResources = false;
let permissions;
if (typeof nameOrFnOrOptions === "function") {
name = nameOrFnOrOptions.name;
fn = nameOrFnOrOptions;
} else if (typeof nameOrFnOrOptions === "object") {
return nameOrFnOrOptions;
} else if (typeof fnOrOptions === "function") {
name = nameOrFnOrOptions;
fn = fnOrOptions;
} else if (fnOrOptions !== undefined && maybeFn !== undefined) {
name = nameOrFnOrOptions;
only = fnOrOptions.only;
ignore = fnOrOptions.ignore;
sanitizeExit = fnOrOptions.sanitizeExit,
sanitizeOps = fnOrOptions.sanitizeOps,
sanitizeResources = fnOrOptions.sanitizeResources;
permissions = fnOrOptions.permissions;
fn = maybeFn;
} else {
throw new TypeError(`Invalid arguments passed to "Deno.test/it/describe"`);
}
return {
name: escapeName(name),
fn,
only,
ignore,
sanitizeExit,
sanitizeOps,
sanitizeResources,
permissions,
};
}
/**
* @param {BddArgs} args
*/
function itInner({
name,
fn,
ignore,
only,
sanitizeExit,
sanitizeOps,
sanitizeResources,
permissions,
}) {
if (
!ignore && BDD_CONTEXT.stack.length > 1 &&
BDD_CONTEXT.stack.some((x) => x.ignore)
) {
ignore = true;
}
if (cachedOrigin == undefined) {
cachedOrigin = op_test_get_origin();
}
const location = core.currentUserCallSite();
const parent = getGroupParent();
let testFn = fn;
if (permissions !== undefined) {
testFn = withPermissions(testFn);
}
/** @type {BddTest} */
const testDef = {
id: 0,
parentId: parent.id,
name,
fn: testFn,
ignore,
only,
location,
sanitizeExit,
sanitizeOps,
sanitizeResources,
permissions,
};
parent.children.push(testDef);
BDD_CONTEXT.total++;
op_register_test(
testDef.parentId,
testDef.fn,
escapeName(name),
ignore,
only,
sanitizeOps,
sanitizeResources,
location.fileName,
location.lineNumber,
location.columnNumber,
registerTestIdRetBufU8,
);
testDef.id = registerTestIdRetBuf[0];
}
/**
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
*/
function it(nameOrFnOrOptions, fnOrOptions, maybeFn) {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
if (args.only) BDD_CONTEXT.hasOnly = true;
itInner(args);
}
/**
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
*/
it.only = (nameOrFnOrOptions, fnOrOptions, maybeFn) => {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
BDD_CONTEXT.hasOnly = true;
args.only = true;
args.ignore = false;
itInner(args);
};
/**
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
*/
it.ignore = (nameOrFnOrOptions, fnOrOptions, maybeFn) => {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
args.ignore = true;
args.only = false;
itInner(args);
};
it.skip = it.ignore;
/** @type {(x: TestGroup | BddTest) => x is TestGroup} */
function isTestGroup(x) {
return "beforeAll" in x;
}
/**
* @returns {TestGroup}
*/
function getGroupParent() {
return /** @type {TestGroup} */ (BDD_CONTEXT.stack.at(-1));
}
/**
* @param {BddArgs} args
*/
function describeInner(
{
name,
fn,
ignore,
only,
sanitizeExit,
sanitizeOps,
sanitizeResources,
permissions,
},
) {
// No-op if we're not running in `deno test` subcommand.
if (typeof op_register_test !== "function") {
return;
}
const parent = getGroupParent();
op_test_group_register(registerTestGroupIdRetBufU8, name, parent.id);
const id = registerTestGroupIdRetBuf[0];
/** @type {TestGroup} */
const group = {
id,
parentId: parent.id,
name,
ignore,
only,
children: [],
beforeAll: null,
beforeEach: null,
afterAll: null,
afterEach: null,
sanitizeExit,
sanitizeOps,
sanitizeResources,
permissions,
};
parent.children.push(group);
BDD_CONTEXT.stack.push(group);
try {
fn();
} finally {
let allIgnore = true;
let onlyChildCount = 0;
for (let i = 0; i < group.children.length; i++) {
const child = group.children[i];
if (!child.ignore) allIgnore = false;
if (!isTestGroup(child) && child.only) {
onlyChildCount++;
}
}
if (!group.ignore) {
group.ignore = allIgnore;
}
if (!group.ignore) {
if (onlyChildCount > 0) {
group.only = true;
if (onlyChildCount < group.children.length - 1) {
for (let i = 0; i < group.children.length; i++) {
const child = group.children[i];
if (!isTestGroup(child) && !child.only) {
child.ignore = true;
}
}
}
} else if (group.only) {
for (let i = 0; i < group.children.length; i++) {
const child = group.children[i];
child.only = true;
}
}
}
BDD_CONTEXT.stack.pop();
}
}
/**
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
*/
function describe(nameOrFnOrOptions, fnOrOptions, maybeFn) {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
if (args.only) BDD_CONTEXT.hasOnly = true;
describeInner(args);
}
/**
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
*/
describe.only = (nameOrFnOrOptions, fnOrOptions, maybeFn) => {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
BDD_CONTEXT.hasOnly = true;
args.only = true;
args.ignore = false;
describeInner(args);
};
/**
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
*/
describe.ignore = (nameOrFnOrOptions, fnOrOptions, maybeFn) => {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
args.only = false;
args.ignore = true;
describeInner(args);
};
describe.skip = describe.ignore;
/**
* @param {() => any} fn
*/
function beforeAll(fn) {
getGroupParent().beforeAll = fn;
}
/**
* @param {() => any} fn
*/
function afterAll(fn) {
getGroupParent().afterAll = fn;
}
/**
* @param {() => any} fn
*/
function beforeEach(fn) {
getGroupParent().beforeEach = fn;
}
/**
* @param {() => any} fn
*/
function afterEach(fn) {
getGroupParent().afterEach = fn;
}
globalThis.before = beforeAll;
globalThis.beforeAll = beforeAll;
globalThis.after = afterAll;
globalThis.afterAll = afterAll;
globalThis.beforeEach = beforeEach;
globalThis.afterEach = afterEach;
globalThis.it = it;
globalThis.describe = describe;
/**
* @param {number} seed
*/
function prepareTests(seed) {
const hasOnly = BDD_CONTEXT.hasOnly;
if (hasOnly) {
ROOT_TEST_GROUP.only = ROOT_TEST_GROUP.children.some((child) => child.only);
}
const stack = [ROOT_TEST_GROUP];
/** @type {TestGroup | undefined} */
let group;
// deno-lint-ignore no-extra-boolean-cast
while (!!(group = stack.pop())) {
if (hasOnly && !group.only) {
group.ignore = true;
}
if (seed > 0 && !group.ignore && group.children.length > 1) {
shuffle(group.children, seed);
}
// Sort tests:
// - non-ignored tests first (might be shuffled earlier)
// - ignored tests second
// - groups last
group.children.sort(sortTestItems);
for (let i = 0; i < group.children.length; i++) {
const child = group.children[i];
if (group.ignore) {
child.ignore = true;
}
if (isTestGroup(child)) {
stack.push(child);
}
}
}
}
/**
* @param {*} seed
* @param {*} group
*/
function prepareGroup(seed, group) {
}
/**
* This function is called from Rust.
* @param {number} seed
* @param {...any} rest
*/
async function runTests(seed, ...rest) {
if (BDD_CONTEXT.hasOnly) {
ROOT_TEST_GROUP.only = ROOT_TEST_GROUP.children.some((child) => child.only);
}
// console.log("RUN TESTS", BDD_CONTEXT.hasOnly, seed, rest, ROOT_TEST_GROUP);
try {
await runGroup(seed, ROOT_TEST_GROUP);
} finally {
//
}
}
/**
* @param {number} seed
* @param {TestGroup} group
*/
async function runGroup(seed, group) {
op_test_group_event_start(group.id);
if (BDD_CONTEXT.hasOnly && !group.only) {
group.ignore = true;
}
if (seed > 0 && !group.ignore && group.children.length > 1) {
shuffle(group.children, seed);
}
// Sort tests:
// - non-ignored tests first (might be shuffled earlier)
// - ignored tests second
// - groups last
group.children.sort(sortTestItems);
try {
if (!group.ignore && group.beforeAll !== null) {
await group.beforeAll();
}
for (let i = 0; i < group.children.length; i++) {
const child = group.children[i];
if (!group.ignore && group.beforeEach !== null) {
await group.beforeEach();
}
if (isTestGroup(child)) {
await runGroup(seed, child);
} else if (
child.ignore || group.ignore || BDD_CONTEXT.hasOnly && !child.only
) {
op_test_event_result_ignored(child.id);
} else {
op_test_event_start(child.id);
const start = DateNow();
try {
await child.fn();
const elapsed = DateNow() - start;
op_test_event_result_ok(child.id, elapsed);
} catch (err) {
const elapsed = DateNow() - start;
op_test_event_result_failed(child.id, elapsed);
}
}
if (!group.ignore && group.afterEach !== null) {
await group.afterEach();
}
}
if (!group.ignore && group.afterAll !== null) {
await group.afterAll();
}
} finally {
op_test_group_event_end(group.id);
}
}
/**
* @param {TestGroup | BddTest} a
* @param {TestGroup | BddTest} b
*/
function sortTestItems(a, b) {
const isAGroup = isTestGroup(a);
const isBGroup = isTestGroup(b);
if (isAGroup && isBGroup) return 0;
if (isAGroup && !isBGroup) return 1;
if (!isAGroup && isBGroup) return -1;
if (a.ignore && b.ignore) return 0;
if (a.ignore && !b.ignore) return 1;
if (!a.ignore && b.ignore) return -1;
return 0;
}
/**
* @template T
* @param {T[]} arr
* @param {number} seed
*/
function shuffle(arr, seed) {
let m = arr.length;
let t;
let i;
while (m) {
i = Math.floor(randomize(seed) * m--);
t = arr[m];
arr[m] = arr[i];
arr[i] = t;
}
}
/**
* @param {number} seed
* @returns {number}
*/
function randomize(seed) {
const x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
}
// No-op if we're not running in `deno test` subcommand.
if (typeof op_register_test === "function") {
op_register_test_run_fn(runTests);
}