1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-30 16:40:57 -05:00
denoland-deno/cli/js/40_test.js

1097 lines
27 KiB
JavaScript
Raw Normal View History

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2024-10-10 07:54:58 -04:00
// @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,
2024-10-10 07:54:58 -04:00
op_test_group_register,
op_test_group_event_start,
op_test_group_event_end,
2024-10-09 19:37:56 -04:00
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,
2024-10-10 07:54:58 -04:00
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,
2024-10-10 07:54:58 -04:00
* only: boolean,
* sanitizeOps: boolean,
* sanitizeResources: boolean,
* sanitizeExit: boolean,
2024-10-10 07:54:58 -04:00
* 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,
2024-10-10 07:54:58 -04:00
* only: boolean,
* sanitizeExit: boolean,
2024-10-10 07:54:58 -04:00
* 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) {
2024-10-10 07:54:58 -04:00
/** @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);
2024-10-10 07:54:58 -04:00
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(
2024-10-10 10:09:48 -04:00
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);
}
}
2024-10-10 07:54:58 -04:00
/** @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,
/**
2024-10-10 07:54:58 -04:00
* @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.
2024-10-10 07:54:58 -04:00
* @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;
2024-10-09 03:51:57 -04:00
2024-10-10 07:54:58 -04:00
/**
* @typedef {{
* id: number,
2024-10-10 10:09:48 -04:00
* parentId: number,
2024-10-10 07:54:58 -04:00
* name: string,
* fn: () => any,
* only: boolean,
2024-10-10 11:14:28 -04:00
* ignore: boolean,
* location: TestLocationInfo,
2024-10-11 08:20:52 -04:00
* sanitizeOps: boolean,
* sanitizeResources: boolean,
* sanitizeExit: boolean,
* permissions?: Deno.PermissionOptions,
2024-10-10 07:54:58 -04:00
* }} BddTest
*
* @typedef {() => unknown | Promise<unknown>} TestLifecycleFn
*
* @typedef {{
* id: number,
2024-10-10 10:09:48 -04:00
* parentId: number,
2024-10-10 07:54:58 -04:00
* name: string,
* ignore: boolean,
* only: boolean,
* children: Array<TestGroup | BddTest>,
* beforeAll: TestLifecycleFn | null,
* afterAll: TestLifecycleFn | null,
* beforeEach: TestLifecycleFn | null,
* afterEach: TestLifecycleFn | null
2024-10-10 11:14:28 -04:00
* sanitizeOps: boolean,
* sanitizeResources: boolean,
* sanitizeExit: boolean,
* permissions?: Deno.PermissionOptions,
2024-10-10 07:54:58 -04:00
* }} TestGroup
2024-10-10 11:14:28 -04:00
*
* @typedef {{
* only: boolean,
* ignore: boolean,
* name: string,
* fn: () => any,
* sanitizeOps: boolean,
* sanitizeResources: boolean,
* sanitizeExit: boolean,
* permissions?: Deno.PermissionOptions,
* }} BddArgs
2024-10-10 07:54:58 -04:00
*/
2024-10-09 19:37:56 -04:00
2024-10-10 07:54:58 -04:00
/** @type {TestGroup} */
2024-10-09 19:37:56 -04:00
const ROOT_TEST_GROUP = {
2024-10-10 07:54:58 -04:00
id: 0,
2024-10-10 10:09:48 -04:00
parentId: 0,
2024-10-10 07:54:58 -04:00
name: "__DENO_TEST_ROOT__",
2024-10-09 19:37:56 -04:00
ignore: false,
only: false,
children: [],
beforeAll: null,
beforeEach: null,
afterAll: null,
afterEach: null,
2024-10-10 11:14:28 -04:00
sanitizeExit: false,
sanitizeOps: false,
sanitizeResources: false,
permissions: undefined,
2024-10-09 19:37:56 -04:00
};
2024-10-10 07:54:58 -04:00
// 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,
2024-10-10 10:09:48 -04:00
ROOT_TEST_GROUP.parentId,
2024-10-10 07:54:58 -04:00
);
ROOT_TEST_GROUP.id = registerTestGroupIdRetBuf[0];
}
2024-10-09 19:37:56 -04:00
/** @type {{ hasOnly: boolean, stack: TestGroup[], total: number }} */
const BDD_CONTEXT = {
hasOnly: false,
stack: [ROOT_TEST_GROUP],
total: 0,
};
2024-10-09 03:51:57 -04:00
/**
2024-10-10 11:14:28 -04:00
* @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
2024-10-09 03:51:57 -04:00
*/
2024-10-10 11:14:28 -04:00
function itInner({
name,
fn,
ignore,
only,
sanitizeExit,
sanitizeOps,
sanitizeResources,
permissions,
}) {
2024-10-10 07:54:58 -04:00
if (
!ignore && BDD_CONTEXT.stack.length > 1 &&
BDD_CONTEXT.stack.some((x) => x.ignore)
) {
ignore = true;
2024-10-09 18:01:59 -04:00
}
if (cachedOrigin == undefined) {
cachedOrigin = op_test_get_origin();
}
const location = core.currentUserCallSite();
2024-10-10 10:09:48 -04:00
const parent = getGroupParent();
2024-10-11 08:20:52 -04:00
let testFn = fn;
if (permissions !== undefined) {
testFn = withPermissions(testFn);
}
2024-10-09 19:37:56 -04:00
/** @type {BddTest} */
const testDef = {
2024-10-10 07:54:58 -04:00
id: 0,
2024-10-10 10:09:48 -04:00
parentId: parent.id,
2024-10-09 19:37:56 -04:00
name,
2024-10-11 08:20:52 -04:00
fn: testFn,
2024-10-09 19:37:56 -04:00
ignore,
only,
2024-10-10 11:14:28 -04:00
location,
2024-10-11 08:20:52 -04:00
sanitizeExit,
sanitizeOps,
sanitizeResources,
permissions,
2024-10-09 19:37:56 -04:00
};
2024-10-10 10:09:48 -04:00
parent.children.push(testDef);
2024-10-09 19:37:56 -04:00
BDD_CONTEXT.total++;
2024-10-09 18:01:59 -04:00
op_register_test(
2024-10-11 08:20:52 -04:00
testDef.parentId,
testDef.fn,
2024-10-09 18:01:59 -04:00
escapeName(name),
2024-10-09 03:51:57 -04:00
ignore,
only,
2024-10-09 18:01:59 -04:00
sanitizeOps,
sanitizeResources,
location.fileName,
location.lineNumber,
location.columnNumber,
registerTestIdRetBufU8,
);
2024-10-10 07:54:58 -04:00
testDef.id = registerTestIdRetBuf[0];
2024-10-09 03:51:57 -04:00
}
/**
2024-10-10 11:14:28 -04:00
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
2024-10-09 03:51:57 -04:00
*/
2024-10-10 11:14:28 -04:00
function it(nameOrFnOrOptions, fnOrOptions, maybeFn) {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
if (args.only) BDD_CONTEXT.hasOnly = true;
itInner(args);
2024-10-09 03:51:57 -04:00
}
/**
2024-10-10 11:14:28 -04:00
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
2024-10-09 03:51:57 -04:00
*/
2024-10-10 11:14:28 -04:00
it.only = (nameOrFnOrOptions, fnOrOptions, maybeFn) => {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
2024-10-09 19:37:56 -04:00
BDD_CONTEXT.hasOnly = true;
2024-10-10 11:14:28 -04:00
args.only = true;
args.ignore = false;
itInner(args);
2024-10-09 03:51:57 -04:00
};
/**
2024-10-10 11:14:28 -04:00
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
2024-10-09 03:51:57 -04:00
*/
2024-10-10 11:14:28 -04:00
it.ignore = (nameOrFnOrOptions, fnOrOptions, maybeFn) => {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
args.ignore = true;
args.only = false;
itInner(args);
2024-10-09 03:51:57 -04:00
};
it.skip = it.ignore;
2024-10-10 07:54:58 -04:00
/** @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));
}
2024-10-09 03:51:57 -04:00
/**
2024-10-10 11:14:28 -04:00
* @param {BddArgs} args
2024-10-09 03:51:57 -04:00
*/
2024-10-10 11:14:28 -04:00
function describeInner(
{
name,
fn,
ignore,
only,
sanitizeExit,
sanitizeOps,
sanitizeResources,
permissions,
},
) {
2024-10-09 18:01:59 -04:00
// No-op if we're not running in `deno test` subcommand.
if (typeof op_register_test !== "function") {
return;
}
2024-10-10 10:09:48 -04:00
const parent = getGroupParent();
op_test_group_register(registerTestGroupIdRetBufU8, name, parent.id);
2024-10-10 07:54:58 -04:00
const id = registerTestGroupIdRetBuf[0];
2024-10-09 19:37:56 -04:00
/** @type {TestGroup} */
const group = {
2024-10-10 07:54:58 -04:00
id,
2024-10-10 10:09:48 -04:00
parentId: parent.id,
2024-10-09 19:37:56 -04:00
name,
ignore,
only,
children: [],
beforeAll: null,
beforeEach: null,
afterAll: null,
afterEach: null,
2024-10-10 11:14:28 -04:00
sanitizeExit,
sanitizeOps,
sanitizeResources,
permissions,
2024-10-09 19:37:56 -04:00
};
parent.children.push(group);
BDD_CONTEXT.stack.push(group);
2024-10-09 03:51:57 -04:00
try {
fn();
} finally {
2024-10-10 07:54:58 -04:00
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;
}
}
}
2024-10-09 19:37:56 -04:00
BDD_CONTEXT.stack.pop();
2024-10-09 03:51:57 -04:00
}
}
/**
2024-10-10 11:14:28 -04:00
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
2024-10-09 03:51:57 -04:00
*/
2024-10-10 11:14:28 -04:00
function describe(nameOrFnOrOptions, fnOrOptions, maybeFn) {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
if (args.only) BDD_CONTEXT.hasOnly = true;
describeInner(args);
2024-10-09 03:51:57 -04:00
}
/**
2024-10-10 11:14:28 -04:00
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
2024-10-09 03:51:57 -04:00
*/
2024-10-10 11:14:28 -04:00
describe.only = (nameOrFnOrOptions, fnOrOptions, maybeFn) => {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
2024-10-09 19:37:56 -04:00
BDD_CONTEXT.hasOnly = true;
2024-10-10 11:14:28 -04:00
args.only = true;
args.ignore = false;
describeInner(args);
2024-10-09 03:51:57 -04:00
};
/**
2024-10-10 11:14:28 -04:00
* @param {string | (() => any) | BddArgs} nameOrFnOrOptions
* @param {(() => any) | BddArgs} [fnOrOptions]
* @param {(() => any)} [maybeFn]
2024-10-09 03:51:57 -04:00
*/
2024-10-10 11:14:28 -04:00
describe.ignore = (nameOrFnOrOptions, fnOrOptions, maybeFn) => {
const args = normalizeBddArgs(nameOrFnOrOptions, fnOrOptions, maybeFn);
args.only = false;
args.ignore = true;
describeInner(args);
2024-10-09 03:51:57 -04:00
};
describe.skip = describe.ignore;
/**
* @param {() => any} fn
*/
function beforeAll(fn) {
2024-10-10 07:54:58 -04:00
getGroupParent().beforeAll = fn;
2024-10-09 03:51:57 -04:00
}
2024-10-09 18:01:59 -04:00
2024-10-09 03:51:57 -04:00
/**
* @param {() => any} fn
*/
function afterAll(fn) {
2024-10-10 07:54:58 -04:00
getGroupParent().afterAll = fn;
2024-10-09 03:51:57 -04:00
}
2024-10-09 18:01:59 -04:00
2024-10-09 03:51:57 -04:00
/**
* @param {() => any} fn
*/
function beforeEach(fn) {
2024-10-10 07:54:58 -04:00
getGroupParent().beforeEach = fn;
2024-10-09 03:51:57 -04:00
}
/**
* @param {() => any} fn
*/
function afterEach(fn) {
2024-10-10 07:54:58 -04:00
getGroupParent().afterEach = fn;
2024-10-09 03:51:57 -04:00
}
globalThis.before = beforeAll;
globalThis.beforeAll = beforeAll;
globalThis.after = afterAll;
globalThis.afterAll = afterAll;
globalThis.beforeEach = beforeEach;
globalThis.afterEach = afterEach;
globalThis.it = it;
globalThis.describe = describe;
2024-10-09 19:37:56 -04:00
/**
* This function is called from Rust.
2024-10-10 07:54:58 -04:00
* @param {number} seed
2024-10-09 19:37:56 -04:00
* @param {...any} rest
*/
async function runTests(seed, ...rest) {
2024-10-10 07:54:58 -04:00
if (BDD_CONTEXT.hasOnly) {
ROOT_TEST_GROUP.only = ROOT_TEST_GROUP.children.some((child) => child.only);
}
2024-10-09 19:37:56 -04:00
2024-10-10 10:09:48 -04:00
// console.log("RUN TESTS", BDD_CONTEXT.hasOnly, seed, rest, ROOT_TEST_GROUP);
2024-10-10 07:54:58 -04:00
try {
await runGroup(seed, ROOT_TEST_GROUP);
} finally {
//
}
2024-10-09 19:37:56 -04:00
}
/**
2024-10-10 07:54:58 -04:00
* @param {number} seed
2024-10-09 19:37:56 -04:00
* @param {TestGroup} group
*/
async function runGroup(seed, group) {
2024-10-10 07:54:58 -04:00
op_test_group_event_start(group.id);
2024-10-09 19:37:56 -04:00
2024-10-10 10:09:48 -04:00
if (BDD_CONTEXT.hasOnly && !group.only) {
group.ignore = true;
}
if (seed > 0 && !group.ignore && group.children.length > 1) {
2024-10-10 07:54:58 -04:00
shuffle(group.children, seed);
2024-10-09 19:37:56 -04:00
}
2024-10-10 07:54:58 -04:00
// Sort tests:
// - non-ignored tests first (might be shuffled earlier)
// - ignored tests second
// - groups last
group.children.sort(sortTestItems);
try {
2024-10-10 10:09:48 -04:00
if (!group.ignore && group.beforeAll !== null) {
2024-10-10 07:54:58 -04:00
await group.beforeAll();
}
2024-10-09 19:37:56 -04:00
2024-10-10 07:54:58 -04:00
for (let i = 0; i < group.children.length; i++) {
const child = group.children[i];
2024-10-09 19:37:56 -04:00
2024-10-10 10:09:48 -04:00
if (!group.ignore && group.beforeEach !== null) {
2024-10-10 07:54:58 -04:00
await group.beforeEach();
}
if (isTestGroup(child)) {
await runGroup(seed, child);
2024-10-11 08:20:52 -04:00
} else if (
child.ignore || group.ignore || BDD_CONTEXT.hasOnly && !child.only
) {
2024-10-10 07:54:58 -04:00
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);
}
}
2024-10-09 19:37:56 -04:00
2024-10-10 10:09:48 -04:00
if (!group.ignore && group.afterEach !== null) {
2024-10-10 07:54:58 -04:00
await group.afterEach();
}
}
2024-10-09 19:37:56 -04:00
2024-10-10 10:09:48 -04:00
if (!group.ignore && group.afterAll !== null) {
2024-10-10 07:54:58 -04:00
await group.afterAll();
}
} finally {
op_test_group_event_end(group.id);
2024-10-09 19:37:56 -04:00
}
2024-10-10 07:54:58 -04:00
}
2024-10-09 19:37:56 -04:00
2024-10-10 07:54:58 -04:00
/**
* @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;
2024-10-10 10:09:48 -04:00
if (isAGroup && !isBGroup) return 1;
if (!isAGroup && isBGroup) return -1;
2024-10-10 07:54:58 -04:00
if (a.ignore && b.ignore) return 0;
2024-10-10 10:09:48 -04:00
if (a.ignore && !b.ignore) return 1;
if (!a.ignore && b.ignore) return -1;
2024-10-10 07:54:58 -04:00
return 0;
2024-10-09 19:37:56 -04:00
}
/**
* @template T
* @param {T[]} arr
2024-10-10 07:54:58 -04:00
* @param {number} seed
2024-10-09 19:37:56 -04:00
*/
function shuffle(arr, seed) {
let m = arr.length;
let t;
let i;
while (m) {
2024-10-10 07:54:58 -04:00
i = Math.floor(randomize(seed) * m--);
2024-10-09 19:37:56 -04:00
t = arr[m];
arr[m] = arr[i];
arr[i] = t;
}
}
2024-10-10 07:54:58 -04:00
/**
* @param {number} seed
* @returns {number}
*/
function randomize(seed) {
const x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
}
2024-10-09 19:37:56 -04:00
// No-op if we're not running in `deno test` subcommand.
if (typeof op_register_test === "function") {
op_register_test_run_fn(runTests);
}