UNPKG

@stryker-mutator/core

Version:

The extendable JavaScript mutation testing framework

160 lines 7.99 kB
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