mirror of
https://github.com/denoland/deno.git
synced 2024-10-29 08:58:01 -04:00
290 lines
7.3 KiB
JavaScript
290 lines
7.3 KiB
JavaScript
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
|
"use strict";
|
|
|
|
((window) => {
|
|
const core = window.Deno.core;
|
|
const { parsePermissions } = window.__bootstrap.worker;
|
|
const { setExitHandler } = window.__bootstrap.os;
|
|
const { Console, inspectArgs } = window.__bootstrap.console;
|
|
const { metrics } = window.__bootstrap.metrics;
|
|
const { assert } = window.__bootstrap.util;
|
|
const {
|
|
ArrayPrototypeFilter,
|
|
ArrayPrototypePush,
|
|
DateNow,
|
|
JSONStringify,
|
|
Promise,
|
|
TypeError,
|
|
StringPrototypeStartsWith,
|
|
StringPrototypeEndsWith,
|
|
StringPrototypeIncludes,
|
|
StringPrototypeSlice,
|
|
RegExp,
|
|
RegExpPrototypeTest,
|
|
} = window.__bootstrap.primordials;
|
|
|
|
// 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) {
|
|
return async function asyncOpSanitizer() {
|
|
const pre = metrics();
|
|
try {
|
|
await fn();
|
|
} 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)
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
}
|
|
|
|
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,
|
|
) {
|
|
return async function resourceSanitizer() {
|
|
const pre = core.resources();
|
|
await fn();
|
|
const post = core.resources();
|
|
|
|
const preStr = JSONStringify(pre, null, 2);
|
|
const postStr = JSONStringify(post, null, 2);
|
|
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);
|
|
};
|
|
}
|
|
|
|
// Wrap test function in additional assertion that makes sure
|
|
// that the test case does not accidentally exit prematurely.
|
|
function assertExit(fn) {
|
|
return async function exitSanitizer() {
|
|
setExitHandler((exitCode) => {
|
|
assert(
|
|
false,
|
|
`Test case attempted to exit with exit code: ${exitCode}`,
|
|
);
|
|
});
|
|
|
|
try {
|
|
await fn();
|
|
} catch (err) {
|
|
throw err;
|
|
} finally {
|
|
setExitHandler(null);
|
|
}
|
|
};
|
|
}
|
|
|
|
const tests = [];
|
|
|
|
// Main test function provided by Deno, as you can see it merely
|
|
// creates a new object with "name" and "fn" fields.
|
|
function test(
|
|
t,
|
|
fn,
|
|
) {
|
|
let testDef;
|
|
const defaults = {
|
|
ignore: false,
|
|
only: false,
|
|
sanitizeOps: true,
|
|
sanitizeResources: true,
|
|
sanitizeExit: true,
|
|
permissions: null,
|
|
};
|
|
|
|
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 };
|
|
}
|
|
|
|
if (testDef.sanitizeOps) {
|
|
testDef.fn = assertOps(testDef.fn);
|
|
}
|
|
|
|
if (testDef.sanitizeResources) {
|
|
testDef.fn = assertResources(testDef.fn);
|
|
}
|
|
|
|
if (testDef.sanitizeExit) {
|
|
testDef.fn = assertExit(testDef.fn);
|
|
}
|
|
|
|
ArrayPrototypePush(tests, testDef);
|
|
}
|
|
|
|
function postTestMessage(kind, data) {
|
|
return core.opSync("op_post_test_message", { message: { kind, data } });
|
|
}
|
|
|
|
function createTestFilter(filter) {
|
|
return (def) => {
|
|
if (filter) {
|
|
if (
|
|
StringPrototypeStartsWith(filter, "/") &&
|
|
StringPrototypeEndsWith(filter, "/")
|
|
) {
|
|
const regex = new RegExp(
|
|
StringPrototypeSlice(filter, 1, filter.length - 1),
|
|
);
|
|
return RegExpPrototypeTest(regex, def.name);
|
|
}
|
|
|
|
return StringPrototypeIncludes(def.name, filter);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
}
|
|
|
|
function pledgeTestPermissions(permissions) {
|
|
return core.opSync(
|
|
"op_pledge_test_permissions",
|
|
parsePermissions(permissions),
|
|
);
|
|
}
|
|
|
|
function restoreTestPermissions(token) {
|
|
core.opSync("op_restore_test_permissions", token);
|
|
}
|
|
|
|
async function runTest({ ignore, fn, permissions }) {
|
|
if (ignore) {
|
|
return "ignored";
|
|
}
|
|
|
|
let token = null;
|
|
|
|
try {
|
|
if (permissions) {
|
|
token = pledgeTestPermissions(permissions);
|
|
}
|
|
await fn();
|
|
|
|
return "ok";
|
|
} catch (error) {
|
|
return { "failed": inspectArgs([error]) };
|
|
} finally {
|
|
if (token) {
|
|
restoreTestPermissions(token);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function runTests({
|
|
disableLog = false,
|
|
filter = null,
|
|
shuffle = null,
|
|
} = {}) {
|
|
const originalConsole = globalThis.console;
|
|
if (disableLog) {
|
|
globalThis.console = new Console(() => {});
|
|
}
|
|
|
|
const only = ArrayPrototypeFilter(tests, (test) => test.only);
|
|
const pending = ArrayPrototypeFilter(
|
|
(only.length > 0 ? only : tests),
|
|
createTestFilter(filter),
|
|
);
|
|
postTestMessage("plan", {
|
|
filtered: tests.length - pending.length,
|
|
pending: pending.length,
|
|
only: only.length > 0,
|
|
});
|
|
|
|
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));
|
|
|
|
for (let i = pending.length - 1; i > 0; i--) {
|
|
const j = nextInt(i);
|
|
[pending[i], pending[j]] = [pending[j], pending[i]];
|
|
}
|
|
}
|
|
|
|
for (const test of pending) {
|
|
const {
|
|
name,
|
|
} = test;
|
|
|
|
const earlier = DateNow();
|
|
|
|
postTestMessage("wait", {
|
|
name,
|
|
});
|
|
|
|
const result = await runTest(test);
|
|
const duration = DateNow() - earlier;
|
|
|
|
postTestMessage("result", {
|
|
name,
|
|
result,
|
|
duration,
|
|
});
|
|
}
|
|
|
|
if (disableLog) {
|
|
globalThis.console = originalConsole;
|
|
}
|
|
}
|
|
|
|
window.__bootstrap.internals = {
|
|
...window.__bootstrap.internals ?? {},
|
|
runTests,
|
|
};
|
|
|
|
window.__bootstrap.testing = {
|
|
test,
|
|
};
|
|
})(this);
|