blob: b508e7d9ee152eb89c601becce4c6ac062c4dfb2 [file] [log] [blame] [edit]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import mocha from 'mocha';
import * as fs from 'fs';
type SuiteChild = Suite | Test;
interface Suite {
name: string;
children: SuiteChild[],
tests: number;
failures: number;
skipped: number;
}
interface Test {
name: string;
speed?: string;
duration?: number;
status: 'pass' | 'fail' | 'pending';
err?: any; // mocha's .d.ts doesn't define the weird structure of mocha's errors
}
export default class TestReporter extends mocha.reporters.Base {
out: fs.WriteStream;
constructor(runner: mocha.Runner, options: mocha.MochaOptions) {
super(runner, options);
const outputName = options?.reporterOptions?.output;
this.out = fs.createWriteStream(outputName);
const suiteName = options?.reporterOptions?.suiteName ?? 'Mocha Tests';
const suiteStack: Suite[] = [
];
let currentSuite: Suite = { // root
name: suiteName,
children: [],
tests: 0,
failures: 0,
skipped: 0,
};
runner.on('suite', (suite) => {
if (suite.root) { return; }
suiteStack.push(currentSuite);
currentSuite = {
name: suite.title,
children: [],
tests: 0,
failures: 0,
skipped: 0,
};
});
runner.on('suite end', (suite: mocha.Suite) => {
if (suiteStack.length > 0) {
const parentSuite = suiteStack.pop() as Suite;
parentSuite.children.push(currentSuite);
parentSuite.tests += currentSuite.tests;
parentSuite.failures += currentSuite.failures;
parentSuite.skipped += currentSuite.skipped;
currentSuite = parentSuite;
}
});
runner.on('pending', (test: mocha.Test) => {
currentSuite.children.push({
name: test.title,
status: 'pending',
});
currentSuite.tests++;
currentSuite.skipped++;
});
runner.on('pass', (test: mocha.Test) => {
currentSuite.children.push({
name: test.title,
speed: test.speed,
duration: test.duration,
status: 'pass',
});
currentSuite.tests++;
});
runner.on('fail', (test: mocha.Test) => {
currentSuite.children.push({
name: test.title,
speed: test.speed,
duration: test.duration,
status: 'fail',
err: test.err,
});
currentSuite.tests++;
currentSuite.failures++;
});
runner.on('end', () => {
this.out.write('<testsuites>\n');
this.writeSuite(currentSuite);
this.out.write('</testsuites>\n');
this.out.end();
});
}
writeSuite(suite: Suite) {
this.out.write(`<testsuite name="${suite.name.replaceAll('"', '&quot;')}" failures="${suite.failures}" tests="${suite.tests}">\n`);
for (const child of suite.children) {
if ('status' in child) {
this.writeTest(child as Test);
} else {
this.writeSuite(child as Suite);
}
}
this.out.write('</testsuite>\n');
}
writeTest(test: Test) {
const status = test.status === 'pending' ? 'notrun' : 'run';
let result = 'completed';
if (test.status === 'pending') {
result = 'skipped';
}
this.out.write(`<testcase name="${test.name.replaceAll('"', '&quot;')}" time="${test.duration /* ms */}" status="${status}" result="${result}">\n`);
if (test.status === 'fail') {
let msg = test.err!.message;
if (test.err!.generatedMessage) {
// we've got an automatically generated message with a diff, make a
// shorter one -- we'll get the diff in expected vs actual
msg = `Expected ${test.err!.operator}`;
}
this.out.write(`<failure message="${msg}"><![CDATA[`);
this.out.write(`${test.err!.stack}\n`);
this.out.write(']]>');
if (test.err!.expected) {
this.out.write(`<expected><value>${test.err!.expected}</value></expected>\n`);
}
if (test.err!.actual) {
this.out.write(`<actual><value>${test.err!.expected}</value></actual>\n`);
}
this.out.write('</failure>\n');
}
this.out.write('</testcase>\n');
}
}