@zohodesk/codestandard-validator 1.2.4-exp-5 → 1.2.4-exp-7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -1
- package/bin/cliCI.js +1 -0
- package/bin/zdcodequality.js +50 -0
- package/build/hooks/hook.js +3 -1
- package/build/mutation/_config.json +55 -0
- package/build/mutation/branchDiff.js +224 -149
- package/build/mutation/fileResolver.js +164 -133
- package/build/mutation/mutationCli.js +162 -100
- package/build/mutation/mutationRunner.js +216 -189
- package/build/mutation/reportGenerator.js +239 -51
- package/build/mutation/strykerConfigBuilder.js +327 -0
- package/build/mutation/strykerWrapper.js +107 -51
- package/build/utils/General/Config.js +4 -0
- package/build/utils/PluginsInstallation/checkIfPluginsAreInstalled.js +1 -0
- package/index.js +2 -1
- package/package.json +12 -6
- package/samples/sample-branch-mode.js +0 -34
- package/samples/sample-cli-entry.js +0 -34
- package/samples/sample-components.js +0 -63
- package/samples/sample-directory-mode.js +0 -30
- package/samples/sample-runner-direct.js +0 -32
- package/samples/sample-with-api.js +0 -44
|
@@ -2,27 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ReportGenerator — builds and writes mutation reports.
|
|
8
|
+
*
|
|
9
|
+
* Produces TWO files:
|
|
10
|
+
* 1. Mutation summary report (mutation-report.json)
|
|
11
|
+
* Contains mutation score, summary, file details — used by CI
|
|
12
|
+
* to decide pass/fail.
|
|
13
|
+
*
|
|
14
|
+
* 2. SonarQube external issues report (sonar-mutation-report.json)
|
|
15
|
+
* Compatible with sonar.externalIssuesReportPaths property.
|
|
16
|
+
* Reports survived and no-coverage mutants as issues so they
|
|
17
|
+
* show up inside SonarQube.
|
|
18
|
+
*
|
|
19
|
+
* Compatible with Node 16+.
|
|
20
|
+
*/
|
|
21
|
+
function ReportGenerator(options) {
|
|
22
|
+
options = options || {};
|
|
23
|
+
this._outputDir = options.outputDir || 'reports/mutation';
|
|
24
|
+
this._outputFileName = options.outputFileName || 'mutation-report.json';
|
|
25
|
+
this._sonarFileName = options.sonarFileName || 'sonar-mutation-report.json';
|
|
26
|
+
this._cwd = options.cwd || process.cwd();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ------------------------------------------------------------------ */
|
|
30
|
+
/* Mutation summary report */
|
|
31
|
+
/* ------------------------------------------------------------------ */
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the mutation summary report object (does not write to disk).
|
|
35
|
+
*/
|
|
36
|
+
ReportGenerator.prototype.generate = function (mutationResult, context) {
|
|
37
|
+
context = context || {};
|
|
38
|
+
var mutants = mutationResult.mutants || [];
|
|
39
|
+
return {
|
|
40
|
+
schemaVersion: '1.0',
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
projectRoot: this._cwd,
|
|
43
|
+
command: context.command || 'unknown',
|
|
44
|
+
mode: context.mode || null,
|
|
45
|
+
configuration: {
|
|
46
|
+
releaseBranch: context.releaseBranch || null
|
|
47
|
+
},
|
|
48
|
+
filePairs: context.filePairs || [],
|
|
49
|
+
summary: mutationResult.summary || this._buildEmptySummary(),
|
|
50
|
+
files: mutationResult.files || {},
|
|
51
|
+
mutants: mutants.map(function (m) {
|
|
52
|
+
return {
|
|
26
53
|
id: m.id,
|
|
27
54
|
mutatorName: m.mutatorName,
|
|
28
55
|
replacement: m.replacement,
|
|
@@ -32,41 +59,202 @@ class ReportGenerator {
|
|
|
32
59
|
killedBy: m.killedBy || [],
|
|
33
60
|
coveredBy: m.coveredBy || [],
|
|
34
61
|
description: m.description || ''
|
|
35
|
-
}
|
|
62
|
+
};
|
|
63
|
+
})
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/* ------------------------------------------------------------------ */
|
|
68
|
+
/* SonarQube external issues report */
|
|
69
|
+
/* ------------------------------------------------------------------ */
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* SonarQube severity mapping for mutant statuses.
|
|
73
|
+
*
|
|
74
|
+
* Only survived and no-coverage mutants are reported as issues.
|
|
75
|
+
* Killed, Timeout, Ignored, etc. are NOT issues.
|
|
76
|
+
*/
|
|
77
|
+
var SONAR_SEVERITY_MAP = {
|
|
78
|
+
survived: 'MAJOR',
|
|
79
|
+
nocoverage: 'MINOR'
|
|
80
|
+
};
|
|
81
|
+
var SONAR_RULE_MAP = {
|
|
82
|
+
survived: 'mutant-survived',
|
|
83
|
+
nocoverage: 'mutant-no-coverage'
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a SonarQube-compatible external issues report.
|
|
88
|
+
*
|
|
89
|
+
* Format: { issues: [ { engineId, ruleId, severity, type, primaryLocation, effortMinutes } ] }
|
|
90
|
+
* Ref: https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/importing-external-issues/generic-issue-import-format/
|
|
91
|
+
*
|
|
92
|
+
* @param {object} mutationResult - normalized Stryker result
|
|
93
|
+
* @returns {object} SonarQube generic issue import object
|
|
94
|
+
*/
|
|
95
|
+
ReportGenerator.prototype.generateSonarReport = function (mutationResult) {
|
|
96
|
+
var mutants = mutationResult.mutants || [];
|
|
97
|
+
var issues = [];
|
|
98
|
+
for (var i = 0; i < mutants.length; i++) {
|
|
99
|
+
var mutant = mutants[i];
|
|
100
|
+
var status = (mutant.status || '').toLowerCase();
|
|
101
|
+
|
|
102
|
+
// Only survived and no-coverage are actionable issues
|
|
103
|
+
if (!SONAR_SEVERITY_MAP[status]) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
var fileName = mutant.fileName || mutant.sourceFile || '';
|
|
107
|
+
if (!fileName) continue;
|
|
108
|
+
var message = this._buildSonarMessage(mutant, status);
|
|
109
|
+
var textRange = this._buildSonarTextRange(mutant.location);
|
|
110
|
+
var issue = {
|
|
111
|
+
engineId: 'stryker',
|
|
112
|
+
ruleId: SONAR_RULE_MAP[status],
|
|
113
|
+
severity: SONAR_SEVERITY_MAP[status],
|
|
114
|
+
type: 'CODE_SMELL',
|
|
115
|
+
primaryLocation: {
|
|
116
|
+
message: message,
|
|
117
|
+
filePath: fileName.replace(/\\/g, '/'),
|
|
118
|
+
textRange: textRange
|
|
119
|
+
},
|
|
120
|
+
effortMinutes: status === 'survived' ? 15 : 10
|
|
36
121
|
};
|
|
37
|
-
|
|
122
|
+
issues.push(issue);
|
|
38
123
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
124
|
+
return {
|
|
125
|
+
issues: issues
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build a human-readable message for a SonarQube issue.
|
|
131
|
+
*/
|
|
132
|
+
ReportGenerator.prototype._buildSonarMessage = function (mutant, status) {
|
|
133
|
+
var parts = [];
|
|
134
|
+
if (status === 'survived') {
|
|
135
|
+
parts.push('Survived mutant');
|
|
136
|
+
} else {
|
|
137
|
+
parts.push('Mutant has no test coverage');
|
|
51
138
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
const filePath = path.join(outputPath, this._outputFileName);
|
|
60
|
-
fs.writeFileSync(filePath, JSON.stringify(report, null, 2), 'utf-8');
|
|
61
|
-
return filePath;
|
|
139
|
+
if (mutant.mutatorName) {
|
|
140
|
+
parts.push(': ' + mutant.mutatorName);
|
|
141
|
+
}
|
|
142
|
+
if (mutant.replacement !== undefined && mutant.replacement !== null) {
|
|
143
|
+
parts.push(' → replaced with "' + String(mutant.replacement) + '"');
|
|
62
144
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
145
|
+
if (mutant.description) {
|
|
146
|
+
parts.push(' (' + mutant.description + ')');
|
|
147
|
+
}
|
|
148
|
+
return parts.join('');
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build a SonarQube textRange from Stryker location.
|
|
153
|
+
*
|
|
154
|
+
* SonarQube expects 1-based lines and 0-based columns.
|
|
155
|
+
* Stryker v5 uses 0-based lines and 0-based columns by default,
|
|
156
|
+
* but some reporters produce 1-based; we handle both.
|
|
157
|
+
*/
|
|
158
|
+
ReportGenerator.prototype._buildSonarTextRange = function (location) {
|
|
159
|
+
if (!location || !location.start) {
|
|
66
160
|
return {
|
|
67
|
-
|
|
68
|
-
|
|
161
|
+
startLine: 1,
|
|
162
|
+
endLine: 1,
|
|
163
|
+
startColumn: 0,
|
|
164
|
+
endColumn: 1
|
|
69
165
|
};
|
|
70
166
|
}
|
|
71
|
-
|
|
167
|
+
var startLine = location.start.line;
|
|
168
|
+
var endLine = location.end && location.end.line || startLine;
|
|
169
|
+
var startColumn = location.start.column || 0;
|
|
170
|
+
var endColumn = location.end && location.end.column || startColumn + 1;
|
|
171
|
+
|
|
172
|
+
// Ensure lines are at least 1 (SonarQube requires 1-based)
|
|
173
|
+
if (startLine < 1) startLine = 1;
|
|
174
|
+
if (endLine < 1) endLine = 1;
|
|
175
|
+
return {
|
|
176
|
+
startLine: startLine,
|
|
177
|
+
endLine: endLine,
|
|
178
|
+
startColumn: startColumn,
|
|
179
|
+
endColumn: endColumn
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/* ------------------------------------------------------------------ */
|
|
184
|
+
/* Common helpers */
|
|
185
|
+
/* ------------------------------------------------------------------ */
|
|
186
|
+
|
|
187
|
+
ReportGenerator.prototype._buildEmptySummary = function () {
|
|
188
|
+
return {
|
|
189
|
+
totalMutants: 0,
|
|
190
|
+
killed: 0,
|
|
191
|
+
survived: 0,
|
|
192
|
+
timeout: 0,
|
|
193
|
+
noCoverage: 0,
|
|
194
|
+
ignored: 0,
|
|
195
|
+
runtimeErrors: 0,
|
|
196
|
+
compileErrors: 0,
|
|
197
|
+
mutationScore: 0
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/* ------------------------------------------------------------------ */
|
|
202
|
+
/* Write to disk */
|
|
203
|
+
/* ------------------------------------------------------------------ */
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Write the mutation summary report to disk.
|
|
207
|
+
*
|
|
208
|
+
* @param {object} report
|
|
209
|
+
* @returns {string} absolute file path
|
|
210
|
+
*/
|
|
211
|
+
ReportGenerator.prototype.write = function (report) {
|
|
212
|
+
var outputPath = path.resolve(this._cwd, this._outputDir);
|
|
213
|
+
if (!fs.existsSync(outputPath)) {
|
|
214
|
+
fs.mkdirSync(outputPath, {
|
|
215
|
+
recursive: true
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
var filePath = path.join(outputPath, this._outputFileName);
|
|
219
|
+
fs.writeFileSync(filePath, JSON.stringify(report, null, 2), 'utf-8');
|
|
220
|
+
return filePath;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Write the SonarQube external issues report to disk.
|
|
225
|
+
*
|
|
226
|
+
* @param {object} sonarReport
|
|
227
|
+
* @returns {string} absolute file path
|
|
228
|
+
*/
|
|
229
|
+
ReportGenerator.prototype.writeSonarReport = function (sonarReport) {
|
|
230
|
+
var outputPath = path.resolve(this._cwd, this._outputDir);
|
|
231
|
+
if (!fs.existsSync(outputPath)) {
|
|
232
|
+
fs.mkdirSync(outputPath, {
|
|
233
|
+
recursive: true
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
var filePath = path.join(outputPath, this._sonarFileName);
|
|
237
|
+
fs.writeFileSync(filePath, JSON.stringify(sonarReport, null, 2), 'utf-8');
|
|
238
|
+
return filePath;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generate + write both reports in one call.
|
|
243
|
+
*
|
|
244
|
+
* @param {object} mutationResult - normalized Stryker result
|
|
245
|
+
* @param {object} context - command, mode, releaseBranch, filePairs
|
|
246
|
+
* @returns {{ report: object, sonarReport: object, filePath: string, sonarFilePath: string }}
|
|
247
|
+
*/
|
|
248
|
+
ReportGenerator.prototype.generateAndWrite = function (mutationResult, context) {
|
|
249
|
+
var report = this.generate(mutationResult, context);
|
|
250
|
+
var sonarReport = this.generateSonarReport(mutationResult);
|
|
251
|
+
var filePath = this.write(report);
|
|
252
|
+
var sonarFilePath = this.writeSonarReport(sonarReport);
|
|
253
|
+
return {
|
|
254
|
+
report: report,
|
|
255
|
+
sonarReport: sonarReport,
|
|
256
|
+
filePath: filePath,
|
|
257
|
+
sonarFilePath: sonarFilePath
|
|
258
|
+
};
|
|
259
|
+
};
|
|
72
260
|
module.exports = ReportGenerator;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var path = require('path');
|
|
4
|
+
var os = require('os');
|
|
5
|
+
var VALID_TEST_RUNNERS = ['jest', 'command'];
|
|
6
|
+
var VALID_COVERAGE_ANALYSES = ['off', 'all', 'perTest'];
|
|
7
|
+
var VALID_LOG_LEVELS = ['off', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'];
|
|
8
|
+
var VALID_REPORTERS = ['html', 'json', 'clear-text', 'progress', 'dots', 'dashboard', 'event-recorder'];
|
|
9
|
+
var VALID_JEST_PROJECT_TYPES = ['custom', 'create-react-app', 'create-react-app-ts'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* StrykerConfigBuilder — fluent builder for @stryker-mutator/core config.
|
|
13
|
+
*
|
|
14
|
+
* Covers every option from the PartialStrykerOptions API and applies
|
|
15
|
+
* sensible defaults. Call .build() to produce the final frozen config
|
|
16
|
+
* object that can be fed directly to `new Stryker(config)`.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* var config = new StrykerConfigBuilder()
|
|
20
|
+
* .setPackageManager('npm')
|
|
21
|
+
* .setTestRunner('jest')
|
|
22
|
+
* .setCoverageAnalysis('perTest')
|
|
23
|
+
* .setMutate(['src/**\/*.ts', '!src/**\/*.d.ts'])
|
|
24
|
+
* .setJest({ configFile: 'jest.config.js', enableFindRelatedTests: true })
|
|
25
|
+
* .setConcurrency(4)
|
|
26
|
+
* .build();
|
|
27
|
+
*/
|
|
28
|
+
function StrykerConfigBuilder() {
|
|
29
|
+
this._config = {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ------------------------------------------------------------------ */
|
|
33
|
+
/* Core settings */
|
|
34
|
+
/* ------------------------------------------------------------------ */
|
|
35
|
+
|
|
36
|
+
StrykerConfigBuilder.prototype.setPackageManager = function (value) {
|
|
37
|
+
if (value !== 'npm' && value !== 'yarn' && value !== 'pnpm') {
|
|
38
|
+
throw new Error('packageManager must be "npm", "yarn", or "pnpm". Got: ' + value);
|
|
39
|
+
}
|
|
40
|
+
this._config.packageManager = value;
|
|
41
|
+
return this;
|
|
42
|
+
};
|
|
43
|
+
StrykerConfigBuilder.prototype.setTestRunner = function (value) {
|
|
44
|
+
if (VALID_TEST_RUNNERS.indexOf(value) === -1) {
|
|
45
|
+
throw new Error('testRunner must be one of [' + VALID_TEST_RUNNERS.join(', ') + ']. Got: ' + value);
|
|
46
|
+
}
|
|
47
|
+
this._config.testRunner = value;
|
|
48
|
+
return this;
|
|
49
|
+
};
|
|
50
|
+
StrykerConfigBuilder.prototype.setCoverageAnalysis = function (value) {
|
|
51
|
+
if (VALID_COVERAGE_ANALYSES.indexOf(value) === -1) {
|
|
52
|
+
throw new Error('coverageAnalysis must be one of [' + VALID_COVERAGE_ANALYSES.join(', ') + ']. Got: ' + value);
|
|
53
|
+
}
|
|
54
|
+
this._config.coverageAnalysis = value;
|
|
55
|
+
return this;
|
|
56
|
+
};
|
|
57
|
+
StrykerConfigBuilder.prototype.setLogLevel = function (value) {
|
|
58
|
+
if (VALID_LOG_LEVELS.indexOf(value) === -1) {
|
|
59
|
+
throw new Error('logLevel must be one of [' + VALID_LOG_LEVELS.join(', ') + ']. Got: ' + value);
|
|
60
|
+
}
|
|
61
|
+
this._config.logLevel = value;
|
|
62
|
+
return this;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/* ------------------------------------------------------------------ */
|
|
66
|
+
/* Reporters */
|
|
67
|
+
/* ------------------------------------------------------------------ */
|
|
68
|
+
|
|
69
|
+
StrykerConfigBuilder.prototype.setReporters = function (reporters) {
|
|
70
|
+
if (!Array.isArray(reporters) || reporters.length === 0) {
|
|
71
|
+
throw new Error('reporters must be a non-empty array.');
|
|
72
|
+
}
|
|
73
|
+
for (var i = 0; i < reporters.length; i++) {
|
|
74
|
+
if (VALID_REPORTERS.indexOf(reporters[i]) === -1) {
|
|
75
|
+
throw new Error('Unknown reporter "' + reporters[i] + '". Valid: [' + VALID_REPORTERS.join(', ') + ']');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
this._config.reporters = reporters.slice();
|
|
79
|
+
return this;
|
|
80
|
+
};
|
|
81
|
+
StrykerConfigBuilder.prototype.setHtmlReporter = function (opts) {
|
|
82
|
+
if (!opts || typeof opts.fileName !== 'string') {
|
|
83
|
+
throw new Error('htmlReporter requires a fileName string.');
|
|
84
|
+
}
|
|
85
|
+
this._config.htmlReporter = {
|
|
86
|
+
fileName: opts.fileName
|
|
87
|
+
};
|
|
88
|
+
return this;
|
|
89
|
+
};
|
|
90
|
+
StrykerConfigBuilder.prototype.setJsonReporter = function (opts) {
|
|
91
|
+
if (!opts || typeof opts.fileName !== 'string') {
|
|
92
|
+
throw new Error('jsonReporter requires a fileName string.');
|
|
93
|
+
}
|
|
94
|
+
this._config.jsonReporter = {
|
|
95
|
+
fileName: opts.fileName
|
|
96
|
+
};
|
|
97
|
+
return this;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/* ------------------------------------------------------------------ */
|
|
101
|
+
/* Mutate patterns */
|
|
102
|
+
/* ------------------------------------------------------------------ */
|
|
103
|
+
|
|
104
|
+
StrykerConfigBuilder.prototype.setMutate = function (patterns) {
|
|
105
|
+
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
106
|
+
throw new Error('mutate must be a non-empty array of glob patterns.');
|
|
107
|
+
}
|
|
108
|
+
this._config.mutate = patterns.slice();
|
|
109
|
+
return this;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/* ------------------------------------------------------------------ */
|
|
113
|
+
/* Jest configuration */
|
|
114
|
+
/* ------------------------------------------------------------------ */
|
|
115
|
+
|
|
116
|
+
StrykerConfigBuilder.prototype.setJest = function (jestOpts) {
|
|
117
|
+
if (!jestOpts || typeof jestOpts !== 'object') {
|
|
118
|
+
throw new Error('jest config must be a non-null object.');
|
|
119
|
+
}
|
|
120
|
+
var jest = {};
|
|
121
|
+
if (jestOpts.projectType) {
|
|
122
|
+
if (VALID_JEST_PROJECT_TYPES.indexOf(jestOpts.projectType) === -1) {
|
|
123
|
+
throw new Error('jest.projectType must be one of [' + VALID_JEST_PROJECT_TYPES.join(', ') + ']. Got: ' + jestOpts.projectType);
|
|
124
|
+
}
|
|
125
|
+
jest.projectType = jestOpts.projectType;
|
|
126
|
+
}
|
|
127
|
+
if (jestOpts.configFile) {
|
|
128
|
+
jest.configFile = jestOpts.configFile;
|
|
129
|
+
}
|
|
130
|
+
if (jestOpts.enableFindRelatedTests != null) {
|
|
131
|
+
jest.enableFindRelatedTests = !!jestOpts.enableFindRelatedTests;
|
|
132
|
+
}
|
|
133
|
+
if (jestOpts.config && typeof jestOpts.config === 'object') {
|
|
134
|
+
jest.config = Object.assign({}, jestOpts.config);
|
|
135
|
+
}
|
|
136
|
+
this._config.jest = jest;
|
|
137
|
+
return this;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/* ------------------------------------------------------------------ */
|
|
141
|
+
/* Command runner (alternative to jest runner) */
|
|
142
|
+
/* ------------------------------------------------------------------ */
|
|
143
|
+
|
|
144
|
+
StrykerConfigBuilder.prototype.setCommandRunner = function (opts) {
|
|
145
|
+
if (!opts || typeof opts.command !== 'string') {
|
|
146
|
+
throw new Error('commandRunner requires a command string.');
|
|
147
|
+
}
|
|
148
|
+
this._config.commandRunner = {
|
|
149
|
+
command: opts.command
|
|
150
|
+
};
|
|
151
|
+
return this;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/* ------------------------------------------------------------------ */
|
|
155
|
+
/* Timeouts & concurrency */
|
|
156
|
+
/* ------------------------------------------------------------------ */
|
|
157
|
+
|
|
158
|
+
StrykerConfigBuilder.prototype.setTimeoutMS = function (value) {
|
|
159
|
+
var n = Number(value);
|
|
160
|
+
if (isNaN(n) || n <= 0) {
|
|
161
|
+
throw new Error('timeoutMS must be a positive number. Got: ' + value);
|
|
162
|
+
}
|
|
163
|
+
this._config.timeoutMS = n;
|
|
164
|
+
return this;
|
|
165
|
+
};
|
|
166
|
+
StrykerConfigBuilder.prototype.setTimeoutFactor = function (value) {
|
|
167
|
+
var n = Number(value);
|
|
168
|
+
if (isNaN(n) || n <= 0) {
|
|
169
|
+
throw new Error('timeoutFactor must be a positive number. Got: ' + value);
|
|
170
|
+
}
|
|
171
|
+
this._config.timeoutFactor = n;
|
|
172
|
+
return this;
|
|
173
|
+
};
|
|
174
|
+
StrykerConfigBuilder.prototype.setConcurrency = function (value) {
|
|
175
|
+
var n = Number(value);
|
|
176
|
+
if (isNaN(n) || n < 1) {
|
|
177
|
+
throw new Error('concurrency must be >= 1. Got: ' + value);
|
|
178
|
+
}
|
|
179
|
+
this._config.concurrency = Math.floor(n);
|
|
180
|
+
return this;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/* ------------------------------------------------------------------ */
|
|
184
|
+
/* Temp directory & cleanup */
|
|
185
|
+
/* ------------------------------------------------------------------ */
|
|
186
|
+
|
|
187
|
+
StrykerConfigBuilder.prototype.setTempDirName = function (value) {
|
|
188
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
189
|
+
throw new Error('tempDirName must be a non-empty string.');
|
|
190
|
+
}
|
|
191
|
+
this._config.tempDirName = value;
|
|
192
|
+
return this;
|
|
193
|
+
};
|
|
194
|
+
StrykerConfigBuilder.prototype.setCleanTempDir = function (value) {
|
|
195
|
+
this._config.cleanTempDir = !!value;
|
|
196
|
+
return this;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/* ------------------------------------------------------------------ */
|
|
200
|
+
/* Thresholds */
|
|
201
|
+
/* ------------------------------------------------------------------ */
|
|
202
|
+
|
|
203
|
+
StrykerConfigBuilder.prototype.setThresholds = function (opts) {
|
|
204
|
+
if (!opts || typeof opts !== 'object') {
|
|
205
|
+
throw new Error('thresholds must be a non-null object.');
|
|
206
|
+
}
|
|
207
|
+
this._config.thresholds = {
|
|
208
|
+
high: opts.high != null ? Number(opts.high) : 80,
|
|
209
|
+
low: opts.low != null ? Number(opts.low) : 60,
|
|
210
|
+
break: opts.break != null ? Number(opts.break) : null
|
|
211
|
+
};
|
|
212
|
+
return this;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/* ------------------------------------------------------------------ */
|
|
216
|
+
/* Miscellaneous */
|
|
217
|
+
/* ------------------------------------------------------------------ */
|
|
218
|
+
|
|
219
|
+
StrykerConfigBuilder.prototype.setAllowEmpty = function (value) {
|
|
220
|
+
this._config.allowEmpty = !!value;
|
|
221
|
+
return this;
|
|
222
|
+
};
|
|
223
|
+
StrykerConfigBuilder.prototype.setConfigFile = function (filePath) {
|
|
224
|
+
if (typeof filePath !== 'string' || filePath.length === 0) {
|
|
225
|
+
throw new Error('configFile must be a non-empty string.');
|
|
226
|
+
}
|
|
227
|
+
this._config.configFile = filePath;
|
|
228
|
+
return this;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/* ------------------------------------------------------------------ */
|
|
232
|
+
/* Build from external config file (stryker.conf.js) */
|
|
233
|
+
/* ------------------------------------------------------------------ */
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Seed the builder from an existing Stryker config file (JS/JSON).
|
|
237
|
+
* Values set after this call override what the file provides.
|
|
238
|
+
*/
|
|
239
|
+
StrykerConfigBuilder.prototype.fromConfigFile = function (filePath) {
|
|
240
|
+
var resolved = path.resolve(filePath);
|
|
241
|
+
var external = require(resolved);
|
|
242
|
+
this._config = Object.assign({}, external, this._config);
|
|
243
|
+
return this;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/* ------------------------------------------------------------------ */
|
|
247
|
+
/* Build from a plain options hash (backwards compat) */
|
|
248
|
+
/* ------------------------------------------------------------------ */
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Populate builder fields from a flat options object.
|
|
252
|
+
* Useful for migrating existing code that already passes an options bag.
|
|
253
|
+
*/
|
|
254
|
+
StrykerConfigBuilder.prototype.fromOptions = function (opts) {
|
|
255
|
+
if (!opts || typeof opts !== 'object') return this;
|
|
256
|
+
if (opts.packageManager) this.setPackageManager(opts.packageManager);
|
|
257
|
+
if (opts.testRunner) this.setTestRunner(opts.testRunner);
|
|
258
|
+
if (opts.coverageAnalysis) this.setCoverageAnalysis(opts.coverageAnalysis);
|
|
259
|
+
if (opts.logLevel) this.setLogLevel(opts.logLevel);
|
|
260
|
+
if (opts.reporters) this.setReporters(opts.reporters);
|
|
261
|
+
if (opts.htmlReporter) this.setHtmlReporter(opts.htmlReporter);
|
|
262
|
+
if (opts.jsonReporter) this.setJsonReporter(opts.jsonReporter);
|
|
263
|
+
if (opts.mutate) this.setMutate(opts.mutate);
|
|
264
|
+
if (opts.jest) this.setJest(opts.jest);
|
|
265
|
+
if (opts.commandRunner) this.setCommandRunner(opts.commandRunner);
|
|
266
|
+
if (opts.timeoutMS != null) this.setTimeoutMS(opts.timeoutMS);
|
|
267
|
+
if (opts.timeoutFactor != null) this.setTimeoutFactor(opts.timeoutFactor);
|
|
268
|
+
if (opts.concurrency != null) this.setConcurrency(opts.concurrency);
|
|
269
|
+
if (opts.tempDirName) this.setTempDirName(opts.tempDirName);
|
|
270
|
+
if (opts.cleanTempDir != null) this.setCleanTempDir(opts.cleanTempDir);
|
|
271
|
+
if (opts.thresholds) this.setThresholds(opts.thresholds);
|
|
272
|
+
if (opts.allowEmpty != null) this.setAllowEmpty(opts.allowEmpty);
|
|
273
|
+
if (opts.configFile) this.setConfigFile(opts.configFile);
|
|
274
|
+
return this;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/* ------------------------------------------------------------------ */
|
|
278
|
+
/* Build */
|
|
279
|
+
/* ------------------------------------------------------------------ */
|
|
280
|
+
|
|
281
|
+
var DEFAULTS = {
|
|
282
|
+
packageManager: 'npm',
|
|
283
|
+
testRunner: 'jest',
|
|
284
|
+
coverageAnalysis: 'perTest',
|
|
285
|
+
reporters: ['html', 'json'],
|
|
286
|
+
logLevel: 'info',
|
|
287
|
+
timeoutMS: 60000,
|
|
288
|
+
timeoutFactor: 1.5,
|
|
289
|
+
cleanTempDir: true,
|
|
290
|
+
allowEmpty: true,
|
|
291
|
+
thresholds: {
|
|
292
|
+
high: 80,
|
|
293
|
+
low: 60,
|
|
294
|
+
break: null
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Produce the final config object.
|
|
300
|
+
*
|
|
301
|
+
* Merges explicit values over sensible defaults; computes concurrency
|
|
302
|
+
* from CPU count if not specified.
|
|
303
|
+
*
|
|
304
|
+
* @returns {object} frozen Stryker-compatible config
|
|
305
|
+
*/
|
|
306
|
+
StrykerConfigBuilder.prototype.build = function () {
|
|
307
|
+
var cfg = Object.assign({}, DEFAULTS, this._config);
|
|
308
|
+
if (cfg.concurrency == null) {
|
|
309
|
+
cfg.concurrency = Math.max(1, os.cpus().length - 1);
|
|
310
|
+
}
|
|
311
|
+
return cfg;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Snapshot the current builder state (shallow copy).
|
|
316
|
+
* Useful for forking a shared base config per Stryker run.
|
|
317
|
+
*/
|
|
318
|
+
StrykerConfigBuilder.prototype.clone = function () {
|
|
319
|
+
var copy = new StrykerConfigBuilder();
|
|
320
|
+
copy._config = Object.assign({}, this._config);
|
|
321
|
+
if (this._config.mutate) copy._config.mutate = this._config.mutate.slice();
|
|
322
|
+
if (this._config.reporters) copy._config.reporters = this._config.reporters.slice();
|
|
323
|
+
if (this._config.jest) copy._config.jest = Object.assign({}, this._config.jest);
|
|
324
|
+
if (this._config.thresholds) copy._config.thresholds = Object.assign({}, this._config.thresholds);
|
|
325
|
+
return copy;
|
|
326
|
+
};
|
|
327
|
+
module.exports = StrykerConfigBuilder;
|