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 | |
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')
-rw-r--r-- | cli/js/colors.ts | 24 | ||||
-rw-r--r-- | cli/js/deno.ts | 1 | ||||
-rw-r--r-- | cli/js/globals.ts | 8 | ||||
-rw-r--r-- | cli/js/lib.deno.ns.d.ts | 20 | ||||
-rw-r--r-- | cli/js/net_test.ts | 3 | ||||
-rw-r--r-- | cli/js/test_util.ts | 10 | ||||
-rw-r--r-- | cli/js/testing.ts | 207 | ||||
-rw-r--r-- | cli/js/tls_test.ts | 3 | ||||
-rw-r--r-- | cli/js/unit_tests.ts | 9 |
9 files changed, 265 insertions, 20 deletions
diff --git a/cli/js/colors.ts b/cli/js/colors.ts index 21539c83e..372e90ba5 100644 --- a/cli/js/colors.ts +++ b/cli/js/colors.ts @@ -31,6 +31,10 @@ export function bold(str: string): string { return run(str, code(1, 22)); } +export function italic(str: string): string { + return run(str, code(3, 23)); +} + export function yellow(str: string): string { return run(str, code(33, 39)); } @@ -38,3 +42,23 @@ export function yellow(str: string): string { export function cyan(str: string): string { return run(str, code(36, 39)); } + +export function red(str: string): string { + return run(str, code(31, 39)); +} + +export function green(str: string): string { + return run(str, code(32, 39)); +} + +export function bgRed(str: string): string { + return run(str, code(41, 49)); +} + +export function white(str: string): string { + return run(str, code(37, 39)); +} + +export function gray(str: string): string { + return run(str, code(90, 39)); +} diff --git a/cli/js/deno.ts b/cli/js/deno.ts index 077b198c4..c52e6dc2d 100644 --- a/cli/js/deno.ts +++ b/cli/js/deno.ts @@ -111,6 +111,7 @@ export { utimeSync, utime } from "./utime.ts"; export { version } from "./version.ts"; export { writeFileSync, writeFile, WriteFileOptions } from "./write_file.ts"; export const args: string[] = []; +export { test, runTests } from "./testing.ts"; // These are internal Deno APIs. We are marking them as internal so they do not // appear in the runtime type library. diff --git a/cli/js/globals.ts b/cli/js/globals.ts index 53eb696ac..9a7161ff0 100644 --- a/cli/js/globals.ts +++ b/cli/js/globals.ts @@ -59,6 +59,11 @@ declare global { thrown: any; } + interface ImportMeta { + url: string; + main: boolean; + } + interface DenoCore { print(s: string, isErr?: boolean): void; dispatch( @@ -137,6 +142,9 @@ declare global { // Assigned to `self` global - compiler var bootstrapTsCompilerRuntime: (() => void) | undefined; var bootstrapWasmCompilerRuntime: (() => void) | undefined; + + var performance: performanceUtil.Performance; + var setTimeout: typeof timers.setTimeout; /* eslint-enable */ } diff --git a/cli/js/lib.deno.ns.d.ts b/cli/js/lib.deno.ns.d.ts index 4bbbf6320..b96e108c3 100644 --- a/cli/js/lib.deno.ns.d.ts +++ b/cli/js/lib.deno.ns.d.ts @@ -10,6 +10,26 @@ declare namespace Deno { /** Reflects the NO_COLOR environment variable: https://no-color.org/ */ export let noColor: boolean; + export type TestFunction = () => void | Promise<void>; + + export interface TestDefinition { + fn: TestFunction; + name: string; + } + + export function test(t: TestDefinition): void; + export function test(fn: TestFunction): void; + export function test(name: string, fn: TestFunction): void; + + export interface RunTestsOptions { + exitOnFail?: boolean; + only?: RegExp; + skip?: RegExp; + disableLog?: boolean; + } + + export function runTests(opts?: RunTestsOptions): Promise<void>; + /** Check if running in terminal. * * console.log(Deno.isTTY().stdout); diff --git a/cli/js/net_test.ts b/cli/js/net_test.ts index 7f5334df3..e4d0be81f 100644 --- a/cli/js/net_test.ts +++ b/cli/js/net_test.ts @@ -1,6 +1,5 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { testPerm, assert, assertEquals } from "./test_util.ts"; -import { runIfMain } from "../../std/testing/mod.ts"; testPerm({ net: true }, function netListenClose(): void { const listener = Deno.listen({ hostname: "127.0.0.1", port: 4500 }); @@ -240,5 +239,3 @@ testPerm({ net: true }, async function netDoubleCloseWrite() { conn.close(); }); */ - -runIfMain(import.meta); diff --git a/cli/js/test_util.ts b/cli/js/test_util.ts index a546fa5c5..dbb7bf2c4 100644 --- a/cli/js/test_util.ts +++ b/cli/js/test_util.ts @@ -7,7 +7,6 @@ // tests by the special string. permW1N0 means allow-write but not allow-net. // See tools/unit_tests.py for more details. -import * as testing from "../../std/testing/mod.ts"; import { assert, assertEquals } from "../../std/testing/asserts.ts"; export { assert, @@ -103,10 +102,7 @@ function normalizeTestPermissions(perms: TestPermissions): Permissions { }; } -export function testPerm( - perms: TestPermissions, - fn: testing.TestFunction -): void { +export function testPerm(perms: TestPermissions, fn: Deno.TestFunction): void { const normalizedPerms = normalizeTestPermissions(perms); registerPermCombination(normalizedPerms); @@ -115,10 +111,10 @@ export function testPerm( return; } - testing.test(fn); + Deno.test(fn); } -export function test(fn: testing.TestFunction): void { +export function test(fn: Deno.TestFunction): void { testPerm( { read: false, 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); + } +} diff --git a/cli/js/tls_test.ts b/cli/js/tls_test.ts index de841f5a1..1273da34f 100644 --- a/cli/js/tls_test.ts +++ b/cli/js/tls_test.ts @@ -2,7 +2,6 @@ import { test, testPerm, assert, assertEquals } from "./test_util.ts"; import { BufWriter, BufReader } from "../../std/io/bufio.ts"; import { TextProtoReader } from "../../std/textproto/mod.ts"; -import { runIfMain } from "../../std/testing/mod.ts"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); @@ -202,5 +201,3 @@ testPerm({ read: true, net: true }, async function dialAndListenTLS(): Promise< assertEquals(decoder.decode(bodyBuf), "Hello World\n"); conn.close(); }); - -runIfMain(import.meta); diff --git a/cli/js/unit_tests.ts b/cli/js/unit_tests.ts index a6435d183..3c808fe46 100644 --- a/cli/js/unit_tests.ts +++ b/cli/js/unit_tests.ts @@ -61,11 +61,6 @@ import "./permissions_test.ts"; import "./version_test.ts"; import "./workers_test.ts"; -import { runIfMain } from "../../std/testing/mod.ts"; - -async function main(): Promise<void> { - // Testing entire test suite serially - runIfMain(import.meta); +if (import.meta.main) { + await Deno.runTests(); } - -main(); |