1 | import path from 'path';
|
2 | import { MutantStatus } from '@stryker-mutator/api/core';
|
3 | import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
|
4 | import { normalizeFileName, normalizeWhitespaces } from '@stryker-mutator/util';
|
5 | import { calculateMutationTestMetrics } from 'mutation-testing-metrics';
|
6 | import { MutantRunStatus } from '@stryker-mutator/api/test-runner';
|
7 | import { CheckStatus } from '@stryker-mutator/api/check';
|
8 | import { strykerVersion } from '../stryker-package.js';
|
9 | import { coreTokens } from '../di/index.js';
|
10 | import { objectUtils } from '../utils/object-utils.js';
|
11 | const STRYKER_FRAMEWORK = Object.freeze({
|
12 | name: 'StrykerJS',
|
13 | version: strykerVersion,
|
14 | branding: {
|
15 | homepageUrl: 'https://stryker-mutator.io',
|
16 | imageUrl: "data:image/svg+xml;utf8,%3Csvg viewBox='0 0 1458 1458' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd' clip-rule='evenodd' stroke-linejoin='round' stroke-miterlimit='2'%3E%3Cpath fill='none' d='M0 0h1458v1458H0z'/%3E%3CclipPath id='a'%3E%3Cpath d='M0 0h1458v1458H0z'/%3E%3C/clipPath%3E%3Cg clip-path='url(%23a)'%3E%3Cpath d='M1458 729c0 402.655-326.345 729-729 729S0 1131.655 0 729C0 326.445 326.345 0 729 0s729 326.345 729 729' fill='%23e74c3c' fill-rule='nonzero'/%3E%3Cpath d='M778.349 1456.15L576.6 1254.401l233-105 85-78.668v-64.332l-257-257-44-187-50-208 251.806-82.793L1076.6 389.401l380.14 379.15c-19.681 367.728-311.914 663.049-678.391 687.599z' fill-opacity='.3'/%3E%3Cpath d='M753.4 329.503c41.79 0 74.579 7.83 97.925 25.444 23.571 18.015 41.69 43.956 55.167 77.097l11.662 28.679 165.733-58.183-14.137-32.13c-26.688-60.655-64.896-108.61-114.191-144.011-49.329-35.423-117.458-54.302-204.859-54.302-50.78 0-95.646 7.376-134.767 21.542-40.093 14.671-74.09 34.79-102.239 60.259-28.84 26.207-50.646 57.06-65.496 92.701-14.718 35.052-22.101 72.538-22.101 112.401 0 72.536 20.667 133.294 61.165 182.704 38.624 47.255 98.346 88.037 179.861 121.291 42.257 17.475 78.715 33.125 109.227 46.994 27.193 12.361 49.294 26.124 66.157 41.751 15.309 14.186 26.497 30.584 33.63 49.258 7.721 20.214 11.16 45.69 11.16 76.402 0 28.021-4.251 51.787-13.591 71.219-8.832 18.374-20.171 33.178-34.523 44.219-14.787 11.374-31.193 19.591-49.393 24.466-19.68 5.359-39.14 7.993-58.69 7.993-29.359 0-54.387-3.407-75.182-10.747-20.112-7.013-37.144-16.144-51.259-27.486-13.618-11.009-24.971-23.766-33.744-38.279-9.64-15.8-17.272-31.924-23.032-48.408l-10.965-31.376-161.669 60.585 10.734 30.124c10.191 28.601 24.197 56.228 42.059 82.748 18.208 27.144 41.322 51.369 69.525 72.745 27.695 21.075 60.904 38.218 99.481 51.041 37.777 12.664 82.004 19.159 132.552 19.159 49.998 0 95.818-8.321 137.611-24.622 42.228-16.471 78.436-38.992 108.835-67.291 30.719-28.597 54.631-62.103 71.834-100.642 17.263-38.56 25.923-79.392 25.923-122.248 0-54.339-8.368-100.37-24.208-138.32-16.29-38.759-38.252-71.661-65.948-98.797-26.965-26.418-58.269-48.835-93.858-67.175-33.655-17.241-69.196-33.11-106.593-47.533-35.934-13.429-65.822-26.601-89.948-39.525-22.153-11.868-40.009-24.21-53.547-37.309-11.429-11.13-19.83-23.678-24.718-37.664-5.413-15.49-7.98-33.423-7.98-53.577 0-40.883 11.293-71.522 37.086-90.539 28.443-20.825 64.985-30.658 109.311-30.658z' fill='%23f1c40f' fill-rule='nonzero'/%3E%3Cpath d='M720 0h18v113h-18zM1458 738v-18h-113v18h113zM720 1345h18v113h-18zM113 738v-18H0v18h113z'/%3E%3C/g%3E%3C/svg%3E",
|
17 | },
|
18 | });
|
19 |
|
20 |
|
21 |
|
22 | export class MutationTestReportHelper {
|
23 | constructor(reporter, options, project, log, testCoverage, fs, requireFromCwd) {
|
24 | this.reporter = reporter;
|
25 | this.options = options;
|
26 | this.project = project;
|
27 | this.log = log;
|
28 | this.testCoverage = testCoverage;
|
29 | this.fs = fs;
|
30 | this.requireFromCwd = requireFromCwd;
|
31 | }
|
32 | reportCheckFailed(mutant, checkResult) {
|
33 | return this.reportOne({
|
34 | ...mutant,
|
35 | status: this.checkStatusToResultStatus(checkResult.status),
|
36 | statusReason: checkResult.reason,
|
37 | });
|
38 | }
|
39 | reportMutantStatus(mutant, status) {
|
40 | return this.reportOne({
|
41 | ...mutant,
|
42 | status,
|
43 | });
|
44 | }
|
45 | reportMutantRunResult(mutant, result) {
|
46 |
|
47 | switch (result.status) {
|
48 | case MutantRunStatus.Error:
|
49 | return this.reportOne({
|
50 | ...mutant,
|
51 | status: MutantStatus.RuntimeError,
|
52 | statusReason: result.errorMessage,
|
53 | });
|
54 | case MutantRunStatus.Killed:
|
55 | return this.reportOne({
|
56 | ...mutant,
|
57 | status: MutantStatus.Killed,
|
58 | testsCompleted: result.nrOfTests,
|
59 | killedBy: result.killedBy,
|
60 | statusReason: result.failureMessage,
|
61 | });
|
62 | case MutantRunStatus.Timeout:
|
63 | return this.reportOne({
|
64 | ...mutant,
|
65 | status: MutantStatus.Timeout,
|
66 | statusReason: result.reason,
|
67 | });
|
68 | case MutantRunStatus.Survived:
|
69 | return this.reportOne({
|
70 | ...mutant,
|
71 | status: MutantStatus.Survived,
|
72 | testsCompleted: result.nrOfTests,
|
73 | });
|
74 | }
|
75 | }
|
76 | reportOne(result) {
|
77 | this.reporter.onMutantTested(result);
|
78 | return result;
|
79 | }
|
80 | checkStatusToResultStatus(status) {
|
81 | switch (status) {
|
82 | case CheckStatus.CompileError:
|
83 | return MutantStatus.CompileError;
|
84 | }
|
85 | }
|
86 | async reportAll(results) {
|
87 | const report = await this.mutationTestReport(results);
|
88 | const metrics = calculateMutationTestMetrics(report);
|
89 | this.reporter.onAllMutantsTested(results);
|
90 | this.reporter.onMutationTestReportReady(report, metrics);
|
91 | if (this.options.incremental) {
|
92 | await this.fs.mkdir(path.dirname(this.options.incrementalFile), { recursive: true });
|
93 | await this.fs.writeFile(this.options.incrementalFile, JSON.stringify(report, null, 2), 'utf-8');
|
94 | }
|
95 | this.determineExitCode(metrics);
|
96 | }
|
97 | determineExitCode(metrics) {
|
98 | const mutationScore = metrics.systemUnderTestMetrics.metrics.mutationScore;
|
99 | const breaking = this.options.thresholds.break;
|
100 | const formattedScore = mutationScore.toFixed(2);
|
101 | if (typeof breaking === 'number') {
|
102 | if (mutationScore < breaking) {
|
103 | this.log.error(`Final mutation score ${formattedScore} under breaking threshold ${breaking}, setting exit code to 1 (failure).`);
|
104 | this.log.info('(improve mutation score or set `thresholds.break = null` to prevent this error in the future)');
|
105 | objectUtils.setExitCode(1);
|
106 | }
|
107 | else {
|
108 | this.log.info(`Final mutation score of ${formattedScore} is greater than or equal to break threshold ${breaking}`);
|
109 | }
|
110 | }
|
111 | else {
|
112 | this.log.debug("No breaking threshold configured. Won't fail the build no matter how low your mutation score is. Set `thresholds.break` to change this behavior.");
|
113 | }
|
114 | }
|
115 | async mutationTestReport(results) {
|
116 |
|
117 |
|
118 |
|
119 | const testIdMap = new Map([...this.testCoverage.testsById.values()].map((test, index) => [test.id, index.toString()]));
|
120 | const remapTestId = (id) => { var _a; return (_a = testIdMap.get(id)) !== null && _a !== void 0 ? _a : id; };
|
121 | const remapTestIds = (ids) => ids === null || ids === void 0 ? void 0 : ids.map(remapTestId);
|
122 | return {
|
123 | files: await this.toFileResults(results, remapTestIds),
|
124 | schemaVersion: '1.0',
|
125 | thresholds: this.options.thresholds,
|
126 | testFiles: await this.toTestFiles(remapTestId),
|
127 | projectRoot: process.cwd(),
|
128 | config: this.options,
|
129 | framework: {
|
130 | ...STRYKER_FRAMEWORK,
|
131 | dependencies: this.discoverDependencies(),
|
132 | },
|
133 | };
|
134 | }
|
135 | async toFileResults(results, remapTestIds) {
|
136 | const fileResultsByName = new Map(await Promise.all([...new Set(results.map(({ fileName }) => fileName))].map(async (fileName) => [fileName, await this.toFileResult(fileName)])));
|
137 | return results.reduce((acc, mutantResult) => {
|
138 | var _a;
|
139 | const reportFileName = normalizeReportFileName(mutantResult.fileName);
|
140 | const fileResult = (_a = acc[reportFileName]) !== null && _a !== void 0 ? _a : (acc[reportFileName] = fileResultsByName.get(mutantResult.fileName));
|
141 | fileResult.mutants.push(this.toMutantResult(mutantResult, remapTestIds));
|
142 | return acc;
|
143 | }, {});
|
144 | }
|
145 | async toTestFiles(remapTestId) {
|
146 | const testFilesByName = new Map(await Promise.all([...new Set([...this.testCoverage.testsById.values()].map(({ fileName }) => fileName))].map(async (fileName) => [normalizeReportFileName(fileName), await this.toTestFile(fileName)])));
|
147 | return [...this.testCoverage.testsById.values()].reduce((acc, testResult) => {
|
148 | var _a;
|
149 | const test = this.toTestDefinition(testResult, remapTestId);
|
150 | const reportFileName = normalizeReportFileName(testResult.fileName);
|
151 | const testFile = (_a = acc[reportFileName]) !== null && _a !== void 0 ? _a : (acc[reportFileName] = testFilesByName.get(reportFileName));
|
152 | testFile.tests.push(test);
|
153 | return acc;
|
154 | }, {});
|
155 | }
|
156 | async toFileResult(fileName) {
|
157 | const fileResult = {
|
158 | language: this.determineLanguage(fileName),
|
159 | mutants: [],
|
160 | source: '',
|
161 | };
|
162 | const sourceFile = this.project.files.get(fileName);
|
163 | if (sourceFile) {
|
164 | fileResult.source = await sourceFile.readOriginal();
|
165 | }
|
166 | else {
|
167 | this.log.warn(normalizeWhitespaces(`File "${fileName}" not found
|
168 | in input files, but did receive mutant result for it. This shouldn't happen`));
|
169 | }
|
170 | return fileResult;
|
171 | }
|
172 | async toTestFile(fileName) {
|
173 | const testFile = { tests: [] };
|
174 | if (fileName) {
|
175 | const file = this.project.files.get(fileName);
|
176 | if (file) {
|
177 | testFile.source = await file.readOriginal();
|
178 | }
|
179 | else {
|
180 | this.log.warn(normalizeWhitespaces(`Test file "${fileName}" not found
|
181 | in input files, but did receive test result for it. This shouldn't happen.`));
|
182 | }
|
183 | }
|
184 | return testFile;
|
185 | }
|
186 | toTestDefinition(test, remapTestId) {
|
187 | return {
|
188 | id: remapTestId(test.id),
|
189 | name: test.name,
|
190 | location: test.startPosition ? { start: this.toPosition(test.startPosition) } : undefined,
|
191 | };
|
192 | }
|
193 | determineLanguage(name) {
|
194 | const ext = path.extname(name).toLowerCase();
|
195 | switch (ext) {
|
196 | case '.ts':
|
197 | case '.tsx':
|
198 | return 'typescript';
|
199 | case '.html':
|
200 | case '.vue':
|
201 | return 'html';
|
202 | default:
|
203 | return 'javascript';
|
204 | }
|
205 | }
|
206 | toMutantResult(mutantResult, remapTestIds) {
|
207 | const { fileName, location, killedBy, coveredBy, ...apiMutant } = mutantResult;
|
208 | return {
|
209 | ...apiMutant,
|
210 | killedBy: remapTestIds(killedBy),
|
211 | coveredBy: remapTestIds(coveredBy),
|
212 | location: this.toLocation(location),
|
213 | };
|
214 | }
|
215 | toLocation(location) {
|
216 | return {
|
217 | end: this.toPosition(location.end),
|
218 | start: this.toPosition(location.start),
|
219 | };
|
220 | }
|
221 | toPosition(pos) {
|
222 | return {
|
223 | column: pos.column + 1,
|
224 | line: pos.line + 1,
|
225 | };
|
226 | }
|
227 | discoverDependencies() {
|
228 | const discover = (specifier) => {
|
229 | try {
|
230 | return [specifier, this.requireFromCwd(`${specifier}/package.json`).version];
|
231 | }
|
232 | catch {
|
233 |
|
234 | return undefined;
|
235 | }
|
236 | };
|
237 | const dependencies = [
|
238 | '@stryker-mutator/mocha-runner',
|
239 | '@stryker-mutator/karma-runner',
|
240 | '@stryker-mutator/jasmine-runner',
|
241 | '@stryker-mutator/jest-runner',
|
242 | '@stryker-mutator/typescript-checker',
|
243 | 'karma',
|
244 | 'karma-chai',
|
245 | 'karma-chrome-launcher',
|
246 | 'karma-jasmine',
|
247 | 'karma-mocha',
|
248 | 'mocha',
|
249 | 'jasmine',
|
250 | 'jasmine-core',
|
251 | 'jest',
|
252 | 'react-scripts',
|
253 | 'typescript',
|
254 | '@angular/cli',
|
255 | 'webpack',
|
256 | 'webpack-cli',
|
257 | 'ts-jest',
|
258 | ];
|
259 | return dependencies.map(discover).reduce((acc, dependency) => {
|
260 | if (dependency) {
|
261 | acc[dependency[0]] = dependency[1];
|
262 | }
|
263 | return acc;
|
264 | }, {});
|
265 | }
|
266 | }
|
267 | MutationTestReportHelper.inject = tokens(coreTokens.reporter, commonTokens.options, coreTokens.project, commonTokens.logger, coreTokens.testCoverage, coreTokens.fs, coreTokens.requireFromCwd);
|
268 | function normalizeReportFileName(fileName) {
|
269 | if (fileName) {
|
270 | return normalizeFileName(path.relative(process.cwd(), fileName));
|
271 | }
|
272 |
|
273 | return '';
|
274 | }
|
275 |
|
\ | No newline at end of file |