2020-02-11 06:01:56 -05:00
|
|
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
2020-03-05 05:52:18 -05:00
|
|
|
import { red, green, bgRed, gray, italic } from "./colors.ts";
|
2020-03-08 08:09:22 -04:00
|
|
|
import { exit } from "./ops/os.ts";
|
2020-03-11 05:53:06 -04:00
|
|
|
import { Console } from "./web/console.ts";
|
2020-02-11 06:01:56 -05:00
|
|
|
|
2020-03-05 05:52:18 -05:00
|
|
|
function formatDuration(time = 0): string {
|
|
|
|
const timeStr = `(${time}ms)`;
|
|
|
|
return gray(italic(timeStr));
|
2020-02-11 06:01:56 -05:00
|
|
|
}
|
|
|
|
|
2020-03-05 05:52:18 -05:00
|
|
|
function defer(n: number): Promise<void> {
|
|
|
|
return new Promise((resolve: () => void, _) => {
|
|
|
|
setTimeout(resolve, n);
|
|
|
|
});
|
2020-02-11 06:01:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export type TestFunction = () => void | Promise<void>;
|
|
|
|
|
|
|
|
export interface TestDefinition {
|
|
|
|
fn: TestFunction;
|
|
|
|
name: string;
|
|
|
|
}
|
|
|
|
|
2020-03-05 05:52:18 -05:00
|
|
|
const TEST_REGISTRY: TestDefinition[] = [];
|
2020-02-11 06:01:56 -05:00
|
|
|
|
|
|
|
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;
|
2020-03-05 05:52:18 -05:00
|
|
|
failFast?: boolean;
|
|
|
|
only?: string | RegExp;
|
|
|
|
skip?: string | RegExp;
|
2020-02-11 06:01:56 -05:00
|
|
|
disableLog?: boolean;
|
|
|
|
}
|
|
|
|
|
2020-03-05 05:52:18 -05:00
|
|
|
function filterTests(
|
|
|
|
tests: TestDefinition[],
|
|
|
|
only: undefined | string | RegExp,
|
|
|
|
skip: undefined | string | RegExp
|
|
|
|
): TestDefinition[] {
|
|
|
|
return tests.filter((def: TestDefinition): boolean => {
|
|
|
|
let passes = true;
|
|
|
|
|
|
|
|
if (only) {
|
|
|
|
if (only instanceof RegExp) {
|
|
|
|
passes = passes && only.test(def.name);
|
|
|
|
} else {
|
|
|
|
passes = passes && def.name.includes(only);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (skip) {
|
|
|
|
if (skip instanceof RegExp) {
|
|
|
|
passes = passes && !skip.test(def.name);
|
|
|
|
} else {
|
|
|
|
passes = passes && !def.name.includes(skip);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return passes;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-02-11 06:01:56 -05:00
|
|
|
export async function runTests({
|
2020-03-05 05:52:18 -05:00
|
|
|
exitOnFail = true,
|
|
|
|
failFast = false,
|
|
|
|
only = undefined,
|
|
|
|
skip = undefined,
|
2020-02-11 06:01:56 -05:00
|
|
|
disableLog = false
|
|
|
|
}: RunTestsOptions = {}): Promise<void> {
|
2020-03-05 05:52:18 -05:00
|
|
|
const testsToRun = filterTests(TEST_REGISTRY, only, skip);
|
2020-02-11 06:01:56 -05:00
|
|
|
|
|
|
|
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`);
|
2020-03-05 05:52:18 -05:00
|
|
|
const suiteStart = +new Date();
|
2020-02-11 06:01:56 -05:00
|
|
|
|
|
|
|
for (const testCase of testCases) {
|
|
|
|
try {
|
2020-03-05 05:52:18 -05:00
|
|
|
const start = +new Date();
|
2020-02-11 06:01:56 -05:00
|
|
|
await testCase.fn();
|
2020-03-05 05:52:18 -05:00
|
|
|
testCase.timeElapsed = +new Date() - start;
|
2020-02-11 06:01:56 -05:00
|
|
|
originalConsole.log(
|
2020-03-05 05:52:18 -05:00
|
|
|
`${GREEN_OK} ${testCase.name} ${formatDuration(
|
|
|
|
testCase.timeElapsed
|
|
|
|
)}`
|
2020-02-11 06:01:56 -05:00
|
|
|
);
|
|
|
|
stats.passed++;
|
|
|
|
} catch (err) {
|
|
|
|
testCase.error = err;
|
|
|
|
originalConsole.log(`${RED_FAILED} ${testCase.name}`);
|
|
|
|
originalConsole.log(err.stack);
|
|
|
|
stats.failed++;
|
2020-03-05 05:52:18 -05:00
|
|
|
if (failFast) {
|
2020-02-11 06:01:56 -05:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-05 05:52:18 -05:00
|
|
|
const suiteDuration = +new Date() - suiteStart;
|
2020-02-11 06:01:56 -05:00
|
|
|
|
|
|
|
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 ` +
|
2020-03-05 05:52:18 -05:00
|
|
|
`${formatDuration(suiteDuration)}\n`
|
2020-02-11 06:01:56 -05:00
|
|
|
);
|
|
|
|
|
2020-03-05 05:52:18 -05:00
|
|
|
// TODO(bartlomieju): is `defer` really needed? Shouldn't unhandled
|
|
|
|
// promise rejection be handled per test case?
|
|
|
|
// Use defer to avoid the error being ignored due to unhandled
|
|
|
|
// promise rejections being swallowed.
|
|
|
|
await defer(0);
|
|
|
|
|
|
|
|
if (stats.failed > 0) {
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (exitOnFail) {
|
2020-02-11 06:01:56 -05:00
|
|
|
exit(1);
|
2020-03-05 05:52:18 -05:00
|
|
|
}
|
2020-02-11 06:01:56 -05:00
|
|
|
}
|
|
|
|
}
|