summaryrefslogtreecommitdiff
path: root/cli/js/40_testing.js
diff options
context:
space:
mode:
Diffstat (limited to 'cli/js/40_testing.js')
-rw-r--r--cli/js/40_testing.js335
1 files changed, 87 insertions, 248 deletions
diff --git a/cli/js/40_testing.js b/cli/js/40_testing.js
index bcb5990a9..74fbb8da3 100644
--- a/cli/js/40_testing.js
+++ b/cli/js/40_testing.js
@@ -142,7 +142,8 @@ function assertOps(fn) {
const pre = core.metrics();
const preTraces = new Map(core.opCallTraces);
try {
- await fn(desc);
+ const innerResult = await fn(desc);
+ if (innerResult) return innerResult;
} finally {
// Defer until next event loop turn - that way timeouts and intervals
// cleared can actually be removed from resource table, otherwise
@@ -150,9 +151,6 @@ function assertOps(fn) {
await opSanitizerDelay();
await opSanitizerDelay();
}
-
- if (shouldSkipSanitizers(desc)) return;
-
const post = core.metrics();
const postTraces = new Map(core.opCallTraces);
@@ -161,7 +159,7 @@ function assertOps(fn) {
const dispatchedDiff = post.opsDispatchedAsync - pre.opsDispatchedAsync;
const completedDiff = post.opsCompletedAsync - pre.opsCompletedAsync;
- if (dispatchedDiff === completedDiff) return;
+ if (dispatchedDiff === completedDiff) return null;
const details = [];
for (const key in post.ops) {
@@ -215,19 +213,7 @@ function assertOps(fn) {
);
}
}
-
- let msg = `Test case is leaking async ops.
-
- - ${ArrayPrototypeJoin(details, "\n - ")}`;
-
- if (!core.isOpCallTracingEnabled()) {
- msg +=
- `\n\nTo get more details where ops were leaked, run again with --trace-ops flag.`;
- } else {
- msg += "\n";
- }
-
- throw assert(false, msg);
+ return { failed: { leakedOps: [details, core.isOpCallTracingEnabled()] } };
};
}
@@ -372,12 +358,8 @@ function assertResources(fn) {
/** @param desc {TestDescription | TestStepDescription} */
return async function resourceSanitizer(desc) {
const pre = core.resources();
- await fn(desc);
-
- if (shouldSkipSanitizers(desc)) {
- return;
- }
-
+ const innerResult = await fn(desc);
+ if (innerResult) return innerResult;
const post = core.resources();
const allResources = new Set([
@@ -404,14 +386,10 @@ function assertResources(fn) {
ArrayPrototypePush(details, detail);
}
}
-
- const message = `Test case is leaking ${details.length} resource${
- details.length === 1 ? "" : "s"
- }:
-
- - ${details.join("\n - ")}
-`;
- assert(details.length === 0, message);
+ if (details.length == 0) {
+ return null;
+ }
+ return { failed: { leakedResources: details } };
};
}
@@ -429,9 +407,8 @@ function assertExit(fn, isTest) {
});
try {
- await fn(...new SafeArrayIterator(params));
- } catch (err) {
- throw err;
+ const innerResult = await fn(...new SafeArrayIterator(params));
+ if (innerResult) return innerResult;
} finally {
setExitHandler(null);
}
@@ -441,79 +418,52 @@ function assertExit(fn, isTest) {
function assertTestStepScopes(fn) {
/** @param desc {TestDescription | TestStepDescription} */
return async function testStepSanitizer(desc) {
- preValidation();
- // only report waiting after pre-validation
- if (canStreamReporting(desc) && "parent" in desc) {
- stepReportWait(desc);
- }
- await fn(MapPrototypeGet(testStates, desc.id).context);
- testStepPostValidation(desc);
-
- function preValidation() {
- const runningStepDescs = getRunningStepDescs();
- const runningStepDescsWithSanitizers = ArrayPrototypeFilter(
- runningStepDescs,
- (d) => usesSanitizer(d),
- );
-
- if (runningStepDescsWithSanitizers.length > 0) {
- throw new Error(
- "Cannot start test step while another test step with sanitizers is running.\n" +
- runningStepDescsWithSanitizers
- .map((d) => ` * ${getFullName(d)}`)
- .join("\n"),
- );
- }
-
- if (usesSanitizer(desc) && runningStepDescs.length > 0) {
- throw new Error(
- "Cannot start test step with sanitizers while another test step is running.\n" +
- runningStepDescs.map((d) => ` * ${getFullName(d)}`).join("\n"),
- );
- }
-
- 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.finalized) {
- ArrayPrototypePush(results, siblingDesc);
- }
+ 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;
+ childDesc = childDesc.parent;
}
+ return results;
}
- };
-}
+ const runningStepDescs = getRunningStepDescs();
+ const runningStepDescsWithSanitizers = ArrayPrototypeFilter(
+ runningStepDescs,
+ (d) => usesSanitizer(d),
+ );
-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(...)`).",
- );
+ if (runningStepDescsWithSanitizers.length > 0) {
+ return {
+ failed: {
+ overlapsWithSanitizers: runningStepDescsWithSanitizers.map(
+ getFullName,
+ ),
+ },
+ };
}
- }
- // 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(...)`).",
- );
+ if (usesSanitizer(desc) && runningStepDescs.length > 0) {
+ return {
+ failed: { hasSanitizersAndOverlaps: runningStepDescs.map(getFullName) },
+ };
}
- currentDesc = currentDesc.parent;
- }
+ await fn(MapPrototypeGet(testStates, desc.id).context);
+ for (const childDesc of MapPrototypeGet(testStates, desc.id).children) {
+ if (!MapPrototypeGet(testStates, childDesc.id).completed) {
+ return { failed: "incompleteSteps" };
+ }
+ }
+ };
}
function pledgePermissions(permissions) {
@@ -573,18 +523,14 @@ function withPermissions(fn, permissions) {
* @typedef {{
* context: TestContext,
* children: TestStepDescription[],
- * finalized: boolean,
+ * completed: boolean,
* }} TestState
*
* @typedef {{
* context: TestContext,
* children: TestStepDescription[],
- * finalized: boolean,
- * status: "pending" | "ok" | ""failed" | ignored",
- * error: unknown,
- * elapsed: number | null,
- * reportedWait: boolean,
- * reportedResult: boolean,
+ * completed: boolean,
+ * failed: boolean,
* }} TestStepState
*
* @typedef {{
@@ -701,13 +647,6 @@ function test(
// 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 = core.destructureError(new Error());
testDesc.location = {
@@ -724,7 +663,7 @@ function test(
MapPrototypeSet(testStates, testDesc.id, {
context: createTestContext(testDesc),
children: [],
- finalized: false,
+ completed: false,
});
}
@@ -832,28 +771,20 @@ 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 {
- await desc.fn(desc);
- const failCount = failedChildStepsCount(desc);
- return failCount === 0 ? "ok" : {
- "failed": core.destructureError(
- new Error(
- `${failCount} test step${failCount === 1 ? "" : "s"} failed.`,
- ),
- ),
- };
+ const result = await testFn(desc);
+ if (result) return result;
+ const failedSteps = failedChildStepsCount(desc);
+ return failedSteps === 0 ? "ok" : { failed: { failedSteps } };
} catch (error) {
- return {
- "failed": core.destructureError(error),
- };
- } finally {
- const state = MapPrototypeGet(testStates, desc.id);
- state.finalized = true;
- // ensure the children report their result
- for (const childDesc of state.children) {
- stepReportResult(childDesc);
- }
+ return { failed: { jsError: core.destructureError(error) } };
}
}
@@ -1094,6 +1025,11 @@ async function runTests({
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],
});
@@ -1153,7 +1089,7 @@ async function runBenchmarks() {
function getFullName(desc) {
if ("parent" in desc) {
- return `${desc.parent.name} > ${desc.name}`;
+ return `${getFullName(desc.parent)} ... ${desc.name}`;
}
return desc.name;
}
@@ -1162,74 +1098,23 @@ function usesSanitizer(desc) {
return desc.sanitizeResources || desc.sanitizeOps || desc.sanitizeExit;
}
-function canStreamReporting(desc) {
- let currentDesc = desc;
- while (currentDesc != null) {
- if (!usesSanitizer(currentDesc)) {
- return false;
- }
- currentDesc = currentDesc.parent;
- }
- for (const childDesc of MapPrototypeGet(testStates, desc.id).children) {
- const state = MapPrototypeGet(testStates, childDesc.id);
- if (!usesSanitizer(childDesc) && !state.finalized) {
- return false;
- }
- }
- return true;
-}
-
-function stepReportWait(desc) {
- const state = MapPrototypeGet(testStates, desc.id);
- if (state.reportedWait) {
- return;
- }
- ops.op_dispatch_test_event({ stepWait: desc.id });
- state.reportedWait = true;
-}
-
-function stepReportResult(desc) {
+function stepReportResult(desc, result, elapsed) {
const state = MapPrototypeGet(testStates, desc.id);
- if (state.reportedResult) {
- return;
- }
- stepReportWait(desc);
for (const childDesc of state.children) {
- stepReportResult(childDesc);
- }
- let result;
- if (state.status == "pending" || state.status == "failed") {
- result = {
- [state.status]: state.error && core.destructureError(state.error),
- };
- } else {
- result = state.status;
+ stepReportResult(childDesc, { failed: "incomplete" }, 0);
}
ops.op_dispatch_test_event({
- stepResult: [desc.id, result, state.elapsed],
+ stepResult: [desc.id, result, elapsed],
});
- state.reportedResult = true;
}
function failedChildStepsCount(desc) {
return ArrayPrototypeFilter(
MapPrototypeGet(testStates, desc.id).children,
- (d) => MapPrototypeGet(testStates, d.id).status === "failed",
+ (d) => MapPrototypeGet(testStates, d.id).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 desc {TestDescription | TestStepDescription} */
function createTestContext(desc) {
let parent;
@@ -1266,7 +1151,7 @@ function createTestContext(desc) {
* @param maybeFn {((t: TestContext) => void | Promise<void>) | undefined}
*/
async step(nameOrFnOrOptions, maybeFn) {
- if (MapPrototypeGet(testStates, desc.id).finalized) {
+ 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.",
@@ -1322,12 +1207,8 @@ function createTestContext(desc) {
const state = {
context: createTestContext(stepDesc),
children: [],
- finalized: false,
- status: "pending",
- error: null,
- elapsed: null,
- reportedWait: false,
- reportedResult: false,
+ failed: false,
+ completed: false,
};
MapPrototypeSet(testStates, stepDesc.id, state);
ArrayPrototypePush(
@@ -1335,56 +1216,14 @@ function createTestContext(desc) {
stepDesc,
);
- try {
- if (stepDesc.ignore) {
- state.status = "ignored";
- state.finalized = true;
- if (canStreamReporting(stepDesc)) {
- stepReportResult(stepDesc);
- }
- return false;
- }
-
- const testFn = wrapTestFnWithSanitizers(stepDesc.fn, stepDesc);
- const start = DateNow();
-
- try {
- await testFn(stepDesc);
-
- if (failedChildStepsCount(stepDesc) > 0) {
- state.status = "failed";
- } else {
- state.status = "ok";
- }
- } catch (error) {
- state.error = error;
- state.status = "failed";
- }
-
- state.elapsed = DateNow() - start;
-
- if (MapPrototypeGet(testStates, stepDesc.parent.id).finalized) {
- // always point this test out as one that was still running
- // if the parent step finalized
- state.status = "pending";
- }
-
- state.finalized = true;
-
- if (state.reportedWait && canStreamReporting(stepDesc)) {
- stepReportResult(stepDesc);
- }
-
- return state.status === "ok";
- } finally {
- if (canStreamReporting(stepDesc.parent)) {
- const parentState = MapPrototypeGet(testStates, stepDesc.parent.id);
- // flush any buffered steps
- for (const childDesc of parentState.children) {
- stepReportResult(childDesc);
- }
- }
- }
+ ops.op_dispatch_test_event({ stepWait: stepDesc.id });
+ const earlier = DateNow();
+ const result = await runTest(stepDesc);
+ const elapsed = DateNow() - earlier;
+ state.failed = !!result.failed;
+ state.completed = true;
+ stepReportResult(stepDesc, result, elapsed);
+ return result == "ok";
},
};
}