@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.
@@ -1,208 +1,235 @@
1
1
  "use strict";
2
2
 
3
- const path = require('path');
4
- const StrykerWrapper = require('./strykerWrapper');
5
- const BranchDiff = require('./branchDiff');
6
- const FileResolver = require('./fileResolver');
7
- const ReportGenerator = require('./reportGenerator');
8
- class MutationRunner {
9
- constructor(options = {}) {
10
- this._cwd = options.cwd || process.cwd();
11
- this._stryker = new StrykerWrapper({
12
- reportPath: options.reportPath,
13
- concurrency: options.concurrency,
14
- timeoutMS: options.timeoutMS,
15
- logLevel: options.logLevel
16
- });
17
- this._branchDiff = new BranchDiff({
18
- cwd: this._cwd,
19
- api: options.api || null,
20
- patToken: options.patToken || null
21
- });
22
- this._fileResolver = new FileResolver({
23
- cwd: this._cwd
24
- });
25
- this._reportGenerator = new ReportGenerator({
26
- cwd: this._cwd,
27
- outputDir: options.outputDir || 'reports/mutation',
28
- outputFileName: options.outputFileName || 'mutation-report.json'
29
- });
30
- this._jestConfig = options.jestConfig || {};
31
- }
32
- async runByBranch(branch, options = {}) {
33
- if (!branch) {
34
- throw new Error('Branch name is required. Usage: npx ZDLintFramework mutate --branch=<branch>');
3
+ var StrykerWrapper = require('./strykerWrapper');
4
+ var StrykerConfigBuilder = require('./strykerConfigBuilder');
5
+ var BranchDiff = require('./branchDiff');
6
+ var FileResolver = require('./fileResolver');
7
+ var ReportGenerator = require('./reportGenerator');
8
+ var path = require('path');
9
+ /**
10
+ * MutationRunner orchestrates mutation testing.
11
+ *
12
+ * Exposes exactly two public methods:
13
+ *
14
+ * runLocal(releaseBranch)
15
+ * Pre-commit / local dev. Collects staged files AND changed files
16
+ * vs the release branch, deduplicates, resolves source↔test pairs,
17
+ * runs Stryker on those specific files, and writes JSON reports
18
+ * (mutation summary + SonarQube-compatible external issues report).
19
+ *
20
+ * runCI(releaseBranch)
21
+ * CI pipeline. Collects changed files vs the release branch,
22
+ * resolves source↔test pairs, runs Stryker, and writes JSON reports.
23
+ * The caller decides pass/fail based on the report.
24
+ *
25
+ * Compatible with Node 16+ and @stryker-mutator/core v5.
26
+ *
27
+ * @param {object} options
28
+ * @param {StrykerConfigBuilder} [options.configBuilder] - pre-configured builder
29
+ */
30
+ function MutationRunner(options) {
31
+ options = options || {};
32
+ this._cwd = options.cwd || process.cwd();
33
+ var configBuilder = options.configBuilder;
34
+ if (!configBuilder) {
35
+ configBuilder = new StrykerConfigBuilder();
36
+ if (options.strykerConfigpath) {
37
+ configBuilder.fromConfigFile(options.strykerConfigpath);
35
38
  }
36
-
37
- // Step 1: Get changed files
38
- const {
39
- sourceFiles,
40
- testFiles
41
- } = await this._branchDiff.getChangedFiles(branch, {
42
- useApi: options.useApi || false,
43
- hostname: options.hostname || null,
44
- projectId: options.projectId || null
45
- });
46
- if (sourceFiles.length === 0 && testFiles.length === 0) {
47
- const emptyResult = this._buildEmptyResult();
48
- const {
49
- report,
50
- filePath
51
- } = this._reportGenerator.generateAndWrite(emptyResult, {
52
- command: `mutate --branch=${branch}`,
53
- branch,
54
- filePairs: []
39
+ if (options.concurrency != null) configBuilder.setConcurrency(options.concurrency);
40
+ if (options.timeoutMS != null) configBuilder.setTimeoutMS(options.timeoutMS);
41
+ if (options.logLevel) configBuilder.setLogLevel(options.logLevel);
42
+ if (options.reportPath) {
43
+ configBuilder.setHtmlReporter({
44
+ fileName: options.reportPath
45
+ }).setJsonReporter({
46
+ fileName: options.reportPath
55
47
  });
56
- return {
57
- report,
58
- filePath,
59
- message: 'No changed source or test files found in branch diff.'
60
- };
61
48
  }
49
+ }
50
+ this._stryker = new StrykerWrapper(configBuilder);
51
+ this._branchDiff = new BranchDiff({
52
+ cwd: this._cwd,
53
+ token: options.token,
54
+ gitEndPoint: options.gitEndPoint,
55
+ branchDiffPath: options.branchDiffPath,
56
+ compareBranch: options.compareBranch,
57
+ projectId: options.projectId
58
+ });
59
+ this._fileResolver = new FileResolver({
60
+ cwd: this._cwd
61
+ });
62
+ this._reportGenerator = new ReportGenerator({
63
+ cwd: this._cwd,
64
+ outputDir: options.outputDir || 'reports/mutation',
65
+ outputFileName: options.outputFileName || 'mutation-report.json'
66
+ });
67
+ this._jestConfig = options.jestConfig || {};
68
+ }
62
69
 
63
- // Step 2: Resolve source-test pairs
64
- const pairs = this._fileResolver.resolveSourceTestPairs(sourceFiles, testFiles);
65
- const validPairs = pairs.filter(p => p.source && p.test);
66
- const sourcesToMutate = [...new Set(validPairs.map(p => p.source))];
67
- const testsToRun = [...new Set(validPairs.map(p => p.test))];
68
- if (sourcesToMutate.length === 0) {
69
- const emptyResult = this._buildEmptyResult();
70
- const {
71
- report,
72
- filePath
73
- } = this._reportGenerator.generateAndWrite(emptyResult, {
74
- command: `mutate --branch=${branch}`,
75
- branch,
76
- filePairs: pairs
77
- });
78
- return {
79
- report,
80
- filePath,
81
- message: 'No source files with corresponding test files found.'
82
- };
83
- }
70
+ /* ------------------------------------------------------------------ */
71
+ /* 1. LOCAL (pre-commit / staged) */
72
+ /* ------------------------------------------------------------------ */
84
73
 
85
- // Step 3: Run Stryker mutation testing
86
- const mutationResult = await this._stryker.run(sourcesToMutate, {
87
- testFiles: testsToRun,
88
- jest: {
89
- ...this._jestConfig,
90
- enableFindRelatedTests: true
91
- }
74
+ /**
75
+ * Run mutation testing on locally-changed files.
76
+ *
77
+ * Combines:
78
+ * • staged files (git diff --staged)
79
+ * • changed files (git diff vs releaseBranch, filter ACMR)
80
+ *
81
+ * @param {string} releaseBranch - branch to diff against (e.g. 'release')
82
+ * @returns {Promise<{ report: object, sonarReport: object, filePath: string, sonarFilePath: string, message?: string }>}
83
+ */
84
+ MutationRunner.prototype.runLocal = function (releaseBranch) {
85
+ if (!releaseBranch) {
86
+ throw new Error('releaseBranch is required. Usage: mutate --mode=local --release=<branch>');
87
+ }
88
+ var self = this;
89
+ return self._branchDiff.getLocalChanges(releaseBranch).then(function (categorized) {
90
+ return self._runMutation(categorized.sourceFiles, categorized.testFiles, {
91
+ mode: 'local',
92
+ releaseBranch: releaseBranch,
93
+ command: 'mutate --mode=local --release=' + releaseBranch
92
94
  });
95
+ });
96
+ };
93
97
 
94
- // Step 4: Generate report
95
- const {
96
- report,
97
- filePath
98
- } = this._reportGenerator.generateAndWrite(mutationResult, {
99
- command: `mutate --branch=${branch}`,
100
- branch,
101
- filePairs: pairs
102
- });
103
- return {
104
- report,
105
- filePath
106
- };
107
- }
108
- async runByDirectory(srcDir, testDir, options = {}) {
109
- if (!srcDir || !testDir) {
110
- throw new Error('Both source and test directories are required. Usage: npx ZDLintFramework mutate --src=<sourcedir> --test=<testdir>');
111
- }
98
+ /* ------------------------------------------------------------------ */
99
+ /* 2. CI */
100
+ /* ------------------------------------------------------------------ */
112
101
 
113
- // Step 1: Collect all source and test files
114
- const sourceFiles = this._fileResolver.collectSourceFiles(srcDir);
115
- const testFiles = this._fileResolver.collectTestFiles(testDir);
116
- if (sourceFiles.length === 0) {
117
- const emptyResult = this._buildEmptyResult();
118
- const {
119
- report,
120
- filePath
121
- } = this._reportGenerator.generateAndWrite(emptyResult, {
122
- command: `mutate --src=${srcDir} --test=${testDir}`,
123
- srcDir,
124
- testDir,
125
- filePairs: []
126
- });
127
- return {
128
- report,
129
- filePath,
130
- message: 'No source files found in the specified directory.'
131
- };
132
- }
102
+ /**
103
+ * Run mutation testing on files changed vs the release branch (CI).
104
+ *
105
+ * @param {string} releaseBranch - branch to diff against
106
+ * @returns {Promise<{ report: object, sonarReport: object, filePath: string, sonarFilePath: string, message?: string }>}
107
+ */
108
+ MutationRunner.prototype.runCI = function (releaseBranch) {
109
+ if (!releaseBranch) {
110
+ throw new Error('releaseBranch is required. Usage: mutate --mode=ci --release=<branch>');
111
+ }
112
+ var self = this;
113
+ return self._branchDiff.getCIChanges(releaseBranch).then(function (categorized) {
114
+ return self._runMutation(categorized.sourceFiles, categorized.testFiles, {
115
+ mode: 'ci',
116
+ releaseBranch: releaseBranch,
117
+ command: 'mutate --mode=ci --release=' + releaseBranch
118
+ });
119
+ });
120
+ };
133
121
 
134
- // Step 2: Resolve source-test pairs
135
- const pairs = this._fileResolver.resolveSourceTestPairs(sourceFiles, testFiles);
136
- const validPairs = pairs.filter(p => p.source && p.test);
137
- const sourcesToMutate = [...new Set(validPairs.map(p => p.source))];
138
- const testsToRun = [...new Set(validPairs.map(p => p.test))];
139
- if (sourcesToMutate.length === 0) {
140
- // Fallback: mutate all source files with all test files
141
- const mutationResult = await this._stryker.run(sourceFiles, {
142
- testFiles,
143
- jest: {
144
- ...this._jestConfig,
145
- enableFindRelatedTests: true
146
- }
147
- });
148
- const {
149
- report,
150
- filePath
151
- } = this._reportGenerator.generateAndWrite(mutationResult, {
152
- command: `mutate --src=${srcDir} --test=${testDir}`,
153
- srcDir,
154
- testDir,
155
- filePairs: sourceFiles.map(s => ({
156
- source: s,
157
- test: null
158
- }))
159
- });
160
- return {
161
- report,
162
- filePath
163
- };
164
- }
122
+ /* ------------------------------------------------------------------ */
123
+ /* Shared pipeline */
124
+ /* ------------------------------------------------------------------ */
165
125
 
166
- // Step 3: Run Stryker mutation testing
167
- const mutationResult = await this._stryker.run(sourcesToMutate, {
168
- testFiles: testsToRun,
169
- jest: {
170
- ...this._jestConfig,
171
- enableFindRelatedTests: true
172
- }
126
+ /**
127
+ * Internal common mutation pipeline for both modes.
128
+ *
129
+ * @param {string[]} sourceFiles
130
+ * @param {string[]} testFiles
131
+ * @param {{ mode: string, releaseBranch: string, command: string }} ctx
132
+ * @returns {Promise<{ report: object, sonarReport: object, filePath: string, sonarFilePath: string, message?: string }>}
133
+ */
134
+ MutationRunner.prototype._runMutation = function (sourceFiles, testFiles, ctx) {
135
+ var self = this;
136
+ if (sourceFiles.length === 0 && testFiles.length === 0) {
137
+ var emptyResult = self._buildEmptyResult();
138
+ var out = self._reportGenerator.generateAndWrite(emptyResult, {
139
+ command: ctx.command,
140
+ mode: ctx.mode,
141
+ releaseBranch: ctx.releaseBranch,
142
+ filePairs: []
173
143
  });
174
-
175
- // Step 4: Generate report
176
- const {
177
- report,
178
- filePath
179
- } = this._reportGenerator.generateAndWrite(mutationResult, {
180
- command: `mutate --src=${srcDir} --test=${testDir}`,
181
- srcDir,
182
- testDir,
144
+ return Promise.resolve({
145
+ report: out.report,
146
+ sonarReport: out.sonarReport,
147
+ filePath: out.filePath,
148
+ sonarFilePath: out.sonarFilePath,
149
+ message: 'No changed source or test files found.'
150
+ });
151
+ }
152
+ var pairs = self._fileResolver.resolveSourceTestPairs(sourceFiles, testFiles);
153
+ var validPairs = pairs.filter(function (p) {
154
+ return p.source && p.test;
155
+ });
156
+ var sourcesToMutate = [];
157
+ var testsToRun = [];
158
+ var seenSrc = {};
159
+ var seenTest = {};
160
+ var i;
161
+ for (i = 0; i < validPairs.length; i++) {
162
+ if (!seenSrc[validPairs[i].source]) {
163
+ seenSrc[validPairs[i].source] = true;
164
+ sourcesToMutate.push(validPairs[i].source);
165
+ }
166
+ if (!seenTest[validPairs[i].test]) {
167
+ seenTest[validPairs[i].test] = true;
168
+ testsToRun.push(path.join(this._branchDiff.getRootDirectory(), validPairs[i].test));
169
+ }
170
+ }
171
+ if (sourcesToMutate.length === 0) {
172
+ var emptyResult2 = self._buildEmptyResult();
173
+ var out2 = self._reportGenerator.generateAndWrite(emptyResult2, {
174
+ command: ctx.command,
175
+ mode: ctx.mode,
176
+ releaseBranch: ctx.releaseBranch,
183
177
  filePairs: pairs
184
178
  });
185
- return {
186
- report,
187
- filePath
188
- };
179
+ return Promise.resolve({
180
+ report: out2.report,
181
+ sonarReport: out2.sonarReport,
182
+ filePath: out2.filePath,
183
+ sonarFilePath: out2.sonarFilePath,
184
+ message: 'No source files with corresponding test files found.'
185
+ });
189
186
  }
190
- _buildEmptyResult() {
187
+ var strykerOpts = {
188
+ rootDirectory: this._branchDiff.getRootDirectory(),
189
+ testFiles: testsToRun,
190
+ jest: Object.assign({}, self._jestConfig, {
191
+ config: {
192
+ testMatch: testsToRun,
193
+ reporters: ['json']
194
+ },
195
+ enableFindRelatedTests: true
196
+ })
197
+ };
198
+ return self._stryker.run(sourcesToMutate, strykerOpts).then(function (mutationResult) {
199
+ var out3 = self._reportGenerator.generateAndWrite(mutationResult, {
200
+ command: ctx.command,
201
+ mode: ctx.mode,
202
+ releaseBranch: ctx.releaseBranch,
203
+ filePairs: pairs
204
+ });
191
205
  return {
192
- files: {},
193
- mutants: [],
194
- summary: {
195
- totalMutants: 0,
196
- killed: 0,
197
- survived: 0,
198
- timeout: 0,
199
- noCoverage: 0,
200
- ignored: 0,
201
- runtimeErrors: 0,
202
- compileErrors: 0,
203
- mutationScore: 0
204
- }
206
+ report: out3.report,
207
+ sonarReport: out3.sonarReport,
208
+ filePath: out3.filePath,
209
+ sonarFilePath: out3.sonarFilePath
205
210
  };
206
- }
207
- }
211
+ });
212
+ };
213
+
214
+ /* ------------------------------------------------------------------ */
215
+ /* Helpers */
216
+ /* ------------------------------------------------------------------ */
217
+
218
+ MutationRunner.prototype._buildEmptyResult = function () {
219
+ return {
220
+ files: {},
221
+ mutants: [],
222
+ summary: {
223
+ totalMutants: 0,
224
+ killed: 0,
225
+ survived: 0,
226
+ timeout: 0,
227
+ noCoverage: 0,
228
+ ignored: 0,
229
+ runtimeErrors: 0,
230
+ compileErrors: 0,
231
+ mutationScore: 0
232
+ }
233
+ };
234
+ };
208
235
  module.exports = MutationRunner;