@zohodesk/codestandard-validator 1.2.4-exp-1 → 1.2.4-exp-3
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 +6 -1
- package/build/ai/run-review.js +1 -1
- package/build/chunk/chunk_Restriction.js +12 -2
- package/build/hooks/Precommit/pre-commit-default.js +155 -140
- package/build/hooks/hook.js +5 -3
- 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/ConfigFileUtils/getLintConfiguration.js +21 -10
- package/build/utils/FileAndFolderOperations/filterFiles.js +8 -6
- package/build/utils/FileAndFolderOperations/removeFolder.js +2 -2
- package/build/utils/General/Config.js +23 -2
- package/jsonUtils/MandatoryListRules.js +1 -1
- package/package.json +2 -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,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;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
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 => ({
|
|
26
|
+
id: m.id,
|
|
27
|
+
mutatorName: m.mutatorName,
|
|
28
|
+
replacement: m.replacement,
|
|
29
|
+
fileName: m.fileName || m.sourceFile,
|
|
30
|
+
status: m.status,
|
|
31
|
+
location: m.location || null,
|
|
32
|
+
killedBy: m.killedBy || [],
|
|
33
|
+
coveredBy: m.coveredBy || [],
|
|
34
|
+
description: m.description || ''
|
|
35
|
+
}))
|
|
36
|
+
};
|
|
37
|
+
return report;
|
|
38
|
+
}
|
|
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
|
+
};
|
|
51
|
+
}
|
|
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;
|
|
62
|
+
}
|
|
63
|
+
generateAndWrite(mutationResult, context = {}) {
|
|
64
|
+
const report = this.generate(mutationResult, context);
|
|
65
|
+
const filePath = this.write(report);
|
|
66
|
+
return {
|
|
67
|
+
report,
|
|
68
|
+
filePath
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
module.exports = ReportGenerator;
|