// 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({ name, ignore, fn, permissions }) { let token = null; const time = DateNow(); try { postTestMessage("wait", { name, }); if (permissions) { token = pledgeTestPermissions(permissions); } if (ignore) { const duration = DateNow() - time; postTestMessage("result", { name, duration, result: "ignored", }); return; } await fn(); const duration = DateNow() - time; postTestMessage("result", { name, duration, result: "ok", }); } catch (error) { const duration = DateNow() - time; postTestMessage("result", { name, duration, result: { "failed": inspectArgs([error]), }, }); } finally { if (token) { restoreTestPermissions(token); } } } async function runTests({ disableLog = false, filter = 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, }); for (const test of pending) { await runTest(test); } if (disableLog) { globalThis.console = originalConsole; } } window.__bootstrap.internals = { ...window.__bootstrap.internals ?? {}, runTests, }; window.__bootstrap.testing = { test, }; })(this);