-
Notifications
You must be signed in to change notification settings - Fork 624
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
Changes from 11 commits
96665ee
2b4e084
1596368
dc37810
de47795
5f578c2
6aa91c5
a25a454
e696b88
11b3b37
217808d
04715c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"; | ||
} | ||
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. | ||
|
@@ -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; | ||
|
@@ -53,6 +108,7 @@ interface TestStats { | |
} | ||
|
||
interface TestResult { | ||
timeElapsed?: number; | ||
name: string; | ||
error?: Error; | ||
ok: boolean; | ||
|
@@ -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`; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
|
@@ -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()) { | ||
|
@@ -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` | ||
); | ||
} | ||
|
||
|
@@ -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; | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
@@ -199,6 +293,7 @@ export interface RunOptions { | |
exitOnFail?: boolean; | ||
only?: RegExp; | ||
skip?: RegExp; | ||
disableLog?: boolean; | ||
} | ||
|
||
/** | ||
|
@@ -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) | ||
|
@@ -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. | ||
|
There was a problem hiding this comment.
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