diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2020-02-11 12:01:56 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-02-11 12:01:56 +0100 |
commit | a3bfbcceade3d359f677106399562b461b4af01a (patch) | |
tree | 93f6fcc56d98bbc0f71f5b5782381f672895f634 /cli/js/testing.ts | |
parent | 701ce9b3342647cf01cb23c4fc28bc99ce0aa8c1 (diff) |
refactor: rewrite deno test, add Deno.test() (#3865)
* rewrite test runner in Rust
* migrate "test" and "runTests" functions from std to "Deno" namespace
* use "Deno.test()" to run internal JS unit tests
* remove std downloads for Deno subcommands
Diffstat (limited to 'cli/js/testing.ts')
-rw-r--r-- | cli/js/testing.ts | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/cli/js/testing.ts b/cli/js/testing.ts new file mode 100644 index 000000000..b4c86e8b8 --- /dev/null +++ b/cli/js/testing.ts @@ -0,0 +1,207 @@ +// 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<void>; + +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<void> { + 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); + } +} |