UNPKG

@stryker-mutator/core

Version:

The extendable JavaScript mutation testing framework

202 lines 10.3 kB
import path from 'path'; import { PlanKind, MutantStatus } from '@stryker-mutator/api/core'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { notEmpty, split } from '@stryker-mutator/util'; import { coreTokens } from '../di/index.js'; import { objectUtils } from '../utils/object-utils.js'; import { optionsPath } from '../utils/index.js'; import { toRelativeNormalizedFileName } from './incremental-differ.js'; /** * The factor by which hit count from dry run is multiplied to calculate the hit limit for a mutant. * This is intentionally a high value to prevent false positives. * * For example, a property testing library might execute a failing scenario multiple times to determine the smallest possible counterexample. * @see https://jsverify.github.io/#minimal-counterexample */ const HIT_LIMIT_FACTOR = 100; /** * Responsible for determining the tests to execute for each mutant, as well as other run option specific details * */ export class MutantTestPlanner { constructor(testCoverage, incrementalDiffer, reporter, sandbox, project, timeOverheadMS, options, logger) { this.testCoverage = testCoverage; this.incrementalDiffer = incrementalDiffer; this.reporter = reporter; this.sandbox = sandbox; this.project = project; this.timeOverheadMS = timeOverheadMS; this.options = options; this.logger = logger; this.timeSpentAllTests = calculateTotalTime(this.testCoverage.testsById.values()); } async makePlan(mutants) { const mutantsDiff = await this.incrementalDiff(mutants); const mutantPlans = mutantsDiff.map((mutant) => this.planMutant(mutant)); this.reporter.onMutationTestingPlanReady({ mutantPlans }); this.warnAboutSlow(mutantPlans); return mutantPlans; } planMutant(mutant) { var _a; const isStatic = this.testCoverage.hasStaticCoverage(mutant.id); if (mutant.status) { // If this mutant was already ignored, early result return this.createMutantEarlyResultPlan(mutant, { isStatic, coveredBy: mutant.coveredBy, killedBy: mutant.killedBy, status: mutant.status, statusReason: mutant.statusReason, }); } else if (this.testCoverage.hasCoverage) { // If there was coverage information (coverageAnalysis not "off") const tests = (_a = this.testCoverage.testsByMutantId.get(mutant.id)) !== null && _a !== void 0 ? _a : []; const coveredBy = toTestIds(tests); if (!isStatic || (this.options.ignoreStatic && coveredBy.length)) { // If not static, or it was "hybrid" (both static and perTest coverage) and ignoreStatic is on. // Only run covered tests with mutant active during runtime const netTime = calculateTotalTime(tests); return this.createMutantRunPlan(mutant, { netTime, coveredBy, isStatic, testFilter: coveredBy }); } else if (this.options.ignoreStatic) { // Static (w/o perTest coverage) and ignoreStatic is on -> Ignore. return this.createMutantEarlyResultPlan(mutant, { status: MutantStatus.Ignored, statusReason: 'Static mutant (and "ignoreStatic" was enabled)', isStatic, coveredBy, }); } else { // Static (or hybrid) and `ignoreStatic` is off -> run all tests return this.createMutantRunPlan(mutant, { netTime: this.timeSpentAllTests, isStatic, coveredBy }); } } else { // No coverage information exists, all tests need to run return this.createMutantRunPlan(mutant, { netTime: this.timeSpentAllTests }); } } createMutantEarlyResultPlan(mutant, { isStatic, status, statusReason, coveredBy, killedBy, }) { return { plan: PlanKind.EarlyResult, mutant: { ...mutant, status, static: isStatic, statusReason, coveredBy, killedBy, }, }; } createMutantRunPlan(mutant, { netTime, testFilter, isStatic, coveredBy, }) { const { disableBail, timeoutMS, timeoutFactor } = this.options; const timeout = timeoutFactor * netTime + timeoutMS + this.timeOverheadMS; const hitCount = this.testCoverage.hitsByMutantId.get(mutant.id); const hitLimit = hitCount === undefined ? undefined : hitCount * HIT_LIMIT_FACTOR; return { plan: PlanKind.Run, netTime, mutant: { ...mutant, coveredBy, static: isStatic, }, runOptions: { // Copy over relevant mutant fields, we don't want to copy over "static" and "coveredBy", test runners should only care about the testFilter activeMutant: { id: mutant.id, fileName: mutant.fileName, location: mutant.location, mutatorName: mutant.mutatorName, replacement: mutant.replacement, }, mutantActivation: testFilter ? 'runtime' : 'static', timeout, testFilter, sandboxFileName: this.sandbox.sandboxFileFor(mutant.fileName), hitLimit, disableBail, reloadEnvironment: !testFilter, }, }; } warnAboutSlow(mutantPlans) { if (!this.options.ignoreStatic && objectUtils.isWarningEnabled('slow', this.options.warnings)) { // Only warn when the estimated time to run all static mutants exceeds 40% // ... and when the average performance impact of a static mutant is estimated to be twice that (or more) of a non-static mutant const ABSOLUTE_CUT_OFF_PERUNAGE = 0.4; const RELATIVE_CUT_OFF_FACTOR = 2; const zeroIfNaN = (n) => (isNaN(n) ? 0 : n); const totalNetTime = (runPlans) => runPlans.reduce((acc, { netTime }) => acc + netTime, 0); const runPlans = mutantPlans.filter(isRunPlan); const [staticRunPlans, runTimeRunPlans] = split(runPlans, ({ mutant }) => Boolean(mutant.static)); const estimatedTimeForStaticMutants = totalNetTime(staticRunPlans); const estimatedTimeForRunTimeMutants = totalNetTime(runTimeRunPlans); const estimatedTotalTime = estimatedTimeForRunTimeMutants + estimatedTimeForStaticMutants; const avgTimeForAStaticMutant = zeroIfNaN(estimatedTimeForStaticMutants / staticRunPlans.length); const avgTimeForARunTimeMutant = zeroIfNaN(estimatedTimeForRunTimeMutants / runTimeRunPlans.length); const relativeTimeForStaticMutants = estimatedTimeForStaticMutants / estimatedTotalTime; const absoluteCondition = relativeTimeForStaticMutants >= ABSOLUTE_CUT_OFF_PERUNAGE; const relativeCondition = avgTimeForAStaticMutant >= RELATIVE_CUT_OFF_FACTOR * avgTimeForARunTimeMutant; if (relativeCondition && absoluteCondition) { const percentage = (perunage) => Math.round(perunage * 100); this.logger.warn(`Detected ${staticRunPlans.length} static mutants (${percentage(staticRunPlans.length / runPlans.length)}% of total) that are estimated to take ${percentage(relativeTimeForStaticMutants)}% of the time running the tests!\n You might want to enable "ignoreStatic" to ignore these static mutants for your next run. \n For more information about static mutants visit: https://stryker-mutator.io/docs/mutation-testing-elements/static-mutants.\n (disable "${optionsPath('warnings', 'slow')}" to ignore this warning)`); } } } async incrementalDiff(currentMutants) { var _a; const { incrementalReport } = this.project; if (incrementalReport) { const currentFiles = await this.readAllOriginalFiles(currentMutants, this.testCoverage.testsById.values(), Object.keys(incrementalReport.files), Object.keys((_a = incrementalReport.testFiles) !== null && _a !== void 0 ? _a : {})); const diffedMutants = this.incrementalDiffer.diff(currentMutants, this.testCoverage, incrementalReport, currentFiles); return diffedMutants; } return currentMutants; } async readAllOriginalFiles(...thingsWithFileNamesOrFileNames) { const uniqueFileNames = [ ...new Set(thingsWithFileNamesOrFileNames .flatMap((container) => [...container].map((thing) => (typeof thing === 'string' ? thing : thing.fileName))) .filter(notEmpty) .map((fileName) => path.resolve(fileName))), ]; const result = await Promise.all(uniqueFileNames.map(async (fileName) => { var _a; const originalContent = await ((_a = this.project.files.get(fileName)) === null || _a === void 0 ? void 0 : _a.readOriginal()); if (originalContent) { return [toRelativeNormalizedFileName(fileName), originalContent]; } else { return undefined; } })); return new Map(result.filter(notEmpty)); } } MutantTestPlanner.inject = tokens(coreTokens.testCoverage, coreTokens.incrementalDiffer, coreTokens.reporter, coreTokens.sandbox, coreTokens.project, coreTokens.timeOverheadMS, commonTokens.options, commonTokens.logger); function calculateTotalTime(testResults) { let total = 0; for (const test of testResults) { total += test.timeSpentMs; } return total; } function toTestIds(testResults) { const result = []; for (const test of testResults) { result.push(test.id); } return result; } export function isEarlyResult(mutantPlan) { return mutantPlan.plan === PlanKind.EarlyResult; } export function isRunPlan(mutantPlan) { return mutantPlan.plan === PlanKind.Run; } //# sourceMappingURL=mutant-test-planner.js.map