1 | import { from, partition, merge, Observable, lastValueFrom, EMPTY, concat, bufferTime, mergeMap } from 'rxjs';
|
2 | import { toArray, map, shareReplay, tap } from 'rxjs/operators';
|
3 | import { tokens, commonTokens } from '@stryker-mutator/api/plugin';
|
4 | import { MutantResult, MutantStatus, Mutant, StrykerOptions, PlanKind, MutantTestPlan, MutantRunPlan } from '@stryker-mutator/api/core';
|
5 | import { TestRunner } from '@stryker-mutator/api/test-runner';
|
6 | import { Logger } from '@stryker-mutator/api/logging';
|
7 | import { I } from '@stryker-mutator/util';
|
8 | import { CheckStatus } from '@stryker-mutator/api/check';
|
9 |
|
10 | import { coreTokens } from '../di/index.js';
|
11 | import { StrictReporter } from '../reporters/strict-reporter.js';
|
12 | import { MutationTestReportHelper } from '../reporters/mutation-test-report-helper.js';
|
13 | import { Timer } from '../utils/timer.js';
|
14 | import { ConcurrencyTokenProvider, Pool } from '../concurrent/index.js';
|
15 | import { isEarlyResult, MutantTestPlanner } from '../mutants/index.js';
|
16 | import { CheckerFacade } from '../checker/index.js';
|
17 |
|
18 | import { DryRunContext } from './3-dry-run-executor.js';
|
19 |
|
20 | export interface MutationTestContext extends DryRunContext {
|
21 | [coreTokens.testRunnerPool]: I<Pool<TestRunner>>;
|
22 | [coreTokens.timeOverheadMS]: number;
|
23 | [coreTokens.mutationTestReportHelper]: MutationTestReportHelper;
|
24 | [coreTokens.mutantTestPlanner]: MutantTestPlanner;
|
25 | }
|
26 |
|
27 | const CHECK_BUFFER_MS = 10_000;
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 | const BUFFER_FOR_SORTING_MS = 0;
|
38 |
|
39 | export 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 |
|
115 |
|
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 |
|
125 | const [checkFailedResult$, checkPassedResult$] = partition(
|
126 | this.executeSingleChecker(checkerName, passedMutant$).pipe(shareReplay()),
|
127 | isEarlyResult
|
128 | );
|
129 |
|
130 |
|
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 |
|
149 |
|
150 |
|
151 |
|
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 |
|
176 |
|
177 |
|
178 |
|
179 | function 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 | }
|