2021-01-11 12:13:41 -05:00
|
|
|
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
2021-02-04 17:18:32 -05:00
|
|
|
"use strict";
|
2020-07-19 13:49:44 -04:00
|
|
|
|
|
|
|
((window) => {
|
2020-09-17 12:09:50 -04:00
|
|
|
const core = window.Deno.core;
|
2021-04-25 17:38:59 -04:00
|
|
|
const { parsePermissions } = window.__bootstrap.worker;
|
2021-05-18 11:24:01 -04:00
|
|
|
const { setExitHandler } = window.__bootstrap.os;
|
2020-07-19 13:49:44 -04:00
|
|
|
const { Console, inspectArgs } = window.__bootstrap.console;
|
2021-10-10 11:20:30 -04:00
|
|
|
const { metrics } = core;
|
2020-07-19 13:49:44 -04:00
|
|
|
const { assert } = window.__bootstrap.util;
|
2021-07-03 18:17:52 -04:00
|
|
|
const {
|
|
|
|
ArrayPrototypeFilter,
|
|
|
|
ArrayPrototypePush,
|
2021-10-11 09:45:02 -04:00
|
|
|
ArrayPrototypeSome,
|
2021-07-03 18:17:52 -04:00
|
|
|
DateNow,
|
2021-10-11 09:45:02 -04:00
|
|
|
Error,
|
|
|
|
Function,
|
2021-07-03 18:17:52 -04:00
|
|
|
JSONStringify,
|
|
|
|
Promise,
|
|
|
|
TypeError,
|
|
|
|
StringPrototypeStartsWith,
|
|
|
|
StringPrototypeEndsWith,
|
|
|
|
StringPrototypeIncludes,
|
|
|
|
StringPrototypeSlice,
|
|
|
|
RegExp,
|
|
|
|
RegExpPrototypeTest,
|
2021-10-11 09:45:02 -04:00
|
|
|
SymbolToStringTag,
|
2021-07-03 18:17:52 -04:00
|
|
|
} = window.__bootstrap.primordials;
|
2021-10-11 09:45:02 -04:00
|
|
|
let testStepsEnabled = false;
|
2020-07-19 13:49:44 -04:00
|
|
|
|
|
|
|
// Wrap test function in additional assertion that makes sure
|
|
|
|
// the test case does not leak async "ops" - ie. number of async
|
|
|
|
// completed ops after the test is the same as number of dispatched
|
|
|
|
// ops. Note that "unref" ops are ignored since in nature that are
|
|
|
|
// optional.
|
|
|
|
function assertOps(fn) {
|
2021-10-11 09:45:02 -04:00
|
|
|
return async function asyncOpSanitizer(...params) {
|
2020-07-19 13:49:44 -04:00
|
|
|
const pre = metrics();
|
2021-02-21 11:21:25 -05:00
|
|
|
try {
|
2021-10-11 09:45:02 -04:00
|
|
|
await fn(...params);
|
2021-02-21 11:21:25 -05:00
|
|
|
} finally {
|
|
|
|
// Defer until next event loop turn - that way timeouts and intervals
|
|
|
|
// cleared can actually be removed from resource table, otherwise
|
|
|
|
// false positives may occur (https://github.com/denoland/deno/issues/4591)
|
2021-04-28 14:17:04 -04:00
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
2021-02-21 11:21:25 -05:00
|
|
|
}
|
2021-04-28 14:17:04 -04:00
|
|
|
|
2020-07-19 13:49:44 -04:00
|
|
|
const post = metrics();
|
|
|
|
// We're checking diff because one might spawn HTTP server in the background
|
|
|
|
// that will be a pending async op before test starts.
|
|
|
|
const dispatchedDiff = post.opsDispatchedAsync - pre.opsDispatchedAsync;
|
|
|
|
const completedDiff = post.opsCompletedAsync - pre.opsCompletedAsync;
|
|
|
|
assert(
|
|
|
|
dispatchedDiff === completedDiff,
|
|
|
|
`Test case is leaking async ops.
|
|
|
|
Before:
|
|
|
|
- dispatched: ${pre.opsDispatchedAsync}
|
|
|
|
- completed: ${pre.opsCompletedAsync}
|
|
|
|
After:
|
|
|
|
- dispatched: ${post.opsDispatchedAsync}
|
|
|
|
- completed: ${post.opsCompletedAsync}
|
|
|
|
|
|
|
|
Make sure to await all promises returned from Deno APIs before
|
|
|
|
finishing test case.`,
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
) {
|
2021-10-11 09:45:02 -04:00
|
|
|
return async function resourceSanitizer(...params) {
|
2020-09-17 12:09:50 -04:00
|
|
|
const pre = core.resources();
|
2021-10-11 09:45:02 -04:00
|
|
|
await fn(...params);
|
2020-09-17 12:09:50 -04:00
|
|
|
const post = core.resources();
|
2020-07-19 13:49:44 -04:00
|
|
|
|
2021-07-03 18:17:52 -04:00
|
|
|
const preStr = JSONStringify(pre, null, 2);
|
|
|
|
const postStr = JSONStringify(post, null, 2);
|
2020-07-19 13:49:44 -04:00
|
|
|
const msg = `Test case is leaking resources.
|
|
|
|
Before: ${preStr}
|
|
|
|
After: ${postStr}
|
|
|
|
|
|
|
|
Make sure to close all open resource handles returned from Deno APIs before
|
|
|
|
finishing test case.`;
|
|
|
|
assert(preStr === postStr, msg);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-02-24 07:55:50 -05:00
|
|
|
// Wrap test function in additional assertion that makes sure
|
|
|
|
// that the test case does not accidentally exit prematurely.
|
|
|
|
function assertExit(fn) {
|
2021-10-11 09:45:02 -04:00
|
|
|
return async function exitSanitizer(...params) {
|
2021-02-24 07:55:50 -05:00
|
|
|
setExitHandler((exitCode) => {
|
|
|
|
assert(
|
|
|
|
false,
|
|
|
|
`Test case attempted to exit with exit code: ${exitCode}`,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
try {
|
2021-10-11 09:45:02 -04:00
|
|
|
await fn(...params);
|
2021-02-24 07:55:50 -05:00
|
|
|
} catch (err) {
|
|
|
|
throw err;
|
|
|
|
} finally {
|
|
|
|
setExitHandler(null);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
function assertTestStepScopes(fn) {
|
|
|
|
/** @param step {TestStep} */
|
|
|
|
return async function testStepSanitizer(step) {
|
|
|
|
preValidation();
|
|
|
|
// only report waiting after pre-validation
|
|
|
|
if (step.canStreamReporting()) {
|
|
|
|
step.reportWait();
|
|
|
|
}
|
|
|
|
await fn(createTestContext(step));
|
|
|
|
postValidation();
|
|
|
|
|
|
|
|
function preValidation() {
|
|
|
|
const runningSteps = getPotentialConflictingRunningSteps();
|
|
|
|
const runningStepsWithSanitizers = ArrayPrototypeFilter(
|
|
|
|
runningSteps,
|
|
|
|
(t) => t.usesSanitizer,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (runningStepsWithSanitizers.length > 0) {
|
|
|
|
throw new Error(
|
|
|
|
"Cannot start test step while another test step with sanitizers is running.\n" +
|
|
|
|
runningStepsWithSanitizers
|
|
|
|
.map((s) => ` * ${s.getFullName()}`)
|
|
|
|
.join("\n"),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (step.usesSanitizer && runningSteps.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"),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getPotentialConflictingRunningSteps() {
|
|
|
|
/** @type {TestStep[]} */
|
|
|
|
const results = [];
|
|
|
|
|
|
|
|
let childStep = step;
|
|
|
|
for (const ancestor of step.ancestors()) {
|
|
|
|
for (const siblingStep of ancestor.children) {
|
|
|
|
if (siblingStep === childStep) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (!siblingStep.finalized) {
|
|
|
|
ArrayPrototypePush(results, siblingStep);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
childStep = ancestor;
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function postValidation() {
|
|
|
|
// check for any running steps
|
|
|
|
const hasRunningSteps = ArrayPrototypeSome(
|
|
|
|
step.children,
|
|
|
|
(r) => r.status === "pending",
|
|
|
|
);
|
|
|
|
if (hasRunningSteps) {
|
|
|
|
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(...)`).",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-07-13 18:11:02 -04:00
|
|
|
function withPermissions(fn, permissions) {
|
|
|
|
function pledgePermissions(permissions) {
|
|
|
|
return core.opSync(
|
|
|
|
"op_pledge_test_permissions",
|
|
|
|
parsePermissions(permissions),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function restorePermissions(token) {
|
|
|
|
core.opSync("op_restore_test_permissions", token);
|
|
|
|
}
|
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
return async function applyPermissions(...params) {
|
2021-07-13 18:11:02 -04:00
|
|
|
const token = pledgePermissions(permissions);
|
|
|
|
|
|
|
|
try {
|
2021-10-11 09:45:02 -04:00
|
|
|
await fn(...params);
|
2021-07-13 18:11:02 -04:00
|
|
|
} finally {
|
|
|
|
restorePermissions(token);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-04-28 14:17:04 -04:00
|
|
|
const tests = [];
|
2020-07-19 13:49:44 -04:00
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
// Main test function provided by Deno.
|
2020-07-19 13:49:44 -04:00
|
|
|
function test(
|
|
|
|
t,
|
|
|
|
fn,
|
|
|
|
) {
|
|
|
|
let testDef;
|
|
|
|
const defaults = {
|
|
|
|
ignore: false,
|
|
|
|
only: false,
|
|
|
|
sanitizeOps: true,
|
|
|
|
sanitizeResources: true,
|
2021-02-24 07:55:50 -05:00
|
|
|
sanitizeExit: true,
|
2021-04-25 17:38:59 -04:00
|
|
|
permissions: null,
|
2020-07-19 13:49:44 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
if (typeof t === "string") {
|
|
|
|
if (!fn || typeof fn != "function") {
|
|
|
|
throw new TypeError("Missing test function");
|
|
|
|
}
|
|
|
|
if (!t) {
|
|
|
|
throw new TypeError("The test name can't be empty");
|
|
|
|
}
|
|
|
|
testDef = { fn: fn, name: t, ...defaults };
|
|
|
|
} else {
|
|
|
|
if (!t.fn) {
|
|
|
|
throw new TypeError("Missing test function");
|
|
|
|
}
|
|
|
|
if (!t.name) {
|
|
|
|
throw new TypeError("The test name can't be empty");
|
|
|
|
}
|
|
|
|
testDef = { ...defaults, ...t };
|
|
|
|
}
|
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
testDef.fn = wrapTestFnWithSanitizers(testDef.fn, testDef);
|
2021-02-24 07:55:50 -05:00
|
|
|
|
2021-07-13 18:11:02 -04:00
|
|
|
if (testDef.permissions) {
|
|
|
|
testDef.fn = withPermissions(
|
|
|
|
testDef.fn,
|
|
|
|
parsePermissions(testDef.permissions),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-07-03 18:17:52 -04:00
|
|
|
ArrayPrototypePush(tests, testDef);
|
2020-07-19 13:49:44 -04:00
|
|
|
}
|
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
function formatError(error) {
|
2021-09-30 15:54:56 -04:00
|
|
|
if (error.errors) {
|
|
|
|
const message = error
|
|
|
|
.errors
|
|
|
|
.map((error) =>
|
|
|
|
inspectArgs([error]).replace(/^(?!\s*$)/gm, " ".repeat(4))
|
|
|
|
)
|
|
|
|
.join("\n");
|
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
return error.name + "\n" + message + error.stack;
|
2021-09-30 15:54:56 -04:00
|
|
|
}
|
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
return inspectArgs([error]);
|
2021-09-30 15:54:56 -04:00
|
|
|
}
|
|
|
|
|
2021-04-28 14:17:04 -04:00
|
|
|
function createTestFilter(filter) {
|
|
|
|
return (def) => {
|
|
|
|
if (filter) {
|
2021-07-03 18:17:52 -04:00
|
|
|
if (
|
|
|
|
StringPrototypeStartsWith(filter, "/") &&
|
|
|
|
StringPrototypeEndsWith(filter, "/")
|
|
|
|
) {
|
|
|
|
const regex = new RegExp(
|
|
|
|
StringPrototypeSlice(filter, 1, filter.length - 1),
|
|
|
|
);
|
|
|
|
return RegExpPrototypeTest(regex, def.name);
|
2020-07-19 13:49:44 -04:00
|
|
|
}
|
|
|
|
|
2021-07-03 18:17:52 -04:00
|
|
|
return StringPrototypeIncludes(def.name, filter);
|
2020-07-19 13:49:44 -04:00
|
|
|
}
|
|
|
|
|
2021-04-28 14:17:04 -04:00
|
|
|
return true;
|
|
|
|
};
|
2020-07-19 13:49:44 -04:00
|
|
|
}
|
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
async function runTest(test, description) {
|
|
|
|
if (test.ignore) {
|
2021-07-05 12:36:43 -04:00
|
|
|
return "ignored";
|
|
|
|
}
|
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
const step = new TestStep({
|
|
|
|
name: test.name,
|
|
|
|
parent: undefined,
|
|
|
|
rootTestDescription: description,
|
|
|
|
sanitizeOps: test.sanitizeOps,
|
|
|
|
sanitizeResources: test.sanitizeResources,
|
|
|
|
sanitizeExit: test.sanitizeExit,
|
|
|
|
});
|
|
|
|
|
2021-04-28 14:17:04 -04:00
|
|
|
try {
|
2021-10-11 09:45:02 -04:00
|
|
|
await test.fn(step);
|
|
|
|
const failCount = step.failedChildStepsCount();
|
|
|
|
return failCount === 0 ? "ok" : {
|
|
|
|
"failed": formatError(
|
|
|
|
new Error(
|
|
|
|
`${failCount} test step${failCount === 1 ? "" : "s"} failed.`,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
};
|
2021-04-28 14:17:04 -04:00
|
|
|
} catch (error) {
|
2021-10-11 09:45:02 -04:00
|
|
|
return {
|
|
|
|
"failed": formatError(error),
|
|
|
|
};
|
|
|
|
} finally {
|
|
|
|
// ensure the children report their result
|
|
|
|
for (const child of step.children) {
|
|
|
|
child.reportResult();
|
|
|
|
}
|
2021-04-28 14:17:04 -04:00
|
|
|
}
|
2020-07-19 13:49:44 -04:00
|
|
|
}
|
|
|
|
|
2021-07-14 15:05:16 -04:00
|
|
|
function getTestOrigin() {
|
|
|
|
return core.opSync("op_get_test_origin");
|
|
|
|
}
|
|
|
|
|
2021-09-05 16:42:35 -04:00
|
|
|
function reportTestPlan(plan) {
|
|
|
|
core.opSync("op_dispatch_test_event", {
|
|
|
|
plan,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function reportTestConsoleOutput(console) {
|
|
|
|
core.opSync("op_dispatch_test_event", {
|
|
|
|
output: { console },
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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],
|
|
|
|
});
|
2021-07-14 15:05:16 -04:00
|
|
|
}
|
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
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],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-07-19 13:49:44 -04:00
|
|
|
async function runTests({
|
2021-04-28 14:17:04 -04:00
|
|
|
filter = null,
|
2021-07-05 21:20:33 -04:00
|
|
|
shuffle = null,
|
2020-07-19 13:49:44 -04:00
|
|
|
} = {}) {
|
2021-07-14 15:05:16 -04:00
|
|
|
const origin = getTestOrigin();
|
2020-07-19 13:49:44 -04:00
|
|
|
const originalConsole = globalThis.console;
|
2021-09-04 09:16:35 -04:00
|
|
|
|
2021-09-05 16:42:35 -04:00
|
|
|
globalThis.console = new Console(reportTestConsoleOutput);
|
2020-07-19 13:49:44 -04:00
|
|
|
|
2021-07-03 18:17:52 -04:00
|
|
|
const only = ArrayPrototypeFilter(tests, (test) => test.only);
|
2021-07-14 15:05:16 -04:00
|
|
|
const filtered = ArrayPrototypeFilter(
|
2021-07-03 18:17:52 -04:00
|
|
|
(only.length > 0 ? only : tests),
|
2021-04-28 14:17:04 -04:00
|
|
|
createTestFilter(filter),
|
|
|
|
);
|
2021-07-14 15:05:16 -04:00
|
|
|
|
2021-09-05 16:42:35 -04:00
|
|
|
reportTestPlan({
|
|
|
|
origin,
|
|
|
|
total: filtered.length,
|
|
|
|
filteredOut: tests.length - filtered.length,
|
|
|
|
usedOnly: only.length > 0,
|
2021-04-28 14:17:04 -04:00
|
|
|
});
|
2020-07-19 13:49:44 -04:00
|
|
|
|
2021-07-05 21:20:33 -04:00
|
|
|
if (shuffle !== null) {
|
|
|
|
// http://en.wikipedia.org/wiki/Linear_congruential_generator
|
|
|
|
const nextInt = (function (state) {
|
|
|
|
const m = 0x80000000;
|
|
|
|
const a = 1103515245;
|
|
|
|
const c = 12345;
|
|
|
|
|
|
|
|
return function (max) {
|
|
|
|
return state = ((a * state + c) % m) % max;
|
|
|
|
};
|
|
|
|
}(shuffle));
|
|
|
|
|
2021-07-14 15:05:16 -04:00
|
|
|
for (let i = filtered.length - 1; i > 0; i--) {
|
2021-07-05 21:20:33 -04:00
|
|
|
const j = nextInt(i);
|
2021-07-14 15:05:16 -04:00
|
|
|
[filtered[i], filtered[j]] = [filtered[j], filtered[i]];
|
2021-07-05 21:20:33 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-14 15:05:16 -04:00
|
|
|
for (const test of filtered) {
|
|
|
|
const description = {
|
|
|
|
origin,
|
|
|
|
name: test.name,
|
|
|
|
};
|
2021-07-05 04:26:57 -04:00
|
|
|
const earlier = DateNow();
|
|
|
|
|
2021-09-05 16:42:35 -04:00
|
|
|
reportTestWait(description);
|
2021-07-05 04:26:57 -04:00
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
const result = await runTest(test, description);
|
2021-07-14 15:05:16 -04:00
|
|
|
const elapsed = DateNow() - earlier;
|
2021-07-05 04:26:57 -04:00
|
|
|
|
2021-09-05 16:42:35 -04:00
|
|
|
reportTestResult(description, result, elapsed);
|
2020-07-19 13:49:44 -04:00
|
|
|
}
|
|
|
|
|
2021-09-04 09:16:35 -04:00
|
|
|
globalThis.console = originalConsole;
|
2020-07-19 13:49:44 -04:00
|
|
|
}
|
|
|
|
|
2021-10-11 09:45:02 -04:00
|
|
|
/**
|
|
|
|
* @typedef {{
|
|
|
|
* fn: (t: TestContext) => void | Promise<void>,
|
|
|
|
* name: string,
|
|
|
|
* ignore?: boolean,
|
|
|
|
* sanitizeOps?: boolean,
|
|
|
|
* sanitizeResources?: boolean,
|
|
|
|
* sanitizeExit?: boolean,
|
|
|
|
* }} TestStepDefinition
|
|
|
|
*
|
|
|
|
* @typedef {{
|
|
|
|
* name: string;
|
|
|
|
* parent: TestStep | 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;
|
|
|
|
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 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
failedChildStepsCount() {
|
|
|
|
return ArrayPrototypeFilter(
|
|
|
|
this.children,
|
|
|
|
/** @param step {TestStep} */
|
|
|
|
(step) => step.status === "failed",
|
|
|
|
).length;
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
selfAndAllAncestorsUseSanitizer() {
|
|
|
|
if (!this.usesSanitizer) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getFullName() {
|
|
|
|
if (this.parent) {
|
|
|
|
return `${this.parent.getFullName()} > ${this.name}`;
|
|
|
|
} else {
|
|
|
|
return this.name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
reportWait() {
|
|
|
|
if (this.reportedWait || !this.parent) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
reportTestStepWait(this.#getTestStepDescription());
|
|
|
|
|
|
|
|
this.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;
|
|
|
|
}
|
|
|
|
|
|
|
|
#getStepResult() {
|
|
|
|
switch (this.status) {
|
|
|
|
case "ok":
|
|
|
|
return "ok";
|
|
|
|
case "ignored":
|
|
|
|
return "ignored";
|
|
|
|
case "pending":
|
|
|
|
return {
|
|
|
|
"pending": this.error && formatError(this.error),
|
|
|
|
};
|
|
|
|
case "failed":
|
|
|
|
return {
|
|
|
|
"failed": this.error && formatError(this.error),
|
|
|
|
};
|
|
|
|
default:
|
|
|
|
throw new Error(`Unhandled status: ${this.status}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#getTestStepDescription() {
|
|
|
|
return {
|
|
|
|
test: this.rootTestDescription,
|
|
|
|
name: this.name,
|
|
|
|
level: this.#getLevel(),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
#getLevel() {
|
|
|
|
let count = 0;
|
|
|
|
for (const _ of this.ancestors()) {
|
|
|
|
count++;
|
|
|
|
}
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @param parentStep {TestStep} */
|
|
|
|
function createTestContext(parentStep) {
|
|
|
|
return {
|
|
|
|
[SymbolToStringTag]: "TestContext",
|
|
|
|
/**
|
|
|
|
* @param nameOrTestDefinition {string | TestStepDefinition}
|
|
|
|
* @param fn {(t: TestContext) => void | Promise<void>}
|
|
|
|
*/
|
|
|
|
async step(nameOrTestDefinition, fn) {
|
|
|
|
if (!testStepsEnabled) {
|
|
|
|
throw new Error(
|
|
|
|
"Test steps are unstable. The --unstable flag must be provided.",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parentStep.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,
|
|
|
|
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);
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (definition.ignore) {
|
|
|
|
subStep.status = "ignored";
|
|
|
|
subStep.finalized = true;
|
|
|
|
if (subStep.canStreamReporting()) {
|
|
|
|
subStep.reportResult();
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const testFn = wrapTestFnWithSanitizers(
|
|
|
|
definition.fn,
|
|
|
|
subStep.sanitizerOptions,
|
|
|
|
);
|
|
|
|
const start = DateNow();
|
|
|
|
|
|
|
|
try {
|
|
|
|
await testFn(subStep);
|
|
|
|
|
|
|
|
if (subStep.failedChildStepsCount() > 0) {
|
|
|
|
subStep.status = "failed";
|
|
|
|
} else {
|
|
|
|
subStep.status = "ok";
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
subStep.error = formatError(error);
|
|
|
|
subStep.status = "failed";
|
|
|
|
}
|
|
|
|
|
|
|
|
subStep.elapsed = DateNow() - start;
|
|
|
|
|
|
|
|
if (subStep.parent?.finalized) {
|
|
|
|
// always point this test out as one that was still running
|
|
|
|
// if the parent step finalized
|
|
|
|
subStep.status = "pending";
|
|
|
|
}
|
|
|
|
|
|
|
|
subStep.finalized = true;
|
|
|
|
|
|
|
|
if (subStep.reportedWait && subStep.canStreamReporting()) {
|
|
|
|
subStep.reportResult();
|
|
|
|
}
|
|
|
|
|
|
|
|
return subStep.status === "ok";
|
|
|
|
} finally {
|
|
|
|
if (parentStep.canStreamReporting()) {
|
|
|
|
// flush any buffered steps
|
|
|
|
for (const parentChild of parentStep.children) {
|
|
|
|
parentChild.reportResult();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @returns {TestStepDefinition} */
|
|
|
|
function getDefinition() {
|
|
|
|
if (typeof nameOrTestDefinition === "string") {
|
|
|
|
if (!(fn instanceof Function)) {
|
|
|
|
throw new TypeError("Expected function for second argument.");
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
name: nameOrTestDefinition,
|
|
|
|
fn,
|
|
|
|
};
|
|
|
|
} else if (typeof nameOrTestDefinition === "object") {
|
|
|
|
return nameOrTestDefinition;
|
|
|
|
} else {
|
|
|
|
throw new TypeError(
|
|
|
|
"Expected a test definition or name and function.",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @template T {Function}
|
|
|
|
* @param testFn {T}
|
|
|
|
* @param opts {{
|
|
|
|
* sanitizeOps: boolean,
|
|
|
|
* sanitizeResources: boolean,
|
|
|
|
* sanitizeExit: boolean,
|
|
|
|
* }}
|
|
|
|
* @returns {T}
|
|
|
|
*/
|
|
|
|
function wrapTestFnWithSanitizers(testFn, opts) {
|
|
|
|
testFn = assertTestStepScopes(testFn);
|
|
|
|
|
|
|
|
if (opts.sanitizeOps) {
|
|
|
|
testFn = assertOps(testFn);
|
|
|
|
}
|
|
|
|
if (opts.sanitizeResources) {
|
|
|
|
testFn = assertResources(testFn);
|
|
|
|
}
|
|
|
|
if (opts.sanitizeExit) {
|
|
|
|
testFn = assertExit(testFn);
|
|
|
|
}
|
|
|
|
return testFn;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @template T
|
|
|
|
* @param value {T | undefined}
|
|
|
|
* @param defaultValue {T}
|
|
|
|
* @returns T
|
|
|
|
*/
|
|
|
|
function getOrDefault(value, defaultValue) {
|
|
|
|
return value == null ? defaultValue : value;
|
|
|
|
}
|
|
|
|
|
|
|
|
function enableTestSteps() {
|
|
|
|
testStepsEnabled = true;
|
|
|
|
}
|
|
|
|
|
2021-03-12 15:23:59 -05:00
|
|
|
window.__bootstrap.internals = {
|
|
|
|
...window.__bootstrap.internals ?? {},
|
|
|
|
runTests,
|
2021-10-11 09:45:02 -04:00
|
|
|
enableTestSteps,
|
2021-03-12 15:23:59 -05:00
|
|
|
};
|
2020-07-19 13:49:44 -04:00
|
|
|
|
|
|
|
window.__bootstrap.testing = {
|
|
|
|
test,
|
|
|
|
};
|
|
|
|
})(this);
|