UNPKG

@stryker-mutator/core

Version:

The extendable JavaScript mutation testing framework

144 lines 5.32 kB
import { exec } from 'child_process'; import os from 'os'; import { INSTRUMENTER_CONSTANTS, } from '@stryker-mutator/api/core'; import { TestStatus, DryRunStatus, toMutantRunResult, } from '@stryker-mutator/api/test-runner'; import { errorToString, testFilesProvided } from '@stryker-mutator/util'; import { objectUtils } from '../utils/object-utils.js'; import { Timer } from '../utils/timer.js'; /** * A test runner that uses a (bash or cmd) command to execute the tests. * Does not know hom many tests are executed or any code coverage results, * instead, it mimics a simple test result based on the exit code. * The command can be configured, but defaults to `npm test`. */ export class CommandTestRunner { workingDir; /** * "command" */ static runnerName = CommandTestRunner.name .replace('TestRunner', '') .toLowerCase(); /** * Determines whether a given name is "command" (ignore case) * @param name Maybe "command", maybe not */ static is(name) { return this.runnerName === name.toLowerCase(); } settings; testFilesProvided; timeoutHandler; constructor(workingDir, options) { this.workingDir = workingDir; this.settings = options.commandRunner; this.testFilesProvided = testFilesProvided(options); } capabilities() { // Can reload, because each call is a new process. return { reloadEnvironment: true }; } async init() { if (this.testFilesProvided) { throw new Error(`The ${CommandTestRunner.runnerName} test runner does not support the --testFiles option.`); } return Promise.resolve(); } async dryRun(_options) { return this.run({}); } async mutantRun({ activeMutant, }) { const result = await this.run({ activeMutantId: activeMutant.id }); return toMutantRunResult(result); } run({ activeMutantId, }) { const timerInstance = new Timer(); return new Promise((res, rej) => { const output = []; const env = activeMutantId === undefined ? process.env : { ...process.env, [INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE]: activeMutantId, }; const childProcess = exec(this.settings.command, { cwd: this.workingDir, env, }); childProcess.on('error', (error) => { objectUtils .kill(childProcess.pid) .then(() => handleResolve(errorResult(error))) .catch(rej); }); childProcess.on('exit', (code) => { const result = completeResult(code, timerInstance); handleResolve(result); }); childProcess.stdout.on('data', (chunk) => { output.push(chunk); }); childProcess.stderr.on('data', (chunk) => { output.push(chunk); }); this.timeoutHandler = async () => { handleResolve({ status: DryRunStatus.Timeout }); await objectUtils.kill(childProcess.pid); }; const handleResolve = (runResult) => { removeAllListeners(); this.timeoutHandler = undefined; res(runResult); }; function removeAllListeners() { childProcess.stderr.removeAllListeners(); childProcess.stdout.removeAllListeners(); childProcess.removeAllListeners(); } function errorResult(error) { return { errorMessage: errorToString(error), status: DryRunStatus.Error, }; } function completeResult(exitCode, timer) { const duration = timer.elapsedMs(); if (exitCode === 0) { return { status: DryRunStatus.Complete, tests: [ { id: 'all', name: 'All tests', status: TestStatus.Success, timeSpentMs: duration, }, ], }; } else { return { status: DryRunStatus.Complete, tests: [ { id: 'all', failureMessage: output .map((buf) => buf.toString()) .join(os.EOL), name: 'All tests', status: TestStatus.Failed, timeSpentMs: duration, }, ], }; } } }); } async dispose() { if (this.timeoutHandler) { await this.timeoutHandler(); } } } //# sourceMappingURL=command-test-runner.js.map