// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { red, green, bgRed, bold, white, gray, italic } from "./colors.ts"; import { exit } from "./os.ts"; import { Console } from "./console.ts"; function formatTestTime(time = 0): string { return `${time.toFixed(2)}ms`; } function promptTestTime(time = 0, displayWarning = false): string { // if time > 5s we display a warning // only for test time, not the full runtime if (displayWarning && time >= 5000) { return bgRed(white(bold(`(${formatTestTime(time)})`))); } else { return gray(italic(`(${formatTestTime(time)})`)); } } export type TestFunction = () => void | Promise; export interface TestDefinition { fn: TestFunction; name: string; } declare global { // Only `var` variables show up in the `globalThis` type when doing a global // scope augmentation. // eslint-disable-next-line no-var var __DENO_TEST_REGISTRY: TestDefinition[]; } let TEST_REGISTRY: TestDefinition[] = []; if (globalThis["__DENO_TEST_REGISTRY"]) { TEST_REGISTRY = globalThis.__DENO_TEST_REGISTRY as TestDefinition[]; } else { Object.defineProperty(globalThis, "__DENO_TEST_REGISTRY", { enumerable: false, value: TEST_REGISTRY }); } export function test(t: TestDefinition): void; export function test(fn: TestFunction): void; export function test(name: string, fn: TestFunction): void; // Main test function provided by Deno, as you can see it merely // creates a new object with "name" and "fn" fields. export function test( t: string | TestDefinition | TestFunction, fn?: TestFunction ): void { let name: string; if (typeof t === "string") { if (!fn) { throw new Error("Missing test function"); } name = t; if (!name) { throw new Error("The name of test case can't be empty"); } } else if (typeof t === "function") { fn = t; name = t.name; if (!name) { throw new Error("Test function can't be anonymous"); } } else { fn = t.fn; if (!fn) { throw new Error("Missing test function"); } name = t.name; if (!name) { throw new Error("The name of test case can't be empty"); } } TEST_REGISTRY.push({ fn, name }); } interface TestStats { filtered: number; ignored: number; measured: number; passed: number; failed: number; } interface TestCase { name: string; fn: TestFunction; timeElapsed?: number; error?: Error; } export interface RunTestsOptions { exitOnFail?: boolean; only?: RegExp; skip?: RegExp; disableLog?: boolean; } export async function runTests({ exitOnFail = false, only = /[^\s]/, skip = /^\s*$/, disableLog = false }: RunTestsOptions = {}): Promise { const testsToRun = TEST_REGISTRY.filter( ({ name }): boolean => only.test(name) && !skip.test(name) ); const stats: TestStats = { measured: 0, ignored: 0, filtered: 0, passed: 0, failed: 0 }; const testCases = testsToRun.map( ({ name, fn }): TestCase => { return { name, fn, timeElapsed: 0, error: undefined }; } ); // @ts-ignore const originalConsole = globalThis.console; // TODO(bartlomieju): add option to capture output of test // cases and display it if test fails (like --nopcature in Rust) const disabledConsole = new Console( (_x: string, _isErr?: boolean): void => {} ); if (disableLog) { // @ts-ignore globalThis.console = disabledConsole; } const RED_FAILED = red("FAILED"); const GREEN_OK = green("OK"); const RED_BG_FAIL = bgRed(" FAIL "); originalConsole.log(`running ${testsToRun.length} tests`); const suiteStart = performance.now(); for (const testCase of testCases) { try { const start = performance.now(); await testCase.fn(); const end = performance.now(); testCase.timeElapsed = end - start; originalConsole.log( `${GREEN_OK} ${testCase.name} ${promptTestTime(end - start, true)}` ); stats.passed++; } catch (err) { testCase.error = err; originalConsole.log(`${RED_FAILED} ${testCase.name}`); originalConsole.log(err.stack); stats.failed++; if (exitOnFail) { break; } } } const suiteEnd = performance.now(); if (disableLog) { // @ts-ignore globalThis.console = originalConsole; } // Attempting to match the output of Rust's test runner. originalConsole.log( `\ntest result: ${stats.failed ? RED_BG_FAIL : GREEN_OK} ` + `${stats.passed} passed; ${stats.failed} failed; ` + `${stats.ignored} ignored; ${stats.measured} measured; ` + `${stats.filtered} filtered out ` + `${promptTestTime(suiteEnd - suiteStart)}\n` ); // TODO(bartlomieju): what's it for? Do we really need, maybe add handler for unhandled // promise to avoid such shenanigans if (stats.failed) { // Use setTimeout to avoid the error being ignored due to unhandled // promise rejections being swallowed. setTimeout((): void => { originalConsole.error(`There were ${stats.failed} test failures.`); testCases .filter(testCase => !!testCase.error) .forEach(testCase => { originalConsole.error(`${RED_BG_FAIL} ${red(testCase.name)}`); originalConsole.error(testCase.error); }); exit(1); }, 0); } }