@stryker-mutator/core
Version:
The extendable JavaScript mutation testing framework
152 lines • 7.45 kB
JavaScript
import { from, partition, merge, lastValueFrom, EMPTY, concat, bufferTime, mergeMap, } from 'rxjs';
import { toArray, map, shareReplay, tap } from 'rxjs/operators';
import { tokens, commonTokens } from '@stryker-mutator/api/plugin';
import { PlanKind, } from '@stryker-mutator/api/core';
import { CheckStatus } from '@stryker-mutator/api/check';
import { coreTokens } from '../di/index.js';
import { isEarlyResult } from '../mutants/index.js';
const CHECK_BUFFER_MS = 10_000;
/**
* Sorting the tests just before running them can yield a significant performance boost,
* because it can reduce the number of times a test runner process needs to be recreated.
* However, we need to buffer the results in order to be able to sort them.
*
* This value is very low, since it would halt the test execution otherwise.
* @see https://github.com/stryker-mutator/stryker-js/issues/3462
*/
const BUFFER_FOR_SORTING_MS = 0;
export class MutationTestExecutor {
reporter;
testRunnerPool;
checkerPool;
mutants;
planner;
mutationTestReportHelper;
log;
options;
timer;
concurrencyTokenProvider;
dryRunResult;
static inject = tokens(coreTokens.reporter, coreTokens.testRunnerPool, coreTokens.checkerPool, coreTokens.mutants, coreTokens.mutantTestPlanner, coreTokens.mutationTestReportHelper, commonTokens.logger, commonTokens.options, coreTokens.timer, coreTokens.concurrencyTokenProvider, coreTokens.dryRunResult);
constructor(reporter, testRunnerPool, checkerPool, mutants, planner, mutationTestReportHelper, log, options, timer, concurrencyTokenProvider, dryRunResult) {
this.reporter = reporter;
this.testRunnerPool = testRunnerPool;
this.checkerPool = checkerPool;
this.mutants = mutants;
this.planner = planner;
this.mutationTestReportHelper = mutationTestReportHelper;
this.log = log;
this.options = options;
this.timer = timer;
this.concurrencyTokenProvider = concurrencyTokenProvider;
this.dryRunResult = dryRunResult;
}
async execute() {
if (this.options.dryRunOnly) {
this.log.info('The dry-run has been completed successfully. No mutations have been executed.');
return [];
}
if (this.dryRunResult.tests.length === 0 && this.options.allowEmpty) {
this.logDone();
return [];
}
const mutantTestPlans = await this.planner.makePlan(this.mutants);
const { earlyResult$, runMutant$ } = this.executeEarlyResult(from(mutantTestPlans));
const { passedMutant$, checkResult$ } = this.executeCheck(runMutant$);
const { coveredMutant$, noCoverageResult$ } = this.executeNoCoverage(passedMutant$);
const testRunnerResult$ = this.executeRunInTestRunner(coveredMutant$);
const results = await lastValueFrom(merge(testRunnerResult$, checkResult$, noCoverageResult$, earlyResult$).pipe(toArray()));
await this.mutationTestReportHelper.reportAll(results);
await this.reporter.wrapUp();
this.logDone();
return results;
}
executeEarlyResult(input$) {
const [earlyResultMutants$, runMutant$] = partition(input$.pipe(shareReplay()), isEarlyResult);
const earlyResult$ = earlyResultMutants$.pipe(map(({ mutant }) => this.mutationTestReportHelper.reportMutantStatus(mutant, mutant.status)));
return { earlyResult$, runMutant$ };
}
executeNoCoverage(input$) {
const [noCoverageMatchedMutant$, coveredMutant$] = partition(input$.pipe(shareReplay()), ({ runOptions }) => runOptions.testFilter?.length === 0);
const noCoverageResult$ = noCoverageMatchedMutant$.pipe(map(({ mutant }) => this.mutationTestReportHelper.reportMutantStatus(mutant, 'NoCoverage')));
return { noCoverageResult$, coveredMutant$ };
}
executeRunInTestRunner(input$) {
const sortedPlan$ = input$.pipe(bufferTime(BUFFER_FOR_SORTING_MS), mergeMap((plans) => plans.sort(reloadEnvironmentLast)));
return this.testRunnerPool.schedule(sortedPlan$, async (testRunner, { mutant, runOptions }) => {
const result = await testRunner.mutantRun(runOptions);
return this.mutationTestReportHelper.reportMutantRunResult(mutant, result);
});
}
logDone() {
this.log.info('Done in %s.', this.timer.humanReadableElapsed());
}
/**
* Checks mutants against all configured checkers (if any) and returns steams for failed checks and passed checks respectively
* @param input$ The mutant run plans to check
*/
executeCheck(input$) {
let checkResult$ = EMPTY;
let passedMutant$ = input$;
for (const checkerName of this.options.checkers) {
// Use this checker
const [checkFailedResult$, checkPassedResult$] = partition(this.executeSingleChecker(checkerName, passedMutant$).pipe(shareReplay()), isEarlyResult);
// Prepare for the next one
passedMutant$ = checkPassedResult$;
checkResult$ = concat(checkResult$, checkFailedResult$.pipe(map(({ mutant }) => mutant)));
}
return {
checkResult$,
passedMutant$: passedMutant$.pipe(tap({
complete: () => {
this.checkerPool
.dispose()
.then(() => {
this.concurrencyTokenProvider.freeCheckers();
})
.catch((error) => {
this.log.error('An error occurred while disposing checkers: %s', error);
});
},
})),
};
}
/**
* Executes the check task for one checker
* @param checkerName The name of the checker to execute
* @param input$ The mutants tasks to check
* @returns An observable stream with early results (check failed) and passed results
*/
executeSingleChecker(checkerName, input$) {
const group$ = this.checkerPool
.schedule(input$.pipe(bufferTime(CHECK_BUFFER_MS)), (checker, mutants) => checker.group(checkerName, mutants))
.pipe(mergeMap((mutantGroups) => mutantGroups));
const checkTask$ = this.checkerPool
.schedule(group$, (checker, group) => checker.check(checkerName, group))
.pipe(mergeMap((mutantGroupResults) => mutantGroupResults), map(([mutantRunPlan, checkResult]) => checkResult.status === CheckStatus.Passed
? mutantRunPlan
: {
plan: PlanKind.EarlyResult,
mutant: this.mutationTestReportHelper.reportCheckFailed(mutantRunPlan.mutant, checkResult),
}));
return checkTask$;
}
}
/**
* Sorting function that sorts mutant run plans that reload environments last.
* This can yield a significant performance boost, because it reduces the times a test runner process needs to restart.
* @see https://github.com/stryker-mutator/stryker-js/issues/3462
*/
function reloadEnvironmentLast(a, b) {
if (a.plan === PlanKind.Run && b.plan === PlanKind.Run) {
if (a.runOptions.reloadEnvironment && !b.runOptions.reloadEnvironment) {
return 1;
}
if (!a.runOptions.reloadEnvironment && b.runOptions.reloadEnvironment) {
return -1;
}
return 0;
}
return 0;
}
//# sourceMappingURL=4-mutation-test-executor.js.map