@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.
@@ -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,3 @@
1
+ {
2
+ "defaultPattern": "src/**/*.js"
3
+ }
@@ -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;