Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Testing] Pretty output + Silent mode #314

Merged
merged 12 commits into from
Jun 19, 2019
157 changes: 132 additions & 25 deletions testing/mod.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,72 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.

import { green, red } from "../colors/mod.ts";

import {
bgRed,
white,
bold,
green,
red,
gray,
yellow,
italic
} from "../colors/mod.ts";
export type TestFunction = () => void | Promise<void>;

export interface TestDefinition {
fn: TestFunction;
name: string;
}

// Replacement of the global `console` function to be in silent mode
const noop = function(): void {};

// Save Object of the global `console` in case of silent mode
type Console = typeof window.console;
// ref https://console.spec.whatwg.org/#console-namespace
// For historical web-compatibility reasons, the namespace object for
// console must have as its [[Prototype]] an empty object, created as if
// by ObjectCreate(%ObjectPrototype%), instead of %ObjectPrototype%.
const disabledConsole = Object.create({}) as Console;
Object.assign(disabledConsole, {
log: noop,
debug: noop,
info: noop,
dir: noop,
warn: noop,
error: noop,
assert: noop,
count: noop,
countReset: noop,
table: noop,
time: noop,
timeLog: noop,
timeEnd: noop,
group: noop,
groupCollapsed: noop,
groupEnd: noop,
clear: noop
});

const originalConsole = window.console;

function enableConsole(): void {
window.console = originalConsole;
}

function disableConsole(): void {
window.console = disabledConsole;
}

const encoder = new TextEncoder();
function print(txt: string, carriageReturn: boolean = true): void {
if (carriageReturn) {
txt += "\n";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a carriage return character, this a new line character.

s/carriageReturn/newline/g

}
Deno.stdout.writeSync(encoder.encode(`${txt}`));
}

let filterRegExp: RegExp | null;
const candidates: TestDefinition[] = [];

let filtered = 0;

// Must be called before any test() that needs to be filtered.
Expand Down Expand Up @@ -42,7 +97,7 @@ export function test(t: TestDefinition | TestFunction): void {
}

const RED_FAILED = red("FAILED");
const GREEN_OK = green("ok");
const GREEN_OK = green("OK");

interface TestStats {
filtered: number;
Expand All @@ -53,6 +108,7 @@ interface TestStats {
}

interface TestResult {
timeElapsed?: number;
name: string;
error?: Error;
ok: boolean;
Expand All @@ -75,15 +131,36 @@ function createTestResults(tests: TestDefinition[]): TestResults {
);
}

function formatTestTime(time: number = 0): string {
if (time >= 1000) {
return `${(time / 1000).toFixed(2)}s`;
} else {
return `${time.toFixed(2)}ms`;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer if you use a single time unit (ms)

}

function promptTestTime(time: number = 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)})`));
}
}

function report(result: TestResult): void {
if (result.ok) {
console.log(`test ${result.name} ... ${GREEN_OK}`);
zekth marked this conversation as resolved.
Show resolved Hide resolved
} else if (result.error) {
console.error(
`test ${result.name} ... ${RED_FAILED}\n${result.error.stack}`
print(
`${GREEN_OK} ${result.name} ${promptTestTime(
result.timeElapsed,
true
)}`
);
} else if (result.error) {
print(`${RED_FAILED} ${result.name}\n${result.error.stack}`);
} else {
console.log(`test ${result.name} ... unresolved`);
print(`test ${result.name} ... unresolved`);
}
result.printed = true;
}
Expand All @@ -92,7 +169,8 @@ function printResults(
stats: TestStats,
results: TestResults,
flush: boolean,
exitOnFail: boolean
exitOnFail: boolean,
timeElapsed: number
): void {
if (flush) {
for (const result of results.cases.values()) {
Expand All @@ -105,11 +183,12 @@ function printResults(
}
}
// Attempting to match the output of Rust's test runner.
console.log(
print(
`\ntest result: ${stats.failed ? RED_FAILED : GREEN_OK}. ` +
`${stats.passed} passed; ${stats.failed} failed; ` +
`${stats.ignored} ignored; ${stats.measured} measured; ` +
`${stats.filtered} filtered out\n`
`${stats.filtered} filtered out ` +
`${promptTestTime(timeElapsed)}\n`
);
}

Expand All @@ -129,9 +208,12 @@ async function createTestCase(
): Promise<void> {
const result: TestResult = results.cases.get(results.keys.get(name)!)!;
try {
const start = performance.now();
await fn();
const end = performance.now();
stats.passed++;
result.ok = true;
result.timeElapsed = end - start;
} catch (err) {
stats.failed++;
result.error = err;
Expand Down Expand Up @@ -170,21 +252,33 @@ async function runTestsParallel(
async function runTestsSerial(
stats: TestStats,
tests: TestDefinition[],
exitOnFail: boolean
exitOnFail: boolean,
disableLog: boolean
): Promise<void> {
for (const { fn, name } of tests) {
// See https://github.com/denoland/deno/pull/1452
// about this usage of groupCollapsed
console.groupCollapsed(`test ${name} `);
// Displaying the currently running test if silent mode
if (disableLog) {
print(`${yellow("RUNNING")} ${name}`, false);
}
try {
let start, end;
start = performance.now();
await fn();
end = performance.now();
if (disableLog) {
// Rewriting the current prompt line to erase `running ....`
print("\x1b[2K\r", false);
}
stats.passed++;
console.log("...", GREEN_OK);
console.groupEnd();
print(
GREEN_OK + " " + name + " " + promptTestTime(end - start, true)
);
} catch (err) {
console.log("...", RED_FAILED);
console.groupEnd();
console.error(err.stack);
if (disableLog) {
print("\x1b[2K\r", false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ANSI escape sequences are hard to read. Ideally you'd define them in a constant. Something like:

const CLEAR_LINE = "\x1b[2K";

(I don't actually know what that ANSI sequence means, I'm just giving an example)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used to erase the the current line: http://ascii-table.com/ansi-escape-sequences-vt-100.php

}
print(`${RED_FAILED} ${name}`);
print(err.stack);
stats.failed++;
if (exitOnFail) {
break;
Expand All @@ -199,6 +293,7 @@ export interface RunOptions {
exitOnFail?: boolean;
only?: RegExp;
skip?: RegExp;
disableLog?: boolean;
}

/**
Expand All @@ -209,7 +304,8 @@ export async function runTests({
parallel = false,
exitOnFail = false,
only = /[^\s]/,
skip = /^\s*$/
skip = /^\s*$/,
disableLog = false
}: RunOptions = {}): Promise<void> {
const tests: TestDefinition[] = candidates.filter(
({ name }): boolean => only.test(name) && !skip.test(name)
Expand All @@ -222,13 +318,24 @@ export async function runTests({
failed: 0
};
const results: TestResults = createTestResults(tests);
console.log(`running ${tests.length} tests`);
print(`running ${tests.length} tests`);
const start = performance.now();
if (Deno.args.includes("--quiet")) {
disableLog = true;
}
if (disableLog) {
disableConsole();
}
if (parallel) {
await runTestsParallel(stats, results, tests, exitOnFail);
} else {
await runTestsSerial(stats, tests, exitOnFail);
await runTestsSerial(stats, tests, exitOnFail, disableLog);
}
const end = performance.now();
if (disableLog) {
enableConsole();
}
printResults(stats, results, parallel, exitOnFail);
printResults(stats, results, parallel, exitOnFail, end - start);
if (stats.failed) {
// Use setTimeout to avoid the error being ignored due to unhandled
// promise rejections being swallowed.
Expand Down