From c1de95f91f5ef62a8f6b01a93312678da4642e39 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Fri, 14 Aug 2020 07:28:35 -0700 Subject: [PATCH] feat(testrunner): pretty error messages (#3469) --- package-lock.json | 37 ++++++++++++++++++ package.json | 4 ++ test/runner/dotReporter.js | 77 +++++++++++++++++++++++++++++++++++++- test/runner/runner.js | 15 +++++--- 4 files changed, 126 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa329aedc6..83f0a39325 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6783,6 +6783,33 @@ "has-flag": "^3.0.0" } }, + "supports-hyperlinks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "table": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", @@ -6826,6 +6853,16 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, "terser": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", diff --git a/package.json b/package.json index 659342f8de..2cf23684b2 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "ws": "^6.1.0" }, "devDependencies": { + "@babel/code-frame": "^7.10.4", "@babel/core": "^7.10.3", "@babel/preset-env": "^7.10.3", "@babel/preset-typescript": "^7.10.1", @@ -59,6 +60,7 @@ "@types/progress": "^2.0.3", "@types/proxy-from-env": "^1.0.0", "@types/rimraf": "^2.0.2", + "@types/stack-utils": "^1.0.1", "@types/ws": "^6.0.1", "@typescript-eslint/eslint-plugin": "^2.6.1", "@typescript-eslint/parser": "^2.6.1", @@ -79,6 +81,8 @@ "pixelmatch": "^4.0.2", "socksv5": "0.0.6", "source-map-support": "^0.5.19", + "stack-utils": "^2.0.2", + "terminal-link": "^2.1.1", "text-diff": "^1.0.1", "ts-loader": "^6.1.2", "typescript": "^3.8.3", diff --git a/test/runner/dotReporter.js b/test/runner/dotReporter.js index 0511d3245c..f39eaa8d26 100644 --- a/test/runner/dotReporter.js +++ b/test/runner/dotReporter.js @@ -17,13 +17,20 @@ const Base = require('mocha/lib/reporters/base'); const constants = require('mocha/lib/runner').constants; const colors = require('colors/safe'); - +const milliseconds = require('ms'); +const { codeFrameColumns } = require('@babel/code-frame'); +const path = require('path'); +const fs = require('fs'); +const os = require('os'); +const terminalLink = require('terminal-link'); +const StackUtils = require('stack-utils'); +const stackUtils = new StackUtils(); class DotReporter extends Base { constructor(runner, options) { super(runner, options); process.on('SIGINT', async () => { - Base.list(this.failures); + this.epilogue(); process.exit(130); }); @@ -47,6 +54,72 @@ class DotReporter extends Base { this.epilogue(); }); } + + epilogue() { + console.log(''); + + console.log(colors.green(` ${this.stats.passes || 0} passing`) + colors.dim(` (${milliseconds(this.stats.duration)})`)); + + if (this.stats.pending) + console.log(colors.yellow(` ${this.stats.pending} skipped`)); + + if (this.stats.failures) { + console.log(colors.red(` ${this.stats.failures} failing`)); + console.log(''); + this.failures.forEach((failure, index) => { + const relativePath = path.relative(process.cwd(), failure.file); + const header = ` ${index +1}. ${terminalLink(relativePath, `file://${os.hostname()}${failure.file}`)} › ${failure.title}`; + console.log(colors.bold.red(header)); + const stack = failure.err.stack; + if (stack) { + console.log(''); + const messageLocation = failure.err.stack.indexOf(failure.err.message); + const preamble = failure.err.stack.substring(0, messageLocation + failure.err.message.length); + console.log(indent(preamble, ' ')); + const position = positionInFile(stack, failure.file); + if (position) { + const source = fs.readFileSync(failure.file, 'utf8'); + console.log(''); + console.log(indent(codeFrameColumns(source, { + start: position, + }, + { highlightCode: true} + ), ' ')); + } + console.log(''); + console.log(indent(colors.dim(stack.substring(preamble.length + 1)), ' ')); + } else { + console.log(''); + console.log(indent(String(failure.err), ' ')); + } + console.log(''); + }); + } + } +} + +/** + * @param {string} lines + * @param {string} tab + */ +function indent(lines, tab) { + return lines.replace(/^/gm, tab); +} + +/** + * @param {string} stack + * @param {string} file + * @return {{column: number, line: number}} + */ +function positionInFile(stack, file) { + for (const line of stack.split('\n')) { + const parsed = stackUtils.parseLine(line); + if (!parsed) + continue; + if (path.resolve(process.cwd(), parsed.file) === file) + return {column: parsed.column, line: parsed.line}; + } + return null; } module.exports = DotReporter; diff --git a/test/runner/runner.js b/test/runner/runner.js index 4eb301f8cb..dad8051f45 100644 --- a/test/runner/runner.js +++ b/test/runner/runner.js @@ -100,12 +100,14 @@ class Runner extends EventEmitter { this.stats.passes += params.stats.passes; this.stats.pending += params.stats.pending; this.stats.tests += params.stats.tests; - if (params.error) - this._restartWorker(worker); - else - this._workerAvailable(worker); if (this._runCompleteCallback && !this._pendingJobs) this._runCompleteCallback(); + else { + if (params.error) + this._restartWorker(worker); + else + this._workerAvailable(worker); + } }); } @@ -180,7 +182,10 @@ class Worker extends EventEmitter { this.process = child_process.fork(path.join(__dirname, 'worker.js'), { detached: false, - env: process.env, + env: { + FORCE_COLOR: process.stdout.isTTY ? 1 : 0, + ...process.env + }, stdio: ['ignore', 'pipe', 'pipe', 'ipc'] }); this.process.on('exit', () => this.emit('exit'));