UNPKG

13.7 kBJavaScriptView Raw
1import path from 'path';
2import { MutantStatus } from '@stryker-mutator/api/core';
3import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
4import { normalizeFileName, normalizeWhitespaces } from '@stryker-mutator/util';
5import { calculateMutationTestMetrics } from 'mutation-testing-metrics';
6import { MutantRunStatus } from '@stryker-mutator/api/test-runner';
7import { CheckStatus } from '@stryker-mutator/api/check';
8import { strykerVersion } from '../stryker-package.js';
9import { coreTokens } from '../di/index.js';
10import { objectUtils } from '../utils/object-utils.js';
11const 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 * A helper class to convert and report mutants that survived or get killed
21 */
22export 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 // Prune fields used for Stryker bookkeeping
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 // Mocha, jest and karma use test titles as test ids.
117 // This can mean a lot of duplicate strings in the json report.
118 // Therefore we remap the test ids here to numbers.
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 // package does not exist...
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}
267MutationTestReportHelper.inject = tokens(coreTokens.reporter, commonTokens.options, coreTokens.project, commonTokens.logger, coreTokens.testCoverage, coreTokens.fs, coreTokens.requireFromCwd);
268function normalizeReportFileName(fileName) {
269 if (fileName) {
270 return normalizeFileName(path.relative(process.cwd(), fileName));
271 }
272 // File name is not required for test files. By default we accumulate tests under the '' key
273 return '';
274}
275//# sourceMappingURL=mutation-test-report-helper.js.map
\No newline at end of file