@stryker-mutator/tap-runner
Version:
A plugin to use the TAP (test anything protocol) test runner in Stryker, the JavaScript mutation testing framework
136 lines • 5.65 kB
JavaScript
import childProcess from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs/promises';
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
import { determineHitLimitReached, DryRunStatus, TestStatus, toMutantRunResult, } from '@stryker-mutator/api/test-runner';
import { INSTRUMENTER_CONSTANTS } from '@stryker-mutator/api/core';
import { normalizeFileName } from '@stryker-mutator/util';
import * as pluginTokens from './plugin-tokens.js';
import { findTestyLookingFiles, captureTapResult, buildArguments } from './tap-helper.js';
import { strykerHitLimit, strykerNamespace, strykerDryRun, tempTapOutputFileName } from './setup/env.cjs';
export function createTapTestRunnerFactory(namespace = INSTRUMENTER_CONSTANTS.NAMESPACE) {
createTapTestRunner.inject = tokens(commonTokens.injector);
function createTapTestRunner(injector) {
return injector.provideValue(pluginTokens.globalNamespace, namespace).injectClass(TapTestRunner);
}
return createTapTestRunner;
}
export const createTapTestRunner = createTapTestRunnerFactory();
class HitLimitError extends Error {
result;
constructor(result) {
super(result.reason);
this.result = result;
}
}
export class TapTestRunner {
log;
globalNamespace;
static inject = tokens(commonTokens.options, commonTokens.logger, pluginTokens.globalNamespace);
testFiles = [];
static hookFile = normalizeFileName(path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'setup', 'hook.cjs'));
options;
constructor(options, log, globalNamespace) {
this.log = log;
this.globalNamespace = globalNamespace;
this.options = options;
}
capabilities() {
return { reloadEnvironment: true };
}
async init() {
this.testFiles = await findTestyLookingFiles(this.options.tap.testFiles);
}
async dryRun(options) {
return this.run({ disableBail: options.disableBail, dryRun: true });
}
async mutantRun(options) {
return toMutantRunResult(await this.run({ disableBail: options.disableBail, activeMutant: options.activeMutant.id, hitLimit: options.hitLimit }, options.testFilter));
}
async run(testOptions, testFilter) {
const testFiles = testFilter ?? this.testFiles;
const runs = [];
const totalCoverage = {
static: {},
perTest: {},
};
for (const testFile of testFiles) {
try {
const { testResult, coverage } = await this.runFile(testFile, testOptions);
runs.push(testResult);
totalCoverage.perTest[testFile] = coverage?.static ?? {};
if (testResult.status !== TestStatus.Success && !testOptions.disableBail) {
break;
}
}
catch (err) {
if (err instanceof HitLimitError) {
return err.result;
}
else if (err instanceof Error) {
return {
status: DryRunStatus.Error,
errorMessage: `Error running file "${testFile}". ${err.message}`,
};
}
throw err;
}
}
return {
status: DryRunStatus.Complete,
tests: runs,
mutantCoverage: totalCoverage,
};
}
async runFile(testFile, testOptions) {
const env = {
...process.env,
[strykerHitLimit]: testOptions.hitLimit?.toString(),
[strykerNamespace]: this.globalNamespace,
[INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE]: testOptions.activeMutant,
[strykerDryRun]: testOptions.dryRun?.toString(),
};
const args = buildArguments(this.options.tap.nodeArgs, TapTestRunner.hookFile, testFile);
if (this.log.isDebugEnabled()) {
this.log.debug(`Running: \`node ${args.map((arg) => `"${arg}"`).join(' ')}\` in ${process.cwd()}`);
}
const now = () => new Date().getTime();
const before = now();
const tapProcess = childProcess.spawn('node', args, { env });
const result = await captureTapResult(tapProcess, !testOptions.disableBail && this.options.tap.forceBail);
const timeSpentMs = now() - before;
const fileName = tempTapOutputFileName(tapProcess.pid);
const fileContent = await fs.readFile(fileName, 'utf-8');
await fs.rm(fileName);
const file = JSON.parse(fileContent);
const hitLimitReached = determineHitLimitReached(file.hitCount, testOptions.hitLimit);
if (hitLimitReached) {
throw new HitLimitError(hitLimitReached);
}
return { testResult: this.tapResultToTestResult(testFile, result, timeSpentMs), coverage: file.mutantCoverage };
}
tapResultToTestResult(fileName, { result, failedTests }, timeSpentMs) {
const generic = {
id: fileName,
name: fileName,
timeSpentMs: result.time ?? timeSpentMs,
fileName: fileName,
startPosition: undefined,
};
if (result.ok) {
return {
...generic,
status: TestStatus.Success,
};
}
else {
return {
...generic,
status: TestStatus.Failed,
failureMessage: failedTests.map((f) => `${f.fullname}: ${f.name}`).join(', ') ?? 'Unknown issue',
};
}
}
}
//# sourceMappingURL=tap-test-runner.js.map