UNPKG

7.98 kBPlain TextView Raw
1import { from, partition, merge, Observable, lastValueFrom, EMPTY, concat, bufferTime, mergeMap } from 'rxjs';
2import { toArray, map, shareReplay, tap } from 'rxjs/operators';
3import { tokens, commonTokens } from '@stryker-mutator/api/plugin';
4import { MutantResult, MutantStatus, Mutant, StrykerOptions, PlanKind, MutantTestPlan, MutantRunPlan } from '@stryker-mutator/api/core';
5import { TestRunner } from '@stryker-mutator/api/test-runner';
6import { Logger } from '@stryker-mutator/api/logging';
7import { I } from '@stryker-mutator/util';
8import { CheckStatus } from '@stryker-mutator/api/check';
9
10import { coreTokens } from '../di/index.js';
11import { StrictReporter } from '../reporters/strict-reporter.js';
12import { MutationTestReportHelper } from '../reporters/mutation-test-report-helper.js';
13import { Timer } from '../utils/timer.js';
14import { ConcurrencyTokenProvider, Pool } from '../concurrent/index.js';
15import { isEarlyResult, MutantTestPlanner } from '../mutants/index.js';
16import { CheckerFacade } from '../checker/index.js';
17
18import { DryRunContext } from './3-dry-run-executor.js';
19
20export interface MutationTestContext extends DryRunContext {
21 [coreTokens.testRunnerPool]: I<Pool<TestRunner>>;
22 [coreTokens.timeOverheadMS]: number;
23 [coreTokens.mutationTestReportHelper]: MutationTestReportHelper;
24 [coreTokens.mutantTestPlanner]: MutantTestPlanner;
25}
26
27const CHECK_BUFFER_MS = 10_000;
28
29/**
30 * Sorting the tests just before running them can yield a significant performance boost,
31 * because it can reduce the number of times a test runner process needs to be recreated.
32 * However, we need to buffer the results in order to be able to sort them.
33 *
34 * This value is very low, since it would halt the test execution otherwise.
35 * @see https://github.com/stryker-mutator/stryker-js/issues/3462
36 */
37const BUFFER_FOR_SORTING_MS = 0;
38
39export class MutationTestExecutor {
40 public static inject = tokens(
41 coreTokens.reporter,
42 coreTokens.testRunnerPool,
43 coreTokens.checkerPool,
44 coreTokens.mutants,
45 coreTokens.mutantTestPlanner,
46 coreTokens.mutationTestReportHelper,
47 commonTokens.logger,
48 commonTokens.options,
49 coreTokens.timer,
50 coreTokens.concurrencyTokenProvider
51 );
52
53 constructor(
54 private readonly reporter: StrictReporter,
55 private readonly testRunnerPool: I<Pool<TestRunner>>,
56 private readonly checkerPool: I<Pool<I<CheckerFacade>>>,
57 private readonly mutants: readonly Mutant[],
58 private readonly planner: MutantTestPlanner,
59 private readonly mutationTestReportHelper: I<MutationTestReportHelper>,
60 private readonly log: Logger,
61 private readonly options: StrykerOptions,
62 private readonly timer: I<Timer>,
63 private readonly concurrencyTokenProvider: I<ConcurrencyTokenProvider>
64 ) {}
65
66 public async execute(): Promise<MutantResult[]> {
67 if (this.options.dryRunOnly) {
68 this.log.info('The dry-run has been completed successfully. No mutations have been executed.');
69 return [];
70 }
71
72 const mutantTestPlans = await this.planner.makePlan(this.mutants);
73 const { earlyResult$, runMutant$ } = this.executeEarlyResult(from(mutantTestPlans));
74 const { passedMutant$, checkResult$ } = this.executeCheck(runMutant$);
75 const { coveredMutant$, noCoverageResult$ } = this.executeNoCoverage(passedMutant$);
76 const testRunnerResult$ = this.executeRunInTestRunner(coveredMutant$);
77 const results = await lastValueFrom(merge(testRunnerResult$, checkResult$, noCoverageResult$, earlyResult$).pipe(toArray()));
78 await this.mutationTestReportHelper.reportAll(results);
79 await this.reporter.wrapUp();
80 this.logDone();
81 return results;
82 }
83
84 private executeEarlyResult(input$: Observable<MutantTestPlan>) {
85 const [earlyResultMutants$, runMutant$] = partition(input$.pipe(shareReplay()), isEarlyResult);
86 const earlyResult$ = earlyResultMutants$.pipe(map(({ mutant }) => this.mutationTestReportHelper.reportMutantStatus(mutant, mutant.status)));
87 return { earlyResult$, runMutant$ };
88 }
89
90 private executeNoCoverage(input$: Observable<MutantRunPlan>) {
91 const [noCoverageMatchedMutant$, coveredMutant$] = partition(input$.pipe(shareReplay()), ({ runOptions }) => runOptions.testFilter?.length === 0);
92 const noCoverageResult$ = noCoverageMatchedMutant$.pipe(
93 map(({ mutant }) => this.mutationTestReportHelper.reportMutantStatus(mutant, MutantStatus.NoCoverage))
94 );
95 return { noCoverageResult$, coveredMutant$ };
96 }
97
98 private executeRunInTestRunner(input$: Observable<MutantRunPlan>): Observable<MutantResult> {
99 const sortedPlan$ = input$.pipe(
100 bufferTime(BUFFER_FOR_SORTING_MS),
101 mergeMap((plans) => plans.sort(reloadEnvironmentLast))
102 );
103 return this.testRunnerPool.schedule(sortedPlan$, async (testRunner, { mutant, runOptions }) => {
104 const result = await testRunner.mutantRun(runOptions);
105 return this.mutationTestReportHelper.reportMutantRunResult(mutant, result);
106 });
107 }
108
109 private logDone() {
110 this.log.info('Done in %s.', this.timer.humanReadableElapsed());
111 }
112
113 /**
114 * Checks mutants against all configured checkers (if any) and returns steams for failed checks and passed checks respectively
115 * @param input$ The mutant run plans to check
116 */
117 public executeCheck(input$: Observable<MutantRunPlan>): {
118 checkResult$: Observable<MutantResult>;
119 passedMutant$: Observable<MutantRunPlan>;
120 } {
121 let checkResult$: Observable<MutantResult> = EMPTY;
122 let passedMutant$ = input$;
123 for (const checkerName of this.options.checkers) {
124 // Use this checker
125 const [checkFailedResult$, checkPassedResult$] = partition(
126 this.executeSingleChecker(checkerName, passedMutant$).pipe(shareReplay()),
127 isEarlyResult
128 );
129
130 // Prepare for the next one
131 passedMutant$ = checkPassedResult$;
132 checkResult$ = concat(checkResult$, checkFailedResult$.pipe(map(({ mutant }) => mutant)));
133 }
134 return {
135 checkResult$,
136 passedMutant$: passedMutant$.pipe(
137 tap({
138 complete: async () => {
139 await this.checkerPool.dispose();
140 this.concurrencyTokenProvider.freeCheckers();
141 },
142 })
143 ),
144 };
145 }
146
147 /**
148 * Executes the check task for one checker
149 * @param checkerName The name of the checker to execute
150 * @param input$ The mutants tasks to check
151 * @returns An observable stream with early results (check failed) and passed results
152 */
153 private executeSingleChecker(checkerName: string, input$: Observable<MutantRunPlan>): Observable<MutantTestPlan> {
154 const group$ = this.checkerPool
155 .schedule(input$.pipe(bufferTime(CHECK_BUFFER_MS)), (checker, mutants) => checker.group(checkerName, mutants))
156 .pipe(mergeMap((mutantGroups) => mutantGroups));
157 const checkTask$ = this.checkerPool
158 .schedule(group$, (checker, group) => checker.check(checkerName, group))
159 .pipe(
160 mergeMap((mutantGroupResults) => mutantGroupResults),
161 map(([mutantRunPlan, checkResult]) =>
162 checkResult.status === CheckStatus.Passed
163 ? mutantRunPlan
164 : {
165 plan: PlanKind.EarlyResult as const,
166 mutant: this.mutationTestReportHelper.reportCheckFailed(mutantRunPlan.mutant, checkResult),
167 }
168 )
169 );
170 return checkTask$;
171 }
172}
173
174/**
175 * Sorting function that sorts mutant run plans that reload environments last.
176 * This can yield a significant performance boost, because it reduces the times a test runner process needs to restart.
177 * @see https://github.com/stryker-mutator/stryker-js/issues/3462
178 */
179function reloadEnvironmentLast(a: MutantRunPlan, b: MutantRunPlan): number {
180 if (a.plan === PlanKind.Run && b.plan === PlanKind.Run) {
181 if (a.runOptions.reloadEnvironment && !b.runOptions.reloadEnvironment) {
182 return 1;
183 }
184 if (!a.runOptions.reloadEnvironment && b.runOptions.reloadEnvironment) {
185 return -1;
186 }
187 return 0;
188 }
189 return 0;
190}