@stryker-mutator/mocha-runner
Version:
A plugin to use the mocha test runner in Stryker, the JavaScript mutation testing framework
153 lines • 6.41 kB
JavaScript
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
import { escapeRegExp } from '@stryker-mutator/util';
import { DryRunStatus, toMutantRunResult, determineHitLimitReached, } from '@stryker-mutator/api/test-runner';
import { StrykerMochaReporter } from './stryker-mocha-reporter.js';
import * as pluginTokens from './plugin-tokens.js';
export class MochaTestRunner {
log;
options;
loader;
mochaAdapter;
mocha;
instrumenterContext;
originalGrep;
beforeEach;
static inject = tokens(commonTokens.logger, commonTokens.options, pluginTokens.loader, pluginTokens.mochaAdapter, pluginTokens.globalNamespace);
loadedEnv = false;
constructor(log, options, loader, mochaAdapter, globalNamespace) {
this.log = log;
this.options = options;
this.loader = loader;
this.mochaAdapter = mochaAdapter;
StrykerMochaReporter.log = log;
this.instrumenterContext = global[globalNamespace] ?? (global[globalNamespace] = {});
}
capabilities() {
return Promise.resolve({
// Mocha directly uses `import`, so reloading files once they are loaded is impossible
reloadEnvironment: false,
});
}
async init() {
const mochaOptions = this.loader.load(this.options);
const testFileNames = this.mochaAdapter.collectFiles(mochaOptions);
let rootHooks;
if (mochaOptions.require) {
if (mochaOptions.require.includes('esm')) {
throw new Error('Config option "mochaOptions.require" does not support "esm", please use `"testRunnerNodeArgs": ["--require", "esm"]` instead. See https://github.com/stryker-mutator/stryker-js/issues/3014 for more information.');
}
rootHooks = await this.mochaAdapter.handleRequires(mochaOptions.require);
}
this.mocha = this.mochaAdapter.create({
reporter: StrykerMochaReporter,
timeout: 0,
rootHooks,
});
this.mocha.cleanReferencesAfterRun(false);
testFileNames.forEach((fileName) => this.mocha.addFile(fileName));
this.setIfDefined(mochaOptions['async-only'], (asyncOnly) => asyncOnly && this.mocha.asyncOnly());
this.setIfDefined(mochaOptions.ui, this.mocha.ui);
this.setIfDefined(mochaOptions.grep, this.mocha.grep);
this.originalGrep = mochaOptions.grep;
// Bind beforeEach, so we can use that for perTest code coverage in dry run
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
this.mocha.suite.beforeEach(function () {
self.beforeEach?.(this);
});
}
setIfDefined(value, operation) {
if (typeof value !== 'undefined') {
operation.apply(this.mocha, [value]);
}
}
async dryRun({ coverageAnalysis, disableBail }) {
if (coverageAnalysis === 'perTest') {
this.beforeEach = (context) => {
this.instrumenterContext.currentTestId = context.currentTest?.fullTitle();
};
}
const runResult = await this.run(disableBail);
if (runResult.status === DryRunStatus.Complete && coverageAnalysis !== 'off') {
runResult.mutantCoverage = this.instrumenterContext.mutantCoverage;
}
delete this.beforeEach;
return runResult;
}
async mutantRun({ activeMutant, testFilter, disableBail, hitLimit, mutantActivation }) {
this.instrumenterContext.hitLimit = hitLimit;
this.instrumenterContext.hitCount = hitLimit ? 0 : undefined;
if (testFilter) {
const metaRegExp = testFilter.map((testId) => `(^${escapeRegExp(testId)}$)`).join('|');
const regex = new RegExp(metaRegExp);
this.mocha.grep(regex);
}
else {
this.setIfDefined(this.originalGrep, this.mocha.grep);
}
const dryRunResult = await this.run(disableBail, activeMutant.id, mutantActivation);
return toMutantRunResult(dryRunResult);
}
async run(disableBail, activeMutantId, mutantActivation) {
setBail(!disableBail, this.mocha.suite);
try {
if (!this.loadedEnv) {
this.instrumenterContext.activeMutant = mutantActivation === 'static' ? activeMutantId : undefined;
// Loading files Async is needed to support native esm modules
// See https://mochajs.org/api/mocha#loadFilesAsync
await this.mocha.loadFilesAsync();
this.loadedEnv = true;
}
this.instrumenterContext.activeMutant = activeMutantId;
await this.runMocha();
const reporter = StrykerMochaReporter.currentInstance;
if (reporter) {
const timeoutResult = determineHitLimitReached(this.instrumenterContext.hitCount, this.instrumenterContext.hitLimit);
if (timeoutResult) {
return timeoutResult;
}
const result = {
status: DryRunStatus.Complete,
tests: reporter.tests,
};
return result;
}
else {
const errorMessage = `Mocha didn't instantiate the ${StrykerMochaReporter.name} correctly. Test result cannot be reported.`;
this.log.error(errorMessage);
return {
status: DryRunStatus.Error,
errorMessage,
};
}
}
catch (errorMessage) {
return {
errorMessage,
status: DryRunStatus.Error,
};
}
function setBail(bail, suite) {
suite.bail(bail);
suite.suites.forEach((childSuite) => setBail(bail, childSuite));
}
}
// eslint-disable-next-line @typescript-eslint/require-await
async dispose() {
try {
this.mocha?.dispose();
}
catch (err) {
if (err?.code !== 'ERR_MOCHA_INSTANCE_ALREADY_RUNNING') {
// Oops, didn't mean to catch this one
throw err;
}
}
}
async runMocha() {
return new Promise((res) => {
this.mocha.run(() => res());
});
}
}
//# sourceMappingURL=mocha-test-runner.js.map