@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.
@@ -2,27 +2,54 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- class ReportGenerator {
6
- constructor(options = {}) {
7
- this._outputDir = options.outputDir || 'reports/mutation';
8
- this._outputFileName = options.outputFileName || 'mutation-report.json';
9
- this._cwd = options.cwd || process.cwd();
10
- }
11
- generate(mutationResult, context = {}) {
12
- const report = {
13
- schemaVersion: '1.0',
14
- timestamp: new Date().toISOString(),
15
- projectRoot: this._cwd,
16
- command: context.command || 'unknown',
17
- configuration: {
18
- branch: context.branch || null,
19
- srcDir: context.srcDir || null,
20
- testDir: context.testDir || null
21
- },
22
- filePairs: context.filePairs || [],
23
- summary: mutationResult.summary || this._buildEmptySummary(),
24
- files: mutationResult.files || {},
25
- mutants: (mutationResult.mutants || []).map(m => ({
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
- return report;
122
+ issues.push(issue);
38
123
  }
39
- _buildEmptySummary() {
40
- return {
41
- totalMutants: 0,
42
- killed: 0,
43
- survived: 0,
44
- timeout: 0,
45
- noCoverage: 0,
46
- ignored: 0,
47
- runtimeErrors: 0,
48
- compileErrors: 0,
49
- mutationScore: 0
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
- write(report) {
53
- const outputPath = path.resolve(this._cwd, this._outputDir);
54
- if (!fs.existsSync(outputPath)) {
55
- fs.mkdirSync(outputPath, {
56
- recursive: true
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
- generateAndWrite(mutationResult, context = {}) {
64
- const report = this.generate(mutationResult, context);
65
- const filePath = this.write(report);
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
- report,
68
- filePath
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;