Adds failed test tracking

This commit is contained in:
Ron Buckton
2018-06-11 15:08:15 -07:00
parent 6c8ecc7386
commit 0944c29a47
8 changed files with 305 additions and 25 deletions

22
scripts/failed-tests.d.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
import Mocha = require("mocha");
export = FailedTestsReporter;
declare class FailedTestsReporter extends Mocha.reporters.Base {
passes: Mocha.Test[];
failures: Mocha.Test[];
reporterOptions: FailedTestsReporter.ReporterOptions;
reporter?: Mocha.reporters.Base;
constructor(runner: Mocha.Runner, options?: { reporterOptions?: FailedTestsReporter.ReporterOptions });
static writeFailures(file: string, passes: ReadonlyArray<Mocha.Test>, failures: ReadonlyArray<Mocha.Test>, keepFailed: boolean, done: (err?: NodeJS.ErrnoException) => void): void;
done(failures: number, fn?: (failures: number) => void): void;
}
declare namespace FailedTestsReporter {
interface ReporterOptions {
file?: string;
keepFailed?: boolean;
reporter?: string | Mocha.ReporterConstructor;
reporterOptions?: any;
}
}

117
scripts/failed-tests.js Normal file
View File

@@ -0,0 +1,117 @@
// @ts-check
const Mocha = require("mocha");
const path = require("path");
const fs = require("fs");
const os = require("os");
/**
* .failed-tests reporter
*
* @typedef {Object} ReporterOptions
* @property {string} [file]
* @property {boolean} [keepFailed]
* @property {string|Mocha.ReporterConstructor} [reporter]
* @property {*} [reporterOptions]
*/
class FailedTestsReporter extends Mocha.reporters.Base {
/**
* @param {Mocha.Runner} runner
* @param {{ reporterOptions?: ReporterOptions }} [options]
*/
constructor(runner, options) {
super(runner, options);
if (!runner) return;
const reporterOptions = this.reporterOptions = options.reporterOptions || {};
if (reporterOptions.file === undefined) reporterOptions.file = ".failed-tests";
if (reporterOptions.keepFailed === undefined) reporterOptions.keepFailed = false;
if (reporterOptions.reporter) {
/** @type {Mocha.ReporterConstructor} */
let reporter;
if (typeof reporterOptions.reporter === "function") {
reporter = reporterOptions.reporter;
}
else if (Mocha.reporters[reporterOptions.reporter]) {
reporter = Mocha.reporters[reporterOptions.reporter];
}
else {
try {
reporter = require(reporterOptions.reporter);
}
catch (_) {
reporter = require(path.resolve(process.cwd(), reporterOptions.reporter));
}
}
const newOptions = Object.assign({}, options, { reporterOptions: reporterOptions.reporterOptions || {} });
this.reporter = new reporter(runner, newOptions);
}
/** @type {Mocha.Test[]} */
this.passes = [];
/** @type {Mocha.Test[]} */
this.failures = [];
runner.on("pass", test => this.passes.push(test));
runner.on("fail", test => this.failures.push(test));
}
/**
* @param {string} file
* @param {ReadonlyArray<Mocha.Test>} passes
* @param {ReadonlyArray<Mocha.Test>} failures
* @param {boolean} keepFailed
* @param {(err?: NodeJS.ErrnoException) => void} done
*/
static writeFailures(file, passes, failures, keepFailed, done) {
const failingTests = new Set(fs.existsSync(file) ? readTests() : undefined);
if (failingTests.size > 0) {
for (const test of passes) {
const title = test.fullTitle().trim();
if (title) failingTests.delete(title);
}
}
for (const test of failures) {
const title = test.fullTitle().trim();
if (title) failingTests.add(title);
}
if (failingTests.size > 0) {
const failed = Array.from(failingTests).join(os.EOL);
fs.writeFile(file, failed, "utf8", done);
}
else if (!keepFailed) {
fs.unlink(file, done);
}
else {
done();
}
function readTests() {
return fs.readFileSync(file, "utf8")
.split(/\r?\n/g)
.map(line => line.trim())
.filter(line => line.length > 0);
}
}
/**
* @param {number} failures
* @param {(failures: number) => void} [fn]
*/
done(failures, fn) {
FailedTestsReporter.writeFailures(this.reporterOptions.file, this.passes, this.failures, this.reporterOptions.keepFailed || this.stats.tests === 0, (err) => {
const reporter = this.reporter;
if (reporter && reporter.done) {
reporter.done(failures, fn);
}
else if (fn) {
fn(failures);
}
if (err) console.error(err);
});
}
}
module.exports = FailedTestsReporter;

View File

@@ -0,0 +1,93 @@
const spawn = require('child_process').spawn;
const os = require("os");
const fs = require("fs");
const path = require("path");
let grep;
try {
const failedTests = fs.readFileSync(".failed-tests", "utf8");
grep = failedTests
.split(/\r?\n/g)
.map(test => test.trim())
.filter(test => test.length > 0)
.map(escapeRegExp);
}
catch (e) {
grep = [];
}
let args = [];
let waitForGrepValue = false;
let grepIndex = -1;
process.argv.slice(2).forEach((arg, index) => {
const [flag, value] = arg.split('=');
if (flag === "g" || flag === "grep") {
grepIndex = index - 1;
waitForGrepValue = arg !== flag;
if (!waitForGrepValue) grep.push(value.replace(/^"|"$/g, ""));
return;
}
if (waitForGrepValue) {
grep.push(arg.replace(/^"|"$/g, ""));
waitForGrepValue = false;
return;
}
args.push(arg);
});
let mocha = "./node_modules/mocha/bin/mocha";
let grepOption;
let grepOptionValue;
let grepFile;
if (grep.length) {
grepOption = "--grep";
grepOptionValue = grep.join("|");
if (grepOptionValue.length > 20) {
grepFile = path.resolve(os.tmpdir(), ".failed-tests.opts");
fs.writeFileSync(grepFile, `--grep ${grepOptionValue}`, "utf8");
grepOption = "--opts";
grepOptionValue = grepFile;
mocha = "./node_modules/mocha/bin/_mocha";
}
}
if (grepOption) {
if (grepIndex >= 0) {
args.splice(grepIndex, 0, grepOption, grepOptionValue);
}
else {
args.push(grepOption, grepOptionValue);
}
}
args.unshift(path.resolve(mocha));
console.log(args.join(" "));
const proc = spawn(process.execPath, args, {
stdio: 'inherit'
});
proc.on('exit', (code, signal) => {
process.on('exit', () => {
if (grepFile) {
fs.unlinkSync(grepFile);
}
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code);
}
});
});
// terminate children.
process.on('SIGINT', () => {
proc.kill('SIGINT'); // calls runner.abort()
proc.kill('SIGTERM'); // if that didn't work, we're probably in an infinite loop, so make it die.
});
function escapeRegExp(pattern) {
return pattern
.replace(/[^-\w\d\s]/g, match => "\\" + match)
.replace(/\s/g, "\\s");
}