1// @ts-check 2const Mocha = require("mocha"); 3const path = require("path"); 4const fs = require("fs"); 5const os = require("os"); 6 7const failingHookRegExp = /^(.*) "(before|after) (all|each)" hook$/; 8 9/** 10 * .failed-tests reporter 11 * 12 * @typedef {Object} ReporterOptions 13 * @property {string} [file] 14 * @property {boolean} [keepFailed] 15 * @property {string|Mocha.ReporterConstructor} [reporter] 16 * @property {*} [reporterOptions] 17 */ 18class FailedTestsReporter extends Mocha.reporters.Base { 19 /** 20 * @param {Mocha.Runner} runner 21 * @param {{ reporterOptions?: ReporterOptions }} [options] 22 */ 23 constructor(runner, options) { 24 super(runner, options); 25 if (!runner) return; 26 27 const reporterOptions = this.reporterOptions = options.reporterOptions || {}; 28 if (reporterOptions.file === undefined) reporterOptions.file = ".failed-tests"; 29 if (reporterOptions.keepFailed === undefined) reporterOptions.keepFailed = false; 30 if (reporterOptions.reporter) { 31 /** @type {Mocha.ReporterConstructor} */ 32 let reporter; 33 if (typeof reporterOptions.reporter === "function") { 34 reporter = reporterOptions.reporter; 35 } 36 else if (Mocha.reporters[reporterOptions.reporter]) { 37 reporter = Mocha.reporters[reporterOptions.reporter]; 38 } 39 else { 40 try { 41 reporter = require(reporterOptions.reporter); 42 } 43 catch (_) { 44 reporter = require(path.resolve(process.cwd(), reporterOptions.reporter)); 45 } 46 } 47 48 const newOptions = Object.assign({}, options, { reporterOptions: reporterOptions.reporterOptions || {} }); 49 if (reporterOptions.reporter === "xunit") { 50 newOptions.reporterOptions.output = "TEST-results.xml"; 51 } 52 this.reporter = new reporter(runner, newOptions); 53 } 54 55 /** @type {Mocha.Test[]} */ 56 this.passes = []; 57 58 /** @type {(Mocha.Test)[]} */ 59 this.failures = []; 60 61 runner.on("pass", test => this.passes.push(test)); 62 runner.on("fail", test => this.failures.push(test)); 63 } 64 65 /** 66 * @param {string} file 67 * @param {ReadonlyArray<Mocha.Test>} passes 68 * @param {ReadonlyArray<Mocha.Test | Mocha.Hook>} failures 69 * @param {boolean} keepFailed 70 * @param {(err?: NodeJS.ErrnoException) => void} done 71 */ 72 static writeFailures(file, passes, failures, keepFailed, done) { 73 const failingTests = new Set(fs.existsSync(file) ? readTests() : undefined); 74 const possiblyPassingSuites = /**@type {Set<string>}*/(new Set()); 75 76 // Remove tests that are now passing and track suites that are now 77 // possibly passing. 78 if (failingTests.size > 0 && !keepFailed) { 79 for (const test of passes) { 80 failingTests.delete(test.fullTitle().trim()); 81 possiblyPassingSuites.add(test.parent.fullTitle().trim()); 82 } 83 } 84 85 // Add tests that are now failing. If a hook failed, track its 86 // containing suite as failing. If the suite for a test or hook was 87 // possibly passing then it is now definitely failing. 88 for (const test of failures) { 89 const suiteTitle = test.parent.fullTitle().trim(); 90 if (test.type === "test") { 91 failingTests.add(test.fullTitle().trim()); 92 } 93 else { 94 failingTests.add(suiteTitle); 95 } 96 possiblyPassingSuites.delete(suiteTitle); 97 } 98 99 // Remove all definitely passing suites. 100 for (const suite of possiblyPassingSuites) { 101 failingTests.delete(suite); 102 } 103 104 if (failingTests.size > 0) { 105 const failed = Array 106 .from(failingTests) 107 .sort() 108 .join(os.EOL); 109 fs.writeFile(file, failed, "utf8", done); 110 } 111 else if (!keepFailed && fs.existsSync(file)) { 112 fs.unlink(file, done); 113 } 114 else { 115 done(); 116 } 117 118 function readTests() { 119 return fs.readFileSync(file, "utf8") 120 .split(/\r?\n/g) 121 .map(line => line.trim()) 122 .filter(line => line.length > 0); 123 } 124 } 125 126 /** 127 * @param {number} failures 128 * @param {(failures: number) => void} [fn] 129 */ 130 done(failures, fn) { 131 FailedTestsReporter.writeFailures(this.reporterOptions.file, this.passes, this.failures, this.reporterOptions.keepFailed || this.stats.tests === 0, (err) => { 132 const reporter = this.reporter; 133 if (reporter && reporter.done) { 134 reporter.done(failures, fn); 135 } 136 else if (fn) { 137 fn(failures); 138 } 139 140 if (err) console.error(err); 141 }); 142 } 143} 144 145module.exports = FailedTestsReporter;