@zohodesk/codestandard-validator 1.1.4 → 1.2.4-exp-2
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/bin/cliCI.js +0 -0
- package/build/ai/config.example.json +10 -0
- package/build/ai/ollama-service.js +403 -0
- package/build/ai/prompts/CODE_REVIEW_PROMPT.md +72 -0
- package/build/ai/prompts/PROMPT1.MD +70 -0
- package/build/ai/prompts/PROMPT2.md +159 -0
- package/build/ai/prompts/PROMPT3.md +64 -0
- package/build/ai/prompts/PROMPT4.md +64 -0
- package/build/ai/provider-factory.js +19 -0
- package/build/ai/providers/OllamaProvider.js +106 -0
- package/build/ai/render.js +157 -0
- package/build/ai/run-review.js +50 -0
- package/build/chunk/chunk_Restriction.js +202 -0
- package/build/hooks/Precommit/pre-commit-default.js +158 -140
- package/build/hooks/hook.js +6 -5
- package/build/lib/postinstall.js +6 -10
- package/build/mutation/branchDiff.js +178 -0
- package/build/mutation/fileResolver.js +170 -0
- package/build/mutation/index.js +16 -0
- package/build/mutation/mutatePattern.json +3 -0
- package/build/mutation/mutationCli.js +111 -0
- package/build/mutation/mutationRunner.js +208 -0
- package/build/mutation/reportGenerator.js +72 -0
- package/build/mutation/strykerWrapper.js +180 -0
- package/build/utils/FileAndFolderOperations/filterFiles.js +8 -6
- package/build/utils/FileAndFolderOperations/removeFolder.js +2 -2
- package/build/utils/General/Config.js +25 -0
- package/build/utils/General/SonarQubeUtil.js +1 -1
- package/jest.config.js +1 -1
- package/package.json +4 -1
- package/samples/sample-branch-mode.js +34 -0
- package/samples/sample-cli-entry.js +34 -0
- package/samples/sample-components.js +63 -0
- package/samples/sample-directory-mode.js +30 -0
- package/samples/sample-runner-direct.js +32 -0
- package/samples/sample-with-api.js +44 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
execSync
|
|
5
|
+
} = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
class BranchDiff {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this._cwd = options.cwd || process.cwd();
|
|
10
|
+
this._api = options.api || null;
|
|
11
|
+
this._patToken = options.patToken || null;
|
|
12
|
+
this._sourceExtensions = options.sourceExtensions || ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
|
|
13
|
+
this._testPattern = options.testPattern || /\.(test)\.(js|ts|jsx|tsx|mjs|cjs)$/;
|
|
14
|
+
}
|
|
15
|
+
_getCurrentBranch() {
|
|
16
|
+
try {
|
|
17
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
18
|
+
cwd: this._cwd,
|
|
19
|
+
encoding: 'utf-8'
|
|
20
|
+
}).trim();
|
|
21
|
+
} catch (err) {
|
|
22
|
+
throw new Error(`Failed to get current branch: ${err.message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
_fetchBranch(branch) {
|
|
26
|
+
try {
|
|
27
|
+
execSync(`git fetch origin ${branch}`, {
|
|
28
|
+
cwd: this._cwd,
|
|
29
|
+
encoding: 'utf-8',
|
|
30
|
+
stdio: 'pipe'
|
|
31
|
+
});
|
|
32
|
+
} catch (err) {
|
|
33
|
+
// Branch might already be available locally
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
_resolveBranchRef(branch) {
|
|
37
|
+
// Check if the ref exists locally
|
|
38
|
+
const candidates = [branch, `origin/${branch}`];
|
|
39
|
+
for (const ref of candidates) {
|
|
40
|
+
try {
|
|
41
|
+
execSync(`git rev-parse --verify ${ref}`, {
|
|
42
|
+
cwd: this._cwd,
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
stdio: 'pipe'
|
|
45
|
+
});
|
|
46
|
+
return ref;
|
|
47
|
+
} catch {
|
|
48
|
+
// try next
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Branch '${branch}' not found locally or as 'origin/${branch}'. ` + `Run 'git fetch origin ${branch}' or check the branch name.`);
|
|
52
|
+
}
|
|
53
|
+
_getDiffFiles(targetBranch) {
|
|
54
|
+
const currentBranch = this._getCurrentBranch();
|
|
55
|
+
this._fetchBranch(targetBranch);
|
|
56
|
+
const resolvedRef = this._resolveBranchRef(targetBranch);
|
|
57
|
+
let diffRef;
|
|
58
|
+
try {
|
|
59
|
+
const mergeBase = execSync(`git merge-base ${resolvedRef} ${currentBranch}`, {
|
|
60
|
+
cwd: this._cwd,
|
|
61
|
+
encoding: 'utf-8'
|
|
62
|
+
}).trim();
|
|
63
|
+
diffRef = mergeBase;
|
|
64
|
+
} catch {
|
|
65
|
+
diffRef = resolvedRef;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const output = execSync(`git diff --name-only --diff-filter=ACMR ${diffRef}`, {
|
|
69
|
+
cwd: this._cwd,
|
|
70
|
+
encoding: 'utf-8'
|
|
71
|
+
});
|
|
72
|
+
return output.split('\n').map(f => f.trim()).filter(f => f.length > 0);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
throw new Error(`Failed to get diff files against branch '${targetBranch}': ${err.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
_getDiffFilesFromApi(targetBranch) {
|
|
78
|
+
if (!this._api || !this._patToken) {
|
|
79
|
+
throw new Error('API and PAT token are required for API-based diff');
|
|
80
|
+
}
|
|
81
|
+
// Placeholder for API-based diff (e.g., Azure DevOps, GitHub, etc.)
|
|
82
|
+
// Consumers can pass in their own api object that implements `getChangedFiles(targetBranch, patToken)`
|
|
83
|
+
if (typeof this._api.getChangedFiles === 'function') {
|
|
84
|
+
return this._api.getChangedFiles(targetBranch, this._patToken);
|
|
85
|
+
}
|
|
86
|
+
throw new Error('API object must implement getChangedFiles(targetBranch, patToken)');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Fetches branch diff from GitLab compare API via HTTPS
|
|
91
|
+
* @param {string} hostname - GitLab host (e.g. 'gitlab.example.com')
|
|
92
|
+
* @param {string} branch - Target branch to compare against
|
|
93
|
+
* @returns {Promise<string[]>} - Array of changed file paths
|
|
94
|
+
*/
|
|
95
|
+
_fetchBranchDiffFromApi(hostname, branch, projectId) {
|
|
96
|
+
const currentBranch = this._getCurrentBranch();
|
|
97
|
+
const patToken = this._patToken || process.env.ZGIT_TOKEN;
|
|
98
|
+
if (!patToken) {
|
|
99
|
+
return reject(new Error('PAT token is required. Set it via options.patToken or ZGIT_TOKEN env var'));
|
|
100
|
+
}
|
|
101
|
+
if (!projectId) {
|
|
102
|
+
return reject(new Error('GitLab projectId is required for API-based diff'));
|
|
103
|
+
}
|
|
104
|
+
const createBranchDiffChunk = (resolve, reject) => {
|
|
105
|
+
// code duplicate need find solution
|
|
106
|
+
var command;
|
|
107
|
+
const endPoint = `${hostname}/api/v4/projects/${encodeURIComponent(projectId)}/repository/compare?from=${branch}&to=${currentBranch}`;
|
|
108
|
+
try {
|
|
109
|
+
command = `curl --header "PRIVATE-TOKEN:${patToken}" "${endPoint}"`;
|
|
110
|
+
return resolve(parserReleaseDiffRemoteAPI(JSON.parse(execSync(command, {
|
|
111
|
+
cwd: this._cwd
|
|
112
|
+
}))));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
Logger.log(error);
|
|
115
|
+
Logger.log(`\n INFO : If you are using a VPN and encounter an SSL certification issue, ensure that the proxy is enabled for SSH and shell connections.`);
|
|
116
|
+
Logger.log(`\n Make sure that you have access to this repository`);
|
|
117
|
+
reject(error);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
function parserReleaseDiffRemoteAPI(rawresponse) {
|
|
121
|
+
var formatted = [];
|
|
122
|
+
if (rawresponse !== null && rawresponse !== void 0 && rawresponse.diffs) {
|
|
123
|
+
formatted = rawresponse.diffs.map(changeset => {
|
|
124
|
+
return changeset === null || changeset === void 0 ? void 0 : changeset.new_path;
|
|
125
|
+
}).filter(Boolean);
|
|
126
|
+
}
|
|
127
|
+
return formatted;
|
|
128
|
+
}
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
return createBranchDiffChunk(resolve, reject);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async getStagedFiles() {
|
|
134
|
+
try {
|
|
135
|
+
const stdout = await execSync("git diff --staged --name-only").toString();
|
|
136
|
+
const files = stdout.trim().split("\n").filter(Boolean);
|
|
137
|
+
return files;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.log(error);
|
|
140
|
+
throw new Error("Couldn't fetch staged files");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async getChangedFiles(targetBranch, options = {}) {
|
|
144
|
+
let allFiles;
|
|
145
|
+
if (options.useApi && options.hostname && options.projectId) {
|
|
146
|
+
allFiles = await this._fetchBranchDiffFromApi(options.hostname, targetBranch, options.projectId).concat(await this.getStagedFiles());
|
|
147
|
+
} else {
|
|
148
|
+
allFiles = this._getDiffFiles(targetBranch);
|
|
149
|
+
}
|
|
150
|
+
return this._categorizeFiles(allFiles);
|
|
151
|
+
}
|
|
152
|
+
_isSourceFile(filePath) {
|
|
153
|
+
const ext = path.extname(filePath);
|
|
154
|
+
if (!this._sourceExtensions.includes(ext)) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return !this._testPattern.test(filePath);
|
|
158
|
+
}
|
|
159
|
+
_isTestFile(filePath) {
|
|
160
|
+
return this._testPattern.test(filePath);
|
|
161
|
+
}
|
|
162
|
+
_categorizeFiles(files) {
|
|
163
|
+
const sourceFiles = [];
|
|
164
|
+
const testFiles = [];
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
if (this._isTestFile(file)) {
|
|
167
|
+
testFiles.push(file);
|
|
168
|
+
} else if (this._isSourceFile(file)) {
|
|
169
|
+
sourceFiles.push(file);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
sourceFiles,
|
|
174
|
+
testFiles
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
module.exports = BranchDiff;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const glob = require('glob');
|
|
6
|
+
const {
|
|
7
|
+
getRootDirectory
|
|
8
|
+
} = require('../utils/General/RootDirectoryUtils/getRootDirectory');
|
|
9
|
+
class FileResolver {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this._rootdir = getRootDirectory();
|
|
12
|
+
this._cwd = options.cwd || process.cwd();
|
|
13
|
+
this._testSuffixes = options.testSuffixes || ['.test.js', '.test.ts', '.test.jsx', '.test.tsx'];
|
|
14
|
+
this._sourceExtensions = options.sourceExtensions || ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
|
|
15
|
+
}
|
|
16
|
+
findTestForSource(sourceFile) {
|
|
17
|
+
const parsed = path.parse(sourceFile);
|
|
18
|
+
const dir = parsed.dir;
|
|
19
|
+
const name = parsed.name;
|
|
20
|
+
|
|
21
|
+
// Strategy 1: Test file alongside source (e.g., src/foo.js -> src/foo.test.js)
|
|
22
|
+
for (const suffix of this._testSuffixes) {
|
|
23
|
+
const candidate = path.join(dir, `${name}${suffix}`);
|
|
24
|
+
if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
|
|
25
|
+
return candidate;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Strategy 2: Test in __tests__ directory (e.g., src/foo.js -> src/__tests__/foo.test.js)
|
|
30
|
+
for (const suffix of this._testSuffixes) {
|
|
31
|
+
const candidate = path.join(dir, '__tests__', `${name}${suffix}`);
|
|
32
|
+
if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Strategy 3: Mirror structure under test/tests directory
|
|
38
|
+
const testDirs = ['test', 'tests', '__tests__'];
|
|
39
|
+
const srcDirs = ['src', 'lib'];
|
|
40
|
+
for (const srcDir of srcDirs) {
|
|
41
|
+
if (dir.startsWith(srcDir)) {
|
|
42
|
+
const relativePath = dir.substring(srcDir.length);
|
|
43
|
+
for (const testDir of testDirs) {
|
|
44
|
+
for (const suffix of this._testSuffixes) {
|
|
45
|
+
const candidate = path.join(testDir, relativePath, `${name}${suffix}`);
|
|
46
|
+
if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
findSourceForTest(testFile) {
|
|
56
|
+
const parsed = path.parse(testFile);
|
|
57
|
+
const dir = parsed.dir;
|
|
58
|
+
let name = parsed.name;
|
|
59
|
+
|
|
60
|
+
// Remove .test suffix
|
|
61
|
+
for (const suffix of ['.test', '.spec']) {
|
|
62
|
+
if (name.endsWith(suffix)) {
|
|
63
|
+
name = name.slice(0, -suffix.length);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Strategy 1: Source file alongside test
|
|
69
|
+
for (const ext of this._sourceExtensions) {
|
|
70
|
+
const candidate = path.join(dir, `${name}${ext}`);
|
|
71
|
+
if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
|
|
72
|
+
return candidate;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Strategy 2: Source in parent (when test is in __tests__)
|
|
77
|
+
if (dir.includes('__tests__')) {
|
|
78
|
+
const parentDir = dir.replace(/__tests__\/?/, '');
|
|
79
|
+
for (const ext of this._sourceExtensions) {
|
|
80
|
+
const candidate = path.join(parentDir, `${name}${ext}`);
|
|
81
|
+
if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
|
|
82
|
+
return candidate;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Strategy 3: Mirror test dir structure back to src/lib
|
|
88
|
+
const testDirs = ['test', 'tests', '__tests__'];
|
|
89
|
+
const srcDirs = ['src', 'lib'];
|
|
90
|
+
for (const testDir of testDirs) {
|
|
91
|
+
if (dir.startsWith(testDir)) {
|
|
92
|
+
const relativePath = dir.substring(testDir.length);
|
|
93
|
+
for (const srcDir of srcDirs) {
|
|
94
|
+
for (const ext of this._sourceExtensions) {
|
|
95
|
+
const candidate = path.join(srcDir, relativePath, `${name}${ext}`);
|
|
96
|
+
if (fs.existsSync(path.resolve(this._rootdir, candidate))) {
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
resolveSourceTestPairs(sourceFiles, testFiles) {
|
|
106
|
+
const pairs = [];
|
|
107
|
+
const resolvedSources = new Set();
|
|
108
|
+
const resolvedTests = new Set();
|
|
109
|
+
|
|
110
|
+
// Map source -> test
|
|
111
|
+
for (const source of sourceFiles) {
|
|
112
|
+
const normalizedSource = source.replace(/\\/g, '/');
|
|
113
|
+
const test = this.findTestForSource(normalizedSource);
|
|
114
|
+
if (test) {
|
|
115
|
+
const normalizedTest = test.replace(/\\/g, '/');
|
|
116
|
+
pairs.push({
|
|
117
|
+
source: normalizedSource,
|
|
118
|
+
test: normalizedTest
|
|
119
|
+
});
|
|
120
|
+
resolvedSources.add(normalizedSource);
|
|
121
|
+
resolvedTests.add(normalizedTest);
|
|
122
|
+
} else {
|
|
123
|
+
// Source file with no matching test
|
|
124
|
+
pairs.push({
|
|
125
|
+
source: normalizedSource,
|
|
126
|
+
test: null
|
|
127
|
+
});
|
|
128
|
+
resolvedSources.add(normalizedSource);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Map remaining test -> source
|
|
133
|
+
for (const test of testFiles) {
|
|
134
|
+
const normalizedTest = test.replace(/\\/g, '/');
|
|
135
|
+
if (resolvedTests.has(normalizedTest)) continue;
|
|
136
|
+
const source = this.findSourceForTest(normalizedTest);
|
|
137
|
+
if (source && !resolvedSources.has(source.replace(/\\/g, '/'))) {
|
|
138
|
+
pairs.push({
|
|
139
|
+
source: source.replace(/\\/g, '/'),
|
|
140
|
+
test: normalizedTest
|
|
141
|
+
});
|
|
142
|
+
resolvedSources.add(source.replace(/\\/g, '/'));
|
|
143
|
+
}
|
|
144
|
+
resolvedTests.add(normalizedTest);
|
|
145
|
+
}
|
|
146
|
+
return pairs;
|
|
147
|
+
}
|
|
148
|
+
collectFiles(directory, pattern) {
|
|
149
|
+
const fullPattern = path.join(directory, pattern || '**/*.{js,ts,jsx,tsx}').replace(/\\/g, '/');
|
|
150
|
+
try {
|
|
151
|
+
const files = glob.sync(fullPattern, {
|
|
152
|
+
cwd: this._rootdir,
|
|
153
|
+
nodir: true
|
|
154
|
+
});
|
|
155
|
+
return files;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
throw new Error(`Failed to collect files from '${directory}': ${err.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
collectSourceFiles(srcDir) {
|
|
161
|
+
const allFiles = this.collectFiles(srcDir, '**/*.{js,ts,jsx,tsx,mjs,cjs}');
|
|
162
|
+
const testPattern = /\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/;
|
|
163
|
+
return allFiles.filter(f => !testPattern.test(f));
|
|
164
|
+
}
|
|
165
|
+
collectTestFiles(testDir) {
|
|
166
|
+
const allFiles = this.collectFiles(testDir, '**/*.{test,spec}.{js,ts,jsx,tsx}');
|
|
167
|
+
return allFiles;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
module.exports = FileResolver;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const MutationRunner = require('./mutationRunner');
|
|
4
|
+
const MutationCli = require('./mutationCli');
|
|
5
|
+
const StrykerWrapper = require('./strykerWrapper');
|
|
6
|
+
const BranchDiff = require('./branchDiff');
|
|
7
|
+
const FileResolver = require('./fileResolver');
|
|
8
|
+
const ReportGenerator = require('./reportGenerator');
|
|
9
|
+
module.exports = {
|
|
10
|
+
MutationRunner,
|
|
11
|
+
MutationCli,
|
|
12
|
+
StrykerWrapper,
|
|
13
|
+
BranchDiff,
|
|
14
|
+
FileResolver,
|
|
15
|
+
ReportGenerator
|
|
16
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const MutationRunner = require('./mutationRunner');
|
|
4
|
+
class MutationCli {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this._options = options;
|
|
7
|
+
}
|
|
8
|
+
parseArgs(args) {
|
|
9
|
+
const parsed = {
|
|
10
|
+
command: null,
|
|
11
|
+
branch: null,
|
|
12
|
+
src: null,
|
|
13
|
+
test: null,
|
|
14
|
+
useApi: false,
|
|
15
|
+
outputDir: null,
|
|
16
|
+
outputFileName: null,
|
|
17
|
+
logLevel: 'info',
|
|
18
|
+
hostname: null
|
|
19
|
+
};
|
|
20
|
+
for (const arg of args) {
|
|
21
|
+
if (arg === 'mutate') {
|
|
22
|
+
parsed.command = 'mutate';
|
|
23
|
+
} else if (arg.startsWith('--branch=')) {
|
|
24
|
+
parsed.branch = arg.split('=')[1];
|
|
25
|
+
} else if (arg.startsWith('--src=')) {
|
|
26
|
+
parsed.src = arg.split('=')[1];
|
|
27
|
+
} else if (arg.startsWith('--test=')) {
|
|
28
|
+
parsed.test = arg.split('=')[1];
|
|
29
|
+
} else if (arg === '--useApi') {
|
|
30
|
+
parsed.useApi = true;
|
|
31
|
+
} else if (arg.startsWith('--outputDir=')) {
|
|
32
|
+
parsed.outputDir = arg.split('=')[1];
|
|
33
|
+
} else if (arg.startsWith('--outputFileName=')) {
|
|
34
|
+
parsed.outputFileName = arg.split('=')[1];
|
|
35
|
+
} else if (arg.startsWith('--logLevel=')) {
|
|
36
|
+
parsed.logLevel = arg.split('=')[1];
|
|
37
|
+
} else if (arg.startsWith('--hostname=')) {
|
|
38
|
+
parsed.hostname = arg.split('=')[1];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
validate(parsed) {
|
|
44
|
+
if (parsed.command !== 'mutate') {
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
error: 'Unknown command. Expected "mutate".'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const hasBranch = !!parsed.branch;
|
|
51
|
+
const hasSrcTest = !!parsed.src && !!parsed.test;
|
|
52
|
+
if (!hasBranch && !hasSrcTest) {
|
|
53
|
+
return {
|
|
54
|
+
valid: false,
|
|
55
|
+
error: 'Missing arguments. Usage:\n' + ' npx ZDLintFramework mutate --branch=<branch>\n' + ' npx ZDLintFramework mutate --src=<sourcedir> --test=<testdir>'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (hasBranch && hasSrcTest) {
|
|
59
|
+
return {
|
|
60
|
+
valid: false,
|
|
61
|
+
error: 'Cannot use both --branch and --src/--test together. Choose one mode.'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
valid: true
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async execute(args) {
|
|
69
|
+
const parsed = this.parseArgs(args);
|
|
70
|
+
const validation = this.validate(parsed);
|
|
71
|
+
if (!validation.valid) {
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
error: validation.error
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const runner = new MutationRunner({
|
|
78
|
+
cwd: this._options.cwd,
|
|
79
|
+
api: this._options.api,
|
|
80
|
+
patToken: this._options.patToken,
|
|
81
|
+
jestConfig: this._options.jestConfig,
|
|
82
|
+
outputDir: parsed.outputDir || this._options.outputDir,
|
|
83
|
+
outputFileName: parsed.outputFileName || this._options.outputFileName,
|
|
84
|
+
logLevel: parsed.logLevel,
|
|
85
|
+
concurrency: this._options.concurrency,
|
|
86
|
+
timeoutMS: this._options.timeoutMS
|
|
87
|
+
});
|
|
88
|
+
try {
|
|
89
|
+
let result;
|
|
90
|
+
if (parsed.branch) {
|
|
91
|
+
result = await runner.runByBranch(parsed.branch, {
|
|
92
|
+
useApi: parsed.useApi,
|
|
93
|
+
hostname: this._options.hostname,
|
|
94
|
+
projectId: this._options.projectId
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
result = await runner.runByDirectory(parsed.src, parsed.test);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
...result
|
|
102
|
+
};
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
error: err.message
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
module.exports = MutationCli;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
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>');
|
|
35
|
+
}
|
|
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: []
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
report,
|
|
58
|
+
filePath,
|
|
59
|
+
message: 'No changed source or test files found in branch diff.'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
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
|
+
}
|
|
84
|
+
|
|
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
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
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
|
+
}
|
|
112
|
+
|
|
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
|
+
}
|
|
133
|
+
|
|
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
|
+
}
|
|
165
|
+
|
|
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
|
+
}
|
|
173
|
+
});
|
|
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,
|
|
183
|
+
filePairs: pairs
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
report,
|
|
187
|
+
filePath
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
_buildEmptyResult() {
|
|
191
|
+
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
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
module.exports = MutationRunner;
|