summaryrefslogtreecommitdiff
path: root/runtime/js/40_testing.js
diff options
context:
space:
mode:
authorNayeem Rahman <nayeemrmn99@gmail.com>2022-07-15 18:09:22 +0100
committerGitHub <noreply@github.com>2022-07-15 13:09:22 -0400
commit22a4998e299ca8658744c745d8f0afeba15730dc (patch)
tree1db6f394457a3c1bcb75388560f7d1667ae1a9ac /runtime/js/40_testing.js
parent635eed93731c3616cacf53860b9aeeeb8cfe158b (diff)
refactor: allocate IDs for tests (#14729)
Diffstat (limited to 'runtime/js/40_testing.js')
-rw-r--r--runtime/js/40_testing.js753
1 files changed, 311 insertions, 442 deletions
diff --git a/runtime/js/40_testing.js b/runtime/js/40_testing.js
index 48f2fefb8..fabdb8dc4 100644
--- a/runtime/js/40_testing.js
+++ b/runtime/js/40_testing.js
@@ -14,25 +14,20 @@
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeShift,
- ArrayPrototypeSome,
ArrayPrototypeSort,
DateNow,
Error,
FunctionPrototype,
Map,
+ MapPrototypeGet,
MapPrototypeHas,
+ MapPrototypeSet,
MathCeil,
ObjectKeys,
ObjectPrototypeIsPrototypeOf,
Promise,
- RegExp,
- RegExpPrototypeTest,
SafeArrayIterator,
Set,
- StringPrototypeEndsWith,
- StringPrototypeIncludes,
- StringPrototypeSlice,
- StringPrototypeStartsWith,
SymbolToStringTag,
TypeError,
} = window.__bootstrap.primordials;
@@ -139,12 +134,12 @@
// ops. Note that "unref" ops are ignored since in nature that are
// optional.
function assertOps(fn) {
- /** @param step {TestStep} */
- return async function asyncOpSanitizer(step) {
+ /** @param desc {TestDescription | TestStepDescription} */
+ return async function asyncOpSanitizer(desc) {
const pre = core.metrics();
const preTraces = new Map(core.opCallTraces);
try {
- await fn(step);
+ await fn(desc);
} finally {
// Defer until next event loop turn - that way timeouts and intervals
// cleared can actually be removed from resource table, otherwise
@@ -152,7 +147,7 @@
await opSanitizerDelay();
}
- if (step.shouldSkipSanitizers) return;
+ if (shouldSkipSanitizers(desc)) return;
const post = core.metrics();
const postTraces = new Map(core.opCallTraces);
@@ -366,15 +361,13 @@
// Wrap test function in additional assertion that makes sure
// the test case does not "leak" resources - ie. resource table after
// the test has exactly the same contents as before the test.
- function assertResources(
- fn,
- ) {
- /** @param step {TestStep} */
- return async function resourceSanitizer(step) {
+ function assertResources(fn) {
+ /** @param desc {TestDescription | TestStepDescription} */
+ return async function resourceSanitizer(desc) {
const pre = core.resources();
- await fn(step);
+ await fn(desc);
- if (step.shouldSkipSanitizers) {
+ if (shouldSkipSanitizers(desc)) {
return;
}
@@ -396,12 +389,12 @@
const hint = resourceCloseHint(postResource);
const detail =
`${name} (rid ${resource}) was ${action1} during the test, but not ${action2} during the test. ${hint}`;
- details.push(detail);
+ ArrayPrototypePush(details, detail);
} else {
const [name, action1, action2] = prettyResourceNames(preResource);
const detail =
`${name} (rid ${resource}) was ${action1} before the test started, but was ${action2} during the test. Do not close resources in a test that were not created during that test.`;
- details.push(detail);
+ ArrayPrototypePush(details, detail);
}
}
@@ -439,79 +432,81 @@
}
function assertTestStepScopes(fn) {
- /** @param step {TestStep} */
- return async function testStepSanitizer(step) {
+ /** @param desc {TestDescription | TestStepDescription} */
+ return async function testStepSanitizer(desc) {
preValidation();
// only report waiting after pre-validation
- if (step.canStreamReporting()) {
- step.reportWait();
+ if (canStreamReporting(desc) && "parent" in desc) {
+ stepReportWait(desc);
}
- await fn(createTestContext(step));
- postValidation();
+ await fn(MapPrototypeGet(testStates, desc.id).context);
+ testStepPostValidation(desc);
function preValidation() {
- const runningSteps = getPotentialConflictingRunningSteps();
- const runningStepsWithSanitizers = ArrayPrototypeFilter(
- runningSteps,
- (t) => t.usesSanitizer,
+ const runningStepDescs = getRunningStepDescs();
+ const runningStepDescsWithSanitizers = ArrayPrototypeFilter(
+ runningStepDescs,
+ (d) => usesSanitizer(d),
);
- if (runningStepsWithSanitizers.length > 0) {
+ if (runningStepDescsWithSanitizers.length > 0) {
throw new Error(
"Cannot start test step while another test step with sanitizers is running.\n" +
- runningStepsWithSanitizers
- .map((s) => ` * ${s.getFullName()}`)
+ runningStepDescsWithSanitizers
+ .map((d) => ` * ${getFullName(d)}`)
.join("\n"),
);
}
- if (step.usesSanitizer && runningSteps.length > 0) {
+ if (usesSanitizer(desc) && runningStepDescs.length > 0) {
throw new Error(
"Cannot start test step with sanitizers while another test step is running.\n" +
- runningSteps.map((s) => ` * ${s.getFullName()}`).join("\n"),
+ runningStepDescs.map((d) => ` * ${getFullName(d)}`).join("\n"),
);
}
- function getPotentialConflictingRunningSteps() {
- /** @type {TestStep[]} */
+ function getRunningStepDescs() {
const results = [];
-
- let childStep = step;
- for (const ancestor of step.ancestors()) {
- for (const siblingStep of ancestor.children) {
- if (siblingStep === childStep) {
+ 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;
}
- if (!siblingStep.finalized) {
- ArrayPrototypePush(results, siblingStep);
+ const siblingState = MapPrototypeGet(testStates, siblingDesc.id);
+ if (!siblingState.finalized) {
+ ArrayPrototypePush(results, siblingDesc);
}
}
- childStep = ancestor;
+ childDesc = childDesc.parent;
}
return results;
}
}
+ };
+ }
- function postValidation() {
- // check for any running steps
- if (step.hasRunningChildren) {
- throw new Error(
- "There were still test steps running after the current scope finished execution. " +
- "Ensure all steps are awaited (ex. `await t.step(...)`).",
- );
- }
+ function testStepPostValidation(desc) {
+ // check for any running steps
+ for (const childDesc of MapPrototypeGet(testStates, desc.id).children) {
+ if (MapPrototypeGet(testStates, childDesc.id).status == "pending") {
+ throw new Error(
+ "There were still test steps running after the current scope finished execution. Ensure all steps are awaited (ex. `await t.step(...)`).",
+ );
+ }
+ }
- // check if an ancestor already completed
- for (const ancestor of step.ancestors()) {
- if (ancestor.finalized) {
- throw new Error(
- "Parent scope completed before test step finished execution. " +
- "Ensure all steps are awaited (ex. `await t.step(...)`).",
- );
- }
- }
+ // check if an ancestor already completed
+ let currentDesc = desc.parent;
+ while (currentDesc != null) {
+ if (MapPrototypeGet(testStates, currentDesc.id).finalized) {
+ throw new Error(
+ "Parent scope completed before test step finished execution. Ensure all steps are awaited (ex. `await t.step(...)`).",
+ );
}
- };
+ currentDesc = currentDesc.parent;
+ }
}
function pledgePermissions(permissions) {
@@ -541,6 +536,54 @@
* @typedef {{
* id: number,
* name: string,
+ * fn: TestFunction
+ * origin: string,
+ * location: TestLocation,
+ * filteredOut: boolean,
+ * ignore: boolean,
+ * only: boolean.
+ * sanitizeOps: boolean,
+ * sanitizeResources: boolean,
+ * sanitizeExit: boolean,
+ * permissions: PermissionOptions,
+ * }} TestDescription
+ *
+ * @typedef {{
+ * id: number,
+ * name: string,
+ * fn: TestFunction
+ * origin: string,
+ * location: TestLocation,
+ * ignore: boolean,
+ * level: number,
+ * parent: TestDescription | TestStepDescription,
+ * rootId: number,
+ * rootName: String,
+ * sanitizeOps: boolean,
+ * sanitizeResources: boolean,
+ * sanitizeExit: boolean,
+ * }} TestStepDescription
+ *
+ * @typedef {{
+ * context: TestContext,
+ * children: TestStepDescription[],
+ * finalized: boolean,
+ * }} TestState
+ *
+ * @typedef {{
+ * context: TestContext,
+ * children: TestStepDescription[],
+ * finalized: boolean,
+ * status: "pending" | "ok" | ""failed" | ignored",
+ * error: unknown,
+ * elapsed: number | null,
+ * reportedWait: boolean,
+ * reportedResult: boolean,
+ * }} TestStepState
+ *
+ * @typedef {{
+ * id: number,
+ * name: string,
* fn: BenchFunction
* origin: string,
* filteredOut: boolean,
@@ -551,7 +594,10 @@
* }} BenchDescription
*/
- const tests = [];
+ /** @type {TestDescription[]} */
+ const testDescs = [];
+ /** @type {Map<number, TestState | TestStepState>} */
+ const testStates = new Map();
/** @type {BenchDescription[]} */
const benchDescs = [];
let isTestOrBenchSubcommand = false;
@@ -566,7 +612,7 @@
return;
}
- let testDef;
+ let testDesc;
const defaults = {
ignore: false,
only: false,
@@ -581,7 +627,7 @@
throw new TypeError("The test name can't be empty");
}
if (typeof optionsOrFn === "function") {
- testDef = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults };
+ testDesc = { fn: optionsOrFn, name: nameOrFnOrOptions, ...defaults };
} else {
if (!maybeFn || typeof maybeFn !== "function") {
throw new TypeError("Missing test function");
@@ -596,7 +642,7 @@
"Unexpected 'name' field in options, test name is already provided as the first argument.",
);
}
- testDef = {
+ testDesc = {
...defaults,
...optionsOrFn,
fn: maybeFn,
@@ -613,7 +659,7 @@
if (maybeFn != undefined) {
throw new TypeError("Unexpected third argument to Deno.test()");
}
- testDef = {
+ testDesc = {
...defaults,
fn: nameOrFnOrOptions,
name: nameOrFnOrOptions.name,
@@ -643,29 +689,36 @@
if (!name) {
throw new TypeError("The test name can't be empty");
}
- testDef = { ...defaults, ...nameOrFnOrOptions, fn, name };
+ testDesc = { ...defaults, ...nameOrFnOrOptions, fn, name };
}
- testDef.fn = wrapTestFnWithSanitizers(testDef.fn, testDef);
-
- if (testDef.permissions) {
- testDef.fn = withPermissions(
- testDef.fn,
- testDef.permissions,
+ // Delete this prop in case the user passed it. It's used to detect steps.
+ delete testDesc.parent;
+ testDesc.fn = wrapTestFnWithSanitizers(testDesc.fn, testDesc);
+ if (testDesc.permissions) {
+ testDesc.fn = withPermissions(
+ testDesc.fn,
+ testDesc.permissions,
);
}
-
+ testDesc.origin = getTestOrigin();
const jsError = Deno.core.destructureError(new Error());
- // Note: There might pop up a case where one of the filename, line number or
- // column number from the caller isn't defined. We assume never for now.
- // Make `TestDescription::location` optional if such a case is found.
- testDef.location = {
+ testDesc.location = {
fileName: jsError.frames[1].fileName,
lineNumber: jsError.frames[1].lineNumber,
columnNumber: jsError.frames[1].columnNumber,
};
- ArrayPrototypePush(tests, testDef);
+ const { id, filteredOut } = core.opSync("op_register_test", testDesc);
+ testDesc.id = id;
+ testDesc.filteredOut = filteredOut;
+
+ ArrayPrototypePush(testDescs, testDesc);
+ MapPrototypeSet(testStates, testDesc.id, {
+ context: createTestContext(testDesc),
+ children: [],
+ finalized: false,
+ });
}
// Main bench function provided by Deno.
@@ -769,58 +822,14 @@
ArrayPrototypePush(benchDescs, benchDesc);
}
- /**
- * @param {string | { include?: string[], exclude?: string[] }} filter
- * @returns {(def: { name: string }) => boolean}
- */
- function createTestFilter(filter) {
- if (!filter) {
- return () => true;
- }
-
- const regex =
- typeof filter === "string" && StringPrototypeStartsWith(filter, "/") &&
- StringPrototypeEndsWith(filter, "/")
- ? new RegExp(StringPrototypeSlice(filter, 1, filter.length - 1))
- : undefined;
-
- const filterIsObject = filter != null && typeof filter === "object";
-
- return (def) => {
- if (regex) {
- return RegExpPrototypeTest(regex, def.name);
- }
- if (filterIsObject) {
- if (filter.include && !filter.include.includes(def.name)) {
- return false;
- } else if (filter.exclude && filter.exclude.includes(def.name)) {
- return false;
- } else {
- return true;
- }
- }
- return StringPrototypeIncludes(def.name, filter);
- };
- }
-
- async function runTest(test, description) {
- if (test.ignore) {
+ async function runTest(desc) {
+ if (desc.ignore) {
return "ignored";
}
- const step = new TestStep({
- name: test.name,
- parent: undefined,
- parentContext: undefined,
- rootTestDescription: description,
- sanitizeOps: test.sanitizeOps,
- sanitizeResources: test.sanitizeResources,
- sanitizeExit: test.sanitizeExit,
- });
-
try {
- await test.fn(step);
- const failCount = step.failedChildStepsCount();
+ await desc.fn(desc);
+ const failCount = failedChildStepsCount(desc);
return failCount === 0 ? "ok" : {
"failed": core.destructureError(
new Error(
@@ -833,10 +842,11 @@
"failed": core.destructureError(error),
};
} finally {
- step.finalized = true;
+ const state = MapPrototypeGet(testStates, desc.id);
+ state.finalized = true;
// ensure the children report their result
- for (const child of step.children) {
- child.reportResult();
+ for (const childDesc of state.children) {
+ stepReportResult(childDesc);
}
}
}
@@ -961,7 +971,7 @@
n++;
avg += iterationTime;
- all.push(iterationTime);
+ ArrayPrototypePush(all, iterationTime);
if (iterationTime < min) min = iterationTime;
if (iterationTime > max) max = iterationTime;
budget -= iterationTime * lowPrecisionThresholdInNs;
@@ -1018,36 +1028,6 @@
return origin;
}
- function reportTestPlan(plan) {
- core.opSync("op_dispatch_test_event", {
- plan,
- });
- }
-
- function reportTestWait(test) {
- core.opSync("op_dispatch_test_event", {
- wait: test,
- });
- }
-
- function reportTestResult(test, result, elapsed) {
- core.opSync("op_dispatch_test_event", {
- result: [test, result, elapsed],
- });
- }
-
- function reportTestStepWait(testDescription) {
- core.opSync("op_dispatch_test_event", {
- stepWait: testDescription,
- });
- }
-
- function reportTestStepResult(testDescription, result, elapsed) {
- core.opSync("op_dispatch_test_event", {
- stepResult: [testDescription, result, elapsed],
- });
- }
-
function benchNow() {
return core.opSync("op_bench_now");
}
@@ -1060,24 +1040,24 @@
}
async function runTests({
- filter = null,
shuffle = null,
} = {}) {
core.setMacrotaskCallback(handleOpSanitizerDelayMacrotask);
const origin = getTestOrigin();
-
- const only = ArrayPrototypeFilter(tests, (test) => test.only);
+ const only = ArrayPrototypeFilter(testDescs, (test) => test.only);
const filtered = ArrayPrototypeFilter(
- only.length > 0 ? only : tests,
- createTestFilter(filter),
+ only.length > 0 ? only : testDescs,
+ (desc) => !desc.filteredOut,
);
- reportTestPlan({
- origin,
- total: filtered.length,
- filteredOut: tests.length - filtered.length,
- usedOnly: only.length > 0,
+ core.opSync("op_dispatch_test_event", {
+ plan: {
+ origin,
+ total: filtered.length,
+ filteredOut: testDescs.length - filtered.length,
+ usedOnly: only.length > 0,
+ },
});
if (shuffle !== null) {
@@ -1098,20 +1078,14 @@
}
}
- for (const test of filtered) {
- const description = {
- origin,
- name: test.name,
- location: test.location,
- };
+ for (const desc of filtered) {
+ core.opSync("op_dispatch_test_event", { wait: desc.id });
const earlier = DateNow();
-
- reportTestWait(description);
-
- const result = await runTest(test, description);
+ const result = await runTest(desc);
const elapsed = DateNow() - earlier;
-
- reportTestResult(description, result, elapsed);
+ core.opSync("op_dispatch_test_event", {
+ result: [desc.id, result, elapsed],
+ });
}
}
@@ -1166,320 +1140,225 @@
globalThis.console = originalConsole;
}
- /**
- * @typedef {{
- * fn: (t: TestContext) => void | Promise<void>,
- * name: string,
- * ignore?: boolean,
- * sanitizeOps?: boolean,
- * sanitizeResources?: boolean,
- * sanitizeExit?: boolean,
- * }} TestStepDefinition
- *
- * @typedef {{
- * name: string,
- * parent: TestStep | undefined,
- * parentContext: TestContext | undefined,
- * rootTestDescription: { origin: string; name: string };
- * sanitizeOps: boolean,
- * sanitizeResources: boolean,
- * sanitizeExit: boolean,
- * }} TestStepParams
- */
-
- class TestStep {
- /** @type {TestStepParams} */
- #params;
- reportedWait = false;
- #reportedResult = false;
- finalized = false;
- elapsed = 0;
- /** @type "ok" | "ignored" | "pending" | "failed" */
- status = "pending";
- error = undefined;
- /** @type {TestStep[]} */
- children = [];
-
- /** @param params {TestStepParams} */
- constructor(params) {
- this.#params = params;
- }
-
- get name() {
- return this.#params.name;
- }
-
- get parent() {
- return this.#params.parent;
- }
-
- get parentContext() {
- return this.#params.parentContext;
- }
-
- get rootTestDescription() {
- return this.#params.rootTestDescription;
- }
-
- get sanitizerOptions() {
- return {
- sanitizeResources: this.#params.sanitizeResources,
- sanitizeOps: this.#params.sanitizeOps,
- sanitizeExit: this.#params.sanitizeExit,
- };
- }
-
- get usesSanitizer() {
- return this.#params.sanitizeResources ||
- this.#params.sanitizeOps ||
- this.#params.sanitizeExit;
- }
-
- /** If a test validation error already occurred then don't bother checking
- * the sanitizers as that will create extra noise.
- */
- get shouldSkipSanitizers() {
- return this.hasRunningChildren || this.parent?.finalized;
- }
-
- get hasRunningChildren() {
- return ArrayPrototypeSome(
- this.children,
- /** @param step {TestStep} */
- (step) => step.status === "pending",
- );
- }
-
- failedChildStepsCount() {
- return ArrayPrototypeFilter(
- this.children,
- /** @param step {TestStep} */
- (step) => step.status === "failed",
- ).length;
+ function getFullName(desc) {
+ if ("parent" in desc) {
+ return `${desc.parent.name} > ${desc.name}`;
}
+ return desc.name;
+ }
- canStreamReporting() {
- // there should only ever be one sub step running when running with
- // sanitizers, so we can use this to tell if we can stream reporting
- return this.selfAndAllAncestorsUseSanitizer() &&
- this.children.every((c) => c.usesSanitizer || c.finalized);
- }
+ function usesSanitizer(desc) {
+ return desc.sanitizeResources || desc.sanitizeOps || desc.sanitizeExit;
+ }
- selfAndAllAncestorsUseSanitizer() {
- if (!this.usesSanitizer) {
+ function canStreamReporting(desc) {
+ let currentDesc = desc;
+ while (currentDesc != null) {
+ if (!usesSanitizer(currentDesc)) {
return false;
}
-
- for (const ancestor of this.ancestors()) {
- if (!ancestor.usesSanitizer) {
- return false;
- }
- }
-
- return true;
- }
-
- *ancestors() {
- let ancestor = this.parent;
- while (ancestor) {
- yield ancestor;
- ancestor = ancestor.parent;
- }
+ currentDesc = currentDesc.parent;
}
-
- getFullName() {
- if (this.parent) {
- return `${this.parent.getFullName()} > ${this.name}`;
- } else {
- return this.name;
+ for (const childDesc of MapPrototypeGet(testStates, desc.id).children) {
+ const state = MapPrototypeGet(testStates, childDesc.id);
+ if (!usesSanitizer(childDesc) && !state.finalized) {
+ return false;
}
}
+ return true;
+ }
- reportWait() {
- if (this.reportedWait || !this.parent) {
- return;
- }
-
- reportTestStepWait(this.#getTestStepDescription());
-
- this.reportedWait = true;
+ function stepReportWait(desc) {
+ const state = MapPrototypeGet(testStates, desc.id);
+ if (state.reportedWait) {
+ return;
}
+ core.opSync("op_dispatch_test_event", { stepWait: desc.id });
+ state.reportedWait = true;
+ }
- reportResult() {
- if (this.#reportedResult || !this.parent) {
- return;
- }
-
- this.reportWait();
-
- for (const child of this.children) {
- child.reportResult();
- }
-
- reportTestStepResult(
- this.#getTestStepDescription(),
- this.#getStepResult(),
- this.elapsed,
- );
-
- this.#reportedResult = true;
+ function stepReportResult(desc) {
+ const state = MapPrototypeGet(testStates, desc.id);
+ if (state.reportedResult) {
+ return;
}
-
- #getStepResult() {
- switch (this.status) {
- case "ok":
- return "ok";
- case "ignored":
- return "ignored";
- case "pending":
- return {
- "pending": this.error && core.destructureError(this.error),
- };
- case "failed":
- return {
- "failed": this.error && core.destructureError(this.error),
- };
- default:
- throw new Error(`Unhandled status: ${this.status}`);
- }
+ stepReportWait(desc);
+ for (const childDesc of state.children) {
+ stepReportResult(childDesc);
}
-
- #getTestStepDescription() {
- return {
- test: this.rootTestDescription,
- name: this.name,
- level: this.#getLevel(),
+ let result;
+ if (state.status == "pending" || state.status == "failed") {
+ result = {
+ [state.status]: state.error && core.destructureError(state.error),
};
+ } else {
+ result = state.status;
}
+ core.opSync("op_dispatch_test_event", {
+ stepResult: [desc.id, result, state.elapsed],
+ });
+ state.reportedResult = true;
+ }
- #getLevel() {
- let count = 0;
- for (const _ of this.ancestors()) {
- count++;
- }
- return count;
+ function failedChildStepsCount(desc) {
+ return ArrayPrototypeFilter(
+ MapPrototypeGet(testStates, desc.id).children,
+ (d) => MapPrototypeGet(testStates, d.id).status === "failed",
+ ).length;
+ }
+
+ /** If a test validation error already occurred then don't bother checking
+ * the sanitizers as that will create extra noise.
+ */
+ function shouldSkipSanitizers(desc) {
+ try {
+ testStepPostValidation(desc);
+ return false;
+ } catch {
+ return true;
}
}
- /** @param parentStep {TestStep} */
- function createTestContext(parentStep) {
+ /** @param desc {TestDescription | TestStepDescription} */
+ 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: parentStep.name,
+ name: desc.name,
/**
* Parent test context.
*/
- parent: parentStep.parentContext ?? undefined,
+ parent,
/**
* File Uri of the test code.
*/
- origin: parentStep.rootTestDescription.origin,
+ origin: desc.origin,
/**
* @param nameOrTestDefinition {string | TestStepDefinition}
* @param fn {(t: TestContext) => void | Promise<void>}
*/
async step(nameOrTestDefinition, fn) {
- if (parentStep.finalized) {
+ if (MapPrototypeGet(testStates, desc.id).finalized) {
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.",
);
}
- const definition = getDefinition();
- const subStep = new TestStep({
- name: definition.name,
- parent: parentStep,
- parentContext: this,
- rootTestDescription: parentStep.rootTestDescription,
- sanitizeOps: getOrDefault(
- definition.sanitizeOps,
- parentStep.sanitizerOptions.sanitizeOps,
- ),
- sanitizeResources: getOrDefault(
- definition.sanitizeResources,
- parentStep.sanitizerOptions.sanitizeResources,
- ),
- sanitizeExit: getOrDefault(
- definition.sanitizeExit,
- parentStep.sanitizerOptions.sanitizeExit,
- ),
- });
-
- ArrayPrototypePush(parentStep.children, subStep);
+ let stepDesc;
+ if (typeof nameOrTestDefinition === "string") {
+ if (!(ObjectPrototypeIsPrototypeOf(FunctionPrototype, fn))) {
+ throw new TypeError("Expected function for second argument.");
+ }
+ stepDesc = {
+ name: nameOrTestDefinition,
+ fn,
+ };
+ } else if (typeof nameOrTestDefinition === "object") {
+ stepDesc = nameOrTestDefinition;
+ } 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.origin = getTestOrigin();
+ const jsError = Deno.core.destructureError(new Error());
+ stepDesc.location = {
+ fileName: jsError.frames[1].fileName,
+ lineNumber: jsError.frames[1].lineNumber,
+ columnNumber: jsError.frames[1].columnNumber,
+ };
+ stepDesc.level = level + 1;
+ stepDesc.parent = desc;
+ stepDesc.rootId = rootId;
+ stepDesc.rootName = rootName;
+ const { id } = core.opSync("op_register_test_step", stepDesc);
+ stepDesc.id = id;
+ const state = {
+ context: createTestContext(stepDesc),
+ children: [],
+ finalized: false,
+ status: "pending",
+ error: null,
+ elapsed: null,
+ reportedWait: false,
+ reportedResult: false,
+ };
+ MapPrototypeSet(testStates, stepDesc.id, state);
+ ArrayPrototypePush(
+ MapPrototypeGet(testStates, stepDesc.parent.id).children,
+ stepDesc,
+ );
try {
- if (definition.ignore) {
- subStep.status = "ignored";
- subStep.finalized = true;
- if (subStep.canStreamReporting()) {
- subStep.reportResult();
+ if (stepDesc.ignore) {
+ state.status = "ignored";
+ state.finalized = true;
+ if (canStreamReporting(stepDesc)) {
+ stepReportResult(stepDesc);
}
return false;
}
- const testFn = wrapTestFnWithSanitizers(
- definition.fn,
- subStep.sanitizerOptions,
- );
+ const testFn = wrapTestFnWithSanitizers(stepDesc.fn, stepDesc);
const start = DateNow();
try {
- await testFn(subStep);
+ await testFn(stepDesc);
- if (subStep.failedChildStepsCount() > 0) {
- subStep.status = "failed";
+ if (failedChildStepsCount(stepDesc) > 0) {
+ state.status = "failed";
} else {
- subStep.status = "ok";
+ state.status = "ok";
}
} catch (error) {
- subStep.error = error;
- subStep.status = "failed";
+ state.error = error;
+ state.status = "failed";
}
- subStep.elapsed = DateNow() - start;
+ state.elapsed = DateNow() - start;
- if (subStep.parent?.finalized) {
+ if (MapPrototypeGet(testStates, stepDesc.parent.id).finalized) {
// always point this test out as one that was still running
// if the parent step finalized
- subStep.status = "pending";
+ state.status = "pending";
}
- subStep.finalized = true;
+ state.finalized = true;
- if (subStep.reportedWait && subStep.canStreamReporting()) {
- subStep.reportResult();
+ if (state.reportedWait && canStreamReporting(stepDesc)) {
+ stepReportResult(stepDesc);
}
- return subStep.status === "ok";
+ return state.status === "ok";
} finally {
- if (parentStep.canStreamReporting()) {
+ if (canStreamReporting(stepDesc.parent)) {
+ const parentState = MapPrototypeGet(testStates, stepDesc.parent.id);
// flush any buffered steps
- for (const parentChild of parentStep.children) {
- parentChild.reportResult();
- }
- }
- }
-
- /** @returns {TestStepDefinition} */
- function getDefinition() {
- if (typeof nameOrTestDefinition === "string") {
- if (!(ObjectPrototypeIsPrototypeOf(FunctionPrototype, fn))) {
- throw new TypeError("Expected function for second argument.");
+ for (const childDesc of parentState.children) {
+ stepReportResult(childDesc);
}
- return {
- name: nameOrTestDefinition,
- fn,
- };
- } else if (typeof nameOrTestDefinition === "object") {
- return nameOrTestDefinition;
- } else {
- throw new TypeError(
- "Expected a test definition or name and function.",
- );
}
}
},
@@ -1511,16 +1390,6 @@
return testFn;
}
- /**
- * @template T
- * @param value {T | undefined}
- * @param defaultValue {T}
- * @returns T
- */
- function getOrDefault(value, defaultValue) {
- return value == null ? defaultValue : value;
- }
-
window.__bootstrap.internals = {
...window.__bootstrap.internals ?? {},
enableTestAndBench,