@stryker-mutator/core
Version:
The extendable JavaScript mutation testing framework
160 lines • 7.99 kB
JavaScript
import { EOL } from 'os';
import { requireResolve } from '@stryker-mutator/util';
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
import { DryRunStatus, TestStatus, } from '@stryker-mutator/api/test-runner';
import { lastValueFrom, of } from 'rxjs';
import { coreTokens } from '../di/index.js';
import { createTestRunnerFactory } from '../test-runner/index.js';
import { MutationTestReportHelper } from '../reporters/mutation-test-report-helper.js';
import { ConfigError } from '../errors.js';
import { createTestRunnerPool, } from '../concurrent/index.js';
import { FileMatcher } from '../config/index.js';
import { IncrementalDiffer, MutantTestPlanner, TestCoverage, } from '../mutants/index.js';
import { objectUtils } from '../utils/object-utils.js';
import { IdGenerator } from '../child-proxy/id-generator.js';
const INITIAL_TEST_RUN_MARKER = 'Initial test run';
function isFailedTest(testResult) {
return testResult.status === TestStatus.Failed;
}
export class DryRunExecutor {
injector;
log;
options;
timer;
concurrencyTokenProvider;
sandbox;
reporter;
static inject = tokens(commonTokens.injector, commonTokens.logger, commonTokens.options, coreTokens.timer, coreTokens.concurrencyTokenProvider, coreTokens.sandbox, coreTokens.reporter);
constructor(injector, log, options, timer, concurrencyTokenProvider, sandbox, reporter) {
this.injector = injector;
this.log = log;
this.options = options;
this.timer = timer;
this.concurrencyTokenProvider = concurrencyTokenProvider;
this.sandbox = sandbox;
this.reporter = reporter;
}
async execute() {
const testRunnerInjector = this.injector
.provideClass(coreTokens.workerIdGenerator, IdGenerator)
.provideFactory(coreTokens.testRunnerFactory, createTestRunnerFactory)
.provideValue(coreTokens.testRunnerConcurrencyTokens, this.concurrencyTokenProvider.testRunnerToken$)
.provideFactory(coreTokens.testRunnerPool, createTestRunnerPool);
const testRunnerPool = testRunnerInjector.resolve(coreTokens.testRunnerPool);
const { result, timing } = await lastValueFrom(testRunnerPool.schedule(of(0), (testRunner) => this.executeDryRun(testRunner)));
this.logInitialTestRunSucceeded(result.tests, timing);
if (!result.tests.length && !this.options.allowEmpty) {
throw new ConfigError('No tests were executed. Stryker will exit prematurely. Please check your configuration.');
}
return testRunnerInjector
.provideValue(coreTokens.timeOverheadMS, timing.overhead)
.provideValue(coreTokens.dryRunResult, result)
.provideValue(coreTokens.requireFromCwd, requireResolve)
.provideFactory(coreTokens.testCoverage, TestCoverage.from)
.provideClass(coreTokens.incrementalDiffer, IncrementalDiffer)
.provideClass(coreTokens.mutantTestPlanner, MutantTestPlanner)
.provideClass(coreTokens.mutationTestReportHelper, MutationTestReportHelper)
.provideClass(coreTokens.workerIdGenerator, IdGenerator);
}
validateResultCompleted(runResult) {
switch (runResult.status) {
case DryRunStatus.Complete: {
const failedTests = runResult.tests.filter(isFailedTest);
if (failedTests.length) {
this.logFailedTestsInInitialRun(failedTests);
throw new ConfigError('There were failed tests in the initial test run.');
}
return;
}
case DryRunStatus.Error:
this.logErrorsInInitialRun(runResult);
break;
case DryRunStatus.Timeout:
this.logTimeoutInitialRun();
break;
}
throw new Error('Something went wrong in the initial test run');
}
async executeDryRun(testRunner) {
if (this.options.dryRunOnly) {
this.log.info('Note: running the dry-run only. No mutations will be tested.');
}
const dryRunTimeout = this.options.dryRunTimeoutMinutes * 1000 * 60;
const project = this.injector.resolve(coreTokens.project);
const dryRunFiles = objectUtils.map(project.filesToMutate, (_, name) => this.sandbox.sandboxFileFor(name));
this.timer.mark(INITIAL_TEST_RUN_MARKER);
this.log.info(`Starting initial test run (${this.options.testRunner} test runner with "${this.options.coverageAnalysis}" coverage analysis). This may take a while.`);
this.log.debug(`Using timeout of ${dryRunTimeout} ms.`);
const result = await testRunner.dryRun({
timeout: dryRunTimeout,
coverageAnalysis: this.options.coverageAnalysis,
disableBail: this.options.disableBail,
files: dryRunFiles,
});
const grossTimeMS = this.timer.elapsedMs(INITIAL_TEST_RUN_MARKER);
const capabilities = await testRunner.capabilities();
this.validateResultCompleted(result);
this.remapSandboxFilesToOriginalFiles(result);
const timing = this.calculateTiming(grossTimeMS, result.tests);
const dryRunCompleted = { result, timing, capabilities };
this.reporter.onDryRunCompleted(dryRunCompleted);
return dryRunCompleted;
}
/**
* Remaps test files to their respective original names outside the sandbox.
* @param dryRunResult the completed result
*/
remapSandboxFilesToOriginalFiles(dryRunResult) {
const disableTypeCheckingFileMatcher = new FileMatcher(this.options.disableTypeChecks);
dryRunResult.tests.forEach((test) => {
if (test.fileName) {
test.fileName = this.sandbox.originalFileFor(test.fileName);
// HACK line numbers of the tests can be offset by 1 because the disable type checks preprocessor could have added a `// @ts-nocheck` line.
// We correct for that here if needed
// If we do more complex stuff in sandbox preprocessing in the future, we might want to add a robust remapping logic
if (test.startPosition &&
disableTypeCheckingFileMatcher.matches(test.fileName)) {
test.startPosition.line--;
}
}
});
}
logInitialTestRunSucceeded(tests, timing) {
if (!tests.length) {
this.log.info('No tests were found');
return;
}
this.log.info('Initial test run succeeded. Ran %s tests in %s (net %s ms, overhead %s ms).', tests.length, this.timer.humanReadableElapsed(INITIAL_TEST_RUN_MARKER), timing.net, timing.overhead);
}
/**
* Calculates the timing variables for the test run.
* grossTime = NetTime + overheadTime
*
* The overhead time is used to calculate exact timeout values during mutation testing.
* See timeoutMS setting in README for more information on this calculation
*/
calculateTiming(grossTimeMS, tests) {
const netTimeMS = tests.reduce((total, test) => total + test.timeSpentMs, 0);
const overheadTimeMS = grossTimeMS - netTimeMS;
return {
net: netTimeMS,
overhead: overheadTimeMS < 0 ? 0 : overheadTimeMS,
};
}
logFailedTestsInInitialRun(failedTests) {
let message = 'One or more tests failed in the initial test run:';
failedTests.forEach((test) => {
message += `${EOL}\t${test.name}`;
message += `${EOL}\t\t${test.failureMessage}`;
});
this.log.error(message);
}
logErrorsInInitialRun(runResult) {
const message = `One or more tests resulted in an error:${EOL}\t${runResult.errorMessage}`;
this.log.error(message);
}
logTimeoutInitialRun() {
this.log.error('Initial test run timed out!');
}
}
//# sourceMappingURL=3-dry-run-executor.js.map