@ai-dev-tools/csharp-copilot-core 0.0.33 → 0.0.34
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/out/batch/generateCodeTests.js +22 -6
- package/out/batch/generateCodeTests.js.map +1 -1
- package/out/changedFilesProcessor/processChangedFiles.d.ts +9 -0
- package/out/changedFilesProcessor/processChangedFiles.js +156 -0
- package/out/changedFilesProcessor/processChangedFiles.js.map +1 -0
- package/out/codebk/prompts/buildAfGuidelines_20251226.liquid +15 -0
- package/out/codebk/prompts/general/generalUtGuidelines_20251226.liquid +16 -0
- package/out/codebk/prompts/xap/xapUtGuideline_20251225.liquid +37 -0
- package/out/codebk/prompts/xap/xapUtGuideline_20251226.liquid +49 -0
- package/out/command/index.js +32 -0
- package/out/command/index.js.map +1 -1
- package/out/command/utGenWrapper.js +85 -4
- package/out/command/utGenWrapper.js.map +1 -1
- package/out/gen/autoFix.d.ts +2 -2
- package/out/gen/autoFix.js +88 -31
- package/out/gen/autoFix.js.map +1 -1
- package/out/gen/csharpUtGen.d.ts +1 -1
- package/out/gen/csharpUtGen.js +16 -13
- package/out/gen/csharpUtGen.js.map +1 -1
- package/out/gen/ensureValidLLMResponse.d.ts +5 -1
- package/out/gen/ensureValidLLMResponse.js +15 -3
- package/out/gen/ensureValidLLMResponse.js.map +1 -1
- package/out/gen/postGen/postGenMoreUTProcess.d.ts +25 -0
- package/out/gen/postGen/postGenMoreUTProcess.js +502 -0
- package/out/gen/postGen/postGenMoreUTProcess.js.map +1 -0
- package/out/gen/postGen/postGenProcess.d.ts +1 -1
- package/out/gen/postGen/postGenProcess.js +7 -7
- package/out/gen/postGen/postGenProcess.js.map +1 -1
- package/out/gen/postGen/repairRequiredNameSpaces.d.ts +17 -0
- package/out/gen/postGen/repairRequiredNameSpaces.js +87 -20
- package/out/gen/postGen/repairRequiredNameSpaces.js.map +1 -1
- package/out/llm/preparePrompt.d.ts +2 -1
- package/out/llm/preparePrompt.js +38 -4
- package/out/llm/preparePrompt.js.map +1 -1
- package/out/llm/prompt/buildAfGuidelines.liquid +8 -8
- package/out/llm/prompt/general/generalUtGuidelines.liquid +3 -0
- package/out/llm/prompt/moreUT/generateMoreUTSourceCode.liquid +4 -0
- package/out/llm/prompt/moreUT/generateMoreUtAutoFix.liquid +45 -0
- package/out/llm/prompt/moreUT/generateMoreUtTemplate.liquid +115 -0
- package/out/llm/prompt/moreUT/generateMoreUtTestCode.liquid +5 -0
- package/out/llm/prompt/moreUT/utGenerationGuidelines.liquid +15 -0
- package/out/llm/prompt/xap/xapUtGuideline.liquid +66 -15
- package/out/types/changedFilesResult.d.ts +37 -0
- package/out/types/changedFilesResult.js +3 -0
- package/out/types/changedFilesResult.js.map +1 -0
- package/out/types/constants.d.ts +1 -0
- package/out/types/constants.js +2 -1
- package/out/types/constants.js.map +1 -1
- package/out/types/genResult.d.ts +1 -1
- package/out/types/genResult.js.map +1 -1
- package/out/utils/checkXapCode.d.ts +1 -1
- package/out/utils/checkXapCode.js +14 -2
- package/out/utils/checkXapCode.js.map +1 -1
- package/out/utils/fileUtils.d.ts +1 -0
- package/out/utils/fileUtils.js +13 -0
- package/out/utils/fileUtils.js.map +1 -1
- package/out/utils/getCodeStructurePath.d.ts +4 -0
- package/out/utils/getCodeStructurePath.js +30 -0
- package/out/utils/getCodeStructurePath.js.map +1 -1
- package/out/utils/getTestFile.d.ts +18 -8
- package/out/utils/getTestFile.js +734 -59
- package/out/utils/getTestFile.js.map +1 -1
- package/out/utils/removeFailedTestMethods.d.ts +8 -0
- package/out/utils/removeFailedTestMethods.js +363 -0
- package/out/utils/removeFailedTestMethods.js.map +1 -1
- package/out/utils/verifyGeneratedCode.d.ts +2 -0
- package/out/utils/verifyGeneratedCode.js +17 -0
- package/out/utils/verifyGeneratedCode.js.map +1 -0
- package/package.json +4 -3
package/out/utils/getTestFile.js
CHANGED
|
@@ -33,15 +33,120 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
37
|
-
exports.
|
|
36
|
+
exports.isTestFile = isTestFile;
|
|
37
|
+
exports.determineTestFilePath = determineTestFilePath;
|
|
38
38
|
exports.getOrCreatTestFile = getOrCreatTestFile;
|
|
39
39
|
const fs = __importStar(require("fs"));
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const getCodeStructurePath_1 = require("./getCodeStructurePath");
|
|
42
|
+
const getOrCreateDir_1 = require("./getOrCreateDir");
|
|
42
43
|
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
43
44
|
const TEST_NAME_REGEX = /(Test|Tests)/i;
|
|
44
45
|
const TEST_ATTR_REGEX = /\[(Fact|Theory|TestClass|TestMethod|Test)\]/;
|
|
46
|
+
/**
|
|
47
|
+
* Check if a file path is a test file based on common naming conventions.
|
|
48
|
+
* Checks if the file name (without .cs extension) ends with test, tests, unittest, or unittests.
|
|
49
|
+
*/
|
|
50
|
+
function isTestFile(filepath) {
|
|
51
|
+
const fileName = path.basename(filepath).toLowerCase();
|
|
52
|
+
const fileNameWithoutExt = fileName.replace(/\.cs$/, '');
|
|
53
|
+
return fileNameWithoutExt.endsWith('test') ||
|
|
54
|
+
fileNameWithoutExt.endsWith('tests') ||
|
|
55
|
+
fileNameWithoutExt.endsWith('unittest') ||
|
|
56
|
+
fileNameWithoutExt.endsWith('unittests');
|
|
57
|
+
}
|
|
58
|
+
// Modifier pattern for C# type declarations (sealed, abstract, static, partial)
|
|
59
|
+
const MODIFIER_PATTERN = `(?:sealed|abstract|static|partial)?\\s*`;
|
|
60
|
+
// Helper function to create test file name pattern
|
|
61
|
+
function getTestFileNamePattern(sourceFileName) {
|
|
62
|
+
return new RegExp(`^${sourceFileName}(Unit)?(\\.)?(Test|Tests)$`);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Extract words from a name string (e.g., project name, file name)
|
|
66
|
+
* Splits by camelCase and special characters, returns lowercase words
|
|
67
|
+
*
|
|
68
|
+
* @param name - The name string to extract words from
|
|
69
|
+
* @returns Array of lowercase words
|
|
70
|
+
*/
|
|
71
|
+
function extractWordsFromName(name) {
|
|
72
|
+
return name
|
|
73
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
74
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.map(w => w.toLowerCase());
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Calculate word-level matching score between two names
|
|
80
|
+
* Returns the count of matching words
|
|
81
|
+
*
|
|
82
|
+
* @param name1 - First name to compare
|
|
83
|
+
* @param name2 - Second name to compare
|
|
84
|
+
* @returns Number of matching words
|
|
85
|
+
*/
|
|
86
|
+
function calculateWordMatchCount(name1, name2) {
|
|
87
|
+
const words1 = extractWordsFromName(name1);
|
|
88
|
+
const words2 = extractWordsFromName(name2);
|
|
89
|
+
const words1Set = new Set(words1);
|
|
90
|
+
return words2.filter(w => words1Set.has(w)).length;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get all subfolder names under a directory (recursive)
|
|
94
|
+
* Skips: bin, obj, .vs directories
|
|
95
|
+
*
|
|
96
|
+
* @param dirPath - Path to directory
|
|
97
|
+
* @returns Set of lowercase folder names
|
|
98
|
+
*/
|
|
99
|
+
function getSubfolderNamesRecursive(dirPath) {
|
|
100
|
+
const subfolders = new Set();
|
|
101
|
+
try {
|
|
102
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
if (entry.isDirectory() && entry.name !== 'bin' && entry.name !== 'obj' && entry.name !== '.vs') {
|
|
105
|
+
subfolders.add(entry.name.toLowerCase());
|
|
106
|
+
// Recursively get subfolders
|
|
107
|
+
const subPath = path.join(dirPath, entry.name);
|
|
108
|
+
const deepSubfolders = getSubfolderNamesRecursive(subPath);
|
|
109
|
+
deepSubfolders.forEach(f => subfolders.add(f));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
// Ignore errors for inaccessible directories
|
|
115
|
+
}
|
|
116
|
+
return subfolders;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get all .cs file names under a directory (recursive)
|
|
120
|
+
* Skips: bin, obj, .vs directories
|
|
121
|
+
*
|
|
122
|
+
* @param dirPath - Path to directory
|
|
123
|
+
* @returns Set of lowercase file names (without .cs extension)
|
|
124
|
+
*/
|
|
125
|
+
function getFileNamesRecursive(dirPath) {
|
|
126
|
+
const fileNames = new Set();
|
|
127
|
+
try {
|
|
128
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (entry.name === 'bin' || entry.name === 'obj' || entry.name === '.vs') {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
134
|
+
if (entry.isDirectory()) {
|
|
135
|
+
// Recursively get file names from subdirectories
|
|
136
|
+
const subFileNames = getFileNamesRecursive(fullPath);
|
|
137
|
+
subFileNames.forEach(f => fileNames.add(f));
|
|
138
|
+
}
|
|
139
|
+
else if (entry.isFile() && entry.name.endsWith('.cs')) {
|
|
140
|
+
// Add file name without .cs extension
|
|
141
|
+
fileNames.add(entry.name.slice(0, -3).toLowerCase());
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
// Ignore errors for inaccessible directories
|
|
147
|
+
}
|
|
148
|
+
return fileNames;
|
|
149
|
+
}
|
|
45
150
|
function findAllCsprojFiles(dir, excludedPaths) {
|
|
46
151
|
const results = [];
|
|
47
152
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -134,47 +239,197 @@ function isFalconUnitTestProject(csprojPath) {
|
|
|
134
239
|
}
|
|
135
240
|
function isProjectNameMatchedTestProject(csprojPath, projectAssemblyName, projectName) {
|
|
136
241
|
const testProjectName = path.basename(csprojPath, '.csproj').toLowerCase();
|
|
137
|
-
const
|
|
138
|
-
if
|
|
242
|
+
const projectNameLower = projectName.toLowerCase();
|
|
243
|
+
// Check if test project name exactly matches: projectName + suffix
|
|
244
|
+
const suffixes = ['.tests', '.test', '.unittest', '.unittests', 'tests', 'test'];
|
|
245
|
+
const exactMatch = suffixes.some(suffix => testProjectName === projectNameLower + suffix);
|
|
246
|
+
if (exactMatch) {
|
|
139
247
|
return true;
|
|
140
248
|
}
|
|
249
|
+
// Fallback: check assembly name with exact match
|
|
141
250
|
const testProjectAssemblyName = (0, getCodeStructurePath_1.getAssemblyName)(csprojPath);
|
|
142
|
-
|
|
251
|
+
if (testProjectAssemblyName) {
|
|
252
|
+
const assemblyNameLower = testProjectAssemblyName.toLowerCase();
|
|
253
|
+
const projectAssemblyNameLower = projectAssemblyName.toLowerCase();
|
|
254
|
+
return suffixes.some(suffix => assemblyNameLower === projectAssemblyNameLower + suffix);
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
143
257
|
}
|
|
144
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Search for test projects in repository directories
|
|
260
|
+
* Walks up from source file directory and collects all test projects that reference the source project
|
|
261
|
+
*
|
|
262
|
+
* @param csFilePath - Path to the source .cs file
|
|
263
|
+
* @param repoRoot - Repository root directory
|
|
264
|
+
* @param projectFilePath - Path to source project's .csproj
|
|
265
|
+
* @param normalizedProjectPath - Normalized path to source project
|
|
266
|
+
* @returns Array of test project paths that reference the source project
|
|
267
|
+
*/
|
|
268
|
+
function searchTestProjectsInRepositories(csFilePath, repoRoot, projectFilePath, normalizedProjectPath) {
|
|
145
269
|
let currentDir = path.dirname(csFilePath);
|
|
146
|
-
const
|
|
147
|
-
const projectFilePath = (0, getCodeStructurePath_1.getSourceFileCsprojFilePath)(csFilePath);
|
|
148
|
-
const projectName = path.basename(projectFilePath, '.csproj');
|
|
149
|
-
const projectAssemblyName = (0, getCodeStructurePath_1.getAssemblyName)(projectFilePath);
|
|
270
|
+
const possibleTestProjects = [];
|
|
150
271
|
const excludedPaths = [currentDir];
|
|
151
272
|
const repoRootParentPath = path.dirname(repoRoot);
|
|
152
|
-
// Start from the parent directory of the source file, because test project is usually in the parent directory
|
|
153
273
|
currentDir = path.dirname(currentDir);
|
|
154
274
|
while (currentDir !== repoRootParentPath) {
|
|
155
275
|
const csprojFiles = findAllCsprojFiles(currentDir, excludedPaths);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
276
|
+
// Filter csproj files by standard test project naming patterns
|
|
277
|
+
const testProjectCandidates = csprojFiles.filter(csproj => {
|
|
278
|
+
const rawName = path.basename(csproj, '.csproj');
|
|
279
|
+
const csprojName = rawName.toLowerCase();
|
|
280
|
+
// Standard test naming: ends with .tests, .test, .unittests, .unittest, Tests, or Test
|
|
281
|
+
if (csprojName.endsWith('.tests') ||
|
|
282
|
+
csprojName.endsWith('.test') ||
|
|
283
|
+
csprojName.endsWith('.unittests') ||
|
|
284
|
+
csprojName.endsWith('.unittest') ||
|
|
285
|
+
csprojName.endsWith('tests') ||
|
|
286
|
+
csprojName.endsWith('test')) {
|
|
287
|
+
return true;
|
|
162
288
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
289
|
+
// Word-level match: any word ends with "test" or "tests"
|
|
290
|
+
const words = extractWordsFromName(rawName);
|
|
291
|
+
return words.some(w => w.endsWith('test') || w.endsWith('tests'));
|
|
292
|
+
});
|
|
293
|
+
for (const csproj of testProjectCandidates) {
|
|
294
|
+
// Check if this csproj references the source project
|
|
295
|
+
const projectReferences = getProjectReferencesFromCsproj(csproj);
|
|
296
|
+
const referencesSourceProject = checkTestProjectReferencesSourceProject(csproj, normalizedProjectPath, projectReferences);
|
|
297
|
+
if (!referencesSourceProject) {
|
|
298
|
+
continue;
|
|
168
299
|
}
|
|
169
|
-
|
|
170
|
-
|
|
300
|
+
const projectName = path.basename(projectFilePath, '.csproj');
|
|
301
|
+
const projectAssemblyName = (0, getCodeStructurePath_1.getAssemblyName)(projectFilePath);
|
|
302
|
+
if (isProjectNameMatchedTestProject(csproj, projectAssemblyName, projectName)) {
|
|
303
|
+
return [csproj];
|
|
171
304
|
}
|
|
305
|
+
// Collect all test projects that reference the source project
|
|
306
|
+
possibleTestProjects.push(csproj);
|
|
172
307
|
}
|
|
173
|
-
// Add current directory to excluded paths to avoid re-checking
|
|
174
308
|
excludedPaths.push(currentDir);
|
|
175
309
|
currentDir = path.dirname(currentDir);
|
|
176
310
|
}
|
|
177
|
-
return
|
|
311
|
+
return possibleTestProjects;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Filter test projects by subfolder matching
|
|
315
|
+
* Returns projects sorted by match count, or proceeds to file name matching if all zero
|
|
316
|
+
*
|
|
317
|
+
* @param possibleTestProjects - List of test project paths
|
|
318
|
+
* @param projectFilePath - Path to source project's .csproj
|
|
319
|
+
* @returns Filtered projects with scores, or null if needs file name matching
|
|
320
|
+
*/
|
|
321
|
+
function filterBySubfolderMatching(possibleTestProjects, projectFilePath) {
|
|
322
|
+
const sourceProjectDir = path.dirname(projectFilePath);
|
|
323
|
+
const sourceSubfolders = getSubfolderNamesRecursive(sourceProjectDir);
|
|
324
|
+
const projectsWithScores = possibleTestProjects.map(csproj => {
|
|
325
|
+
const testProjectDir = path.dirname(csproj);
|
|
326
|
+
const testSubfolders = getSubfolderNamesRecursive(testProjectDir);
|
|
327
|
+
let matchCount = 0;
|
|
328
|
+
testSubfolders.forEach(folderName => {
|
|
329
|
+
if (sourceSubfolders.has(folderName)) {
|
|
330
|
+
matchCount++;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
return { csproj, matchCount };
|
|
334
|
+
});
|
|
335
|
+
projectsWithScores.sort((a, b) => b.matchCount - a.matchCount);
|
|
336
|
+
const allZero = projectsWithScores.every(p => p.matchCount === 0);
|
|
337
|
+
return allZero ? null : projectsWithScores;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Filter test projects by file name matching with test suffixes
|
|
341
|
+
* Returns filtered projects if any matches found
|
|
342
|
+
*
|
|
343
|
+
* @param projectsWithScores - Projects with scores
|
|
344
|
+
* @param projectFilePath - Path to source project's .csproj
|
|
345
|
+
* @returns Filtered projects by file name, or null if no matches
|
|
346
|
+
*/
|
|
347
|
+
function filterByFileNameMatching(projectsWithScores, projectFilePath) {
|
|
348
|
+
const sourceProjectDir = path.dirname(projectFilePath);
|
|
349
|
+
const sourceFileNames = getFileNamesRecursive(sourceProjectDir);
|
|
350
|
+
const testSuffixes = ['.tests', '.test', '.unittests', '.unittest', 'tests', 'test'];
|
|
351
|
+
const filteredProjectsWithScores = projectsWithScores.filter(projectScore => {
|
|
352
|
+
const testProjectDir = path.dirname(projectScore.csproj);
|
|
353
|
+
const testFileNames = getFileNamesRecursive(testProjectDir);
|
|
354
|
+
for (const sourceFileName of sourceFileNames) {
|
|
355
|
+
for (const suffix of testSuffixes) {
|
|
356
|
+
const expectedTestFileName = (sourceFileName + suffix).toLowerCase();
|
|
357
|
+
if (testFileNames.has(expectedTestFileName)) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
});
|
|
364
|
+
return filteredProjectsWithScores.length > 0 ? filteredProjectsWithScores : null;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Filter test projects by word-level matching
|
|
368
|
+
* Returns top 2 projects with highest word match count
|
|
369
|
+
*
|
|
370
|
+
* @param projectsWithScores - Projects with scores
|
|
371
|
+
* @param projectName - Name of source project
|
|
372
|
+
* @returns Top 2 projects by word match count, or empty array if no matches
|
|
373
|
+
*/
|
|
374
|
+
function filterByWordLevelMatching(projectsWithScores, projectName) {
|
|
375
|
+
const projectsWithWordScores = projectsWithScores.map(projectScore => {
|
|
376
|
+
const testProjectName = path.basename(projectScore.csproj, '.csproj');
|
|
377
|
+
const wordMatchCount = calculateWordMatchCount(projectName, testProjectName);
|
|
378
|
+
return { ...projectScore, wordMatchCount };
|
|
379
|
+
});
|
|
380
|
+
const projectsWithMatches = projectsWithWordScores.filter(p => p.wordMatchCount > 0);
|
|
381
|
+
if (projectsWithMatches.length === 0) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
projectsWithMatches.sort((a, b) => b.wordMatchCount - a.wordMatchCount);
|
|
385
|
+
const countToReturn = Math.min(2, projectsWithMatches.length);
|
|
386
|
+
return projectsWithMatches.slice(0, countToReturn).map(p => p.csproj);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Select top projects by score percentage or minimum count
|
|
390
|
+
*
|
|
391
|
+
* @param projectsWithScores - Projects with scores
|
|
392
|
+
* @param minCount - Minimum projects to return (default 2)
|
|
393
|
+
* @returns Array of selected project paths
|
|
394
|
+
*/
|
|
395
|
+
function selectTopProjectsByScore(projectsWithScores, minCount = 2) {
|
|
396
|
+
const countToReturn = Math.max(minCount, Math.ceil(projectsWithScores.length * 0.3));
|
|
397
|
+
return projectsWithScores.slice(0, countToReturn).map(p => p.csproj);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Find possible test projects for a C# source file
|
|
401
|
+
* Uses multi-level matching: folder names -> file names -> word-level matching
|
|
402
|
+
*
|
|
403
|
+
* @param csFilePath - Path to the source .cs file
|
|
404
|
+
* @returns Array of possible test project paths
|
|
405
|
+
*/
|
|
406
|
+
function findPossibleTestProjectsFromCsFile(csFilePath) {
|
|
407
|
+
const repoRoot = (0, getCodeStructurePath_1.getCodeRepoRoot)(csFilePath);
|
|
408
|
+
const projectFilePath = (0, getCodeStructurePath_1.getSourceFileCsprojFilePath)(csFilePath);
|
|
409
|
+
const projectName = path.basename(projectFilePath, '.csproj');
|
|
410
|
+
const normalizedProjectPath = path.normalize(projectFilePath);
|
|
411
|
+
// Search for test projects in repository
|
|
412
|
+
const possibleTestProjects = searchTestProjectsInRepositories(csFilePath, repoRoot, projectFilePath, normalizedProjectPath);
|
|
413
|
+
if (possibleTestProjects.length === 0) {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
if (possibleTestProjects.length === 1) {
|
|
417
|
+
return possibleTestProjects;
|
|
418
|
+
}
|
|
419
|
+
// Try subfolder-level matching
|
|
420
|
+
const subfolderMatchResults = filterBySubfolderMatching(possibleTestProjects, projectFilePath);
|
|
421
|
+
if (subfolderMatchResults !== null) {
|
|
422
|
+
return selectTopProjectsByScore(subfolderMatchResults);
|
|
423
|
+
}
|
|
424
|
+
// If subfolder matching is all zero, try file name matching
|
|
425
|
+
const projectsWithScores = possibleTestProjects.map(csproj => ({ csproj, matchCount: 0 }));
|
|
426
|
+
const fileNameMatchResults = filterByFileNameMatching(projectsWithScores, projectFilePath);
|
|
427
|
+
if (fileNameMatchResults !== null) {
|
|
428
|
+
return selectTopProjectsByScore(fileNameMatchResults);
|
|
429
|
+
}
|
|
430
|
+
// If file name matching fails, try word-level matching
|
|
431
|
+
const wordLevelResults = filterByWordLevelMatching(projectsWithScores, projectName);
|
|
432
|
+
return wordLevelResults.length > 0 ? wordLevelResults : [];
|
|
178
433
|
}
|
|
179
434
|
function findPossibleTestFiles(testProjectDir, className) {
|
|
180
435
|
const results = [];
|
|
@@ -201,27 +456,205 @@ function fileContainsTestAttributes(filePath) {
|
|
|
201
456
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
202
457
|
return TEST_ATTR_REGEX.test(content);
|
|
203
458
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
459
|
+
/**
|
|
460
|
+
* Check if a test project references a source project.
|
|
461
|
+
*
|
|
462
|
+
* @param testCsprojPath - Path to the test project's csproj file
|
|
463
|
+
* @param sourceCsprojPath - Path to the source project's csproj file
|
|
464
|
+
* @param projectReferences - Project references from the test csproj
|
|
465
|
+
* @returns True if test project references the source project, false otherwise
|
|
466
|
+
*/
|
|
467
|
+
function checkTestProjectReferencesSourceProject(testCsprojPath, sourceCsprojPath, projectReferences) {
|
|
468
|
+
const normalizedSourceCsprojPath = path.normalize(sourceCsprojPath).toLowerCase();
|
|
469
|
+
return projectReferences.some(ref => {
|
|
470
|
+
const resolvedRefPath = path.normalize(path.resolve(path.dirname(testCsprojPath), ref)).toLowerCase();
|
|
471
|
+
return resolvedRefPath === normalizedSourceCsprojPath;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
function getProjectReferencesFromCsproj(csprojPath) {
|
|
475
|
+
const projectReferences = [];
|
|
476
|
+
try {
|
|
477
|
+
const xmlContent = fs.readFileSync(csprojPath, 'utf-8');
|
|
478
|
+
const parser = new fast_xml_parser_1.XMLParser({
|
|
479
|
+
ignoreAttributes: false,
|
|
480
|
+
attributeNamePrefix: ''
|
|
481
|
+
});
|
|
482
|
+
const projectData = parser.parse(xmlContent);
|
|
483
|
+
const project = projectData.Project;
|
|
484
|
+
const itemGroups = Array.isArray(project.ItemGroup) ? project.ItemGroup : [project.ItemGroup];
|
|
485
|
+
for (const group of itemGroups) {
|
|
486
|
+
if (!group)
|
|
487
|
+
continue;
|
|
488
|
+
const projectRefs = group.ProjectReference;
|
|
489
|
+
if (!projectRefs)
|
|
490
|
+
continue;
|
|
491
|
+
const refList = Array.isArray(projectRefs) ? projectRefs : [projectRefs];
|
|
492
|
+
for (const ref of refList) {
|
|
493
|
+
if (ref.Include) {
|
|
494
|
+
projectReferences.push(ref.Include);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
console.error(`Failed to parse csproj: ${csprojPath}`, err);
|
|
210
501
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
502
|
+
return projectReferences;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Calculate matching score between test file path and source file path based on directory structure.
|
|
506
|
+
* Returns two scores: continuous matching folders from end, and total matching folders.
|
|
507
|
+
*
|
|
508
|
+
* @param testRelativePath - Relative path of test file from test project root
|
|
509
|
+
* @param sourceRelativePath - Relative path of source file from source project root
|
|
510
|
+
* @returns Object with 'continuous' (right-to-left consecutive matches) and 'total' (all matches)
|
|
511
|
+
*/
|
|
512
|
+
function calculateMatchingScore(testRelativePath, sourceRelativePath) {
|
|
513
|
+
// Extract directory paths (remove file names)
|
|
514
|
+
const testDir = path.dirname(testRelativePath);
|
|
515
|
+
const sourceDir = path.dirname(sourceRelativePath);
|
|
516
|
+
// If both files are in root directory (no subdirectories), give highest priority
|
|
517
|
+
if ((testDir === '.' || testDir === '') && (sourceDir === '.' || sourceDir === '')) {
|
|
518
|
+
return { continuous: 1000, total: 1000 };
|
|
519
|
+
}
|
|
520
|
+
// Split paths into folder arrays (case-insensitive)
|
|
521
|
+
const testFolders = testDir === '.' || testDir === '' ? [] : testDir.split(path.sep).map(f => f.toLowerCase());
|
|
522
|
+
const sourceFolders = sourceDir === '.' || sourceDir === '' ? [] : sourceDir.split(path.sep).map(f => f.toLowerCase());
|
|
523
|
+
// 1. Calculate continuous matching folders from end (right to left)
|
|
524
|
+
let continuous = 0;
|
|
525
|
+
const minLength = Math.min(testFolders.length, sourceFolders.length);
|
|
526
|
+
for (let i = 1; i <= minLength; i++) {
|
|
527
|
+
if (testFolders[testFolders.length - i] === sourceFolders[sourceFolders.length - i]) {
|
|
528
|
+
continuous++;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
// Stop at first mismatch
|
|
532
|
+
break;
|
|
217
533
|
}
|
|
218
534
|
}
|
|
219
|
-
|
|
535
|
+
// 2. Calculate total matching folders (not required to be continuous)
|
|
536
|
+
const sourceSet = new Set(sourceFolders);
|
|
537
|
+
const total = testFolders.filter(f => sourceSet.has(f)).length;
|
|
538
|
+
return { continuous, total };
|
|
220
539
|
}
|
|
221
|
-
function
|
|
540
|
+
function isTestFileForSourceFile(testFilePath, sourceCodePath) {
|
|
541
|
+
const sourceFileName = path.basename(sourceCodePath, '.cs');
|
|
542
|
+
const testFileName = path.basename(testFilePath, '.cs');
|
|
543
|
+
// First check: test file name should match common test patterns
|
|
544
|
+
// Patterns: SourceFile + (Unit)? + (.)? + (Test|Tests)
|
|
545
|
+
// Examples: SourceFileTests, SourceFileTest, SourceFile.Tests, SourceFile.Test, SourceFileUnitTests, SourceFileUnit.Tests
|
|
546
|
+
const testPattern = getTestFileNamePattern(sourceFileName);
|
|
547
|
+
const matchesPattern = testPattern.test(testFileName);
|
|
548
|
+
if (!matchesPattern) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
// Read both files content
|
|
553
|
+
const testContent = fs.readFileSync(testFilePath, 'utf-8');
|
|
554
|
+
const sourceContent = fs.readFileSync(sourceCodePath, 'utf-8');
|
|
555
|
+
// First check: Extract namespace from source file and verify test file references it
|
|
556
|
+
const sourceNamespaceMatch = sourceContent.match(/namespace\s+([\w.]+)/);
|
|
557
|
+
if (!sourceNamespaceMatch || !sourceNamespaceMatch[1]) {
|
|
558
|
+
// No namespace found in source file, fall back to name-based matching
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
const sourceNamespace = sourceNamespaceMatch[1];
|
|
562
|
+
// Check if test file has using statement for source namespace
|
|
563
|
+
const usingRegex = new RegExp(`using\\s+${sourceNamespace.replace(/\./g, '\\.')};`);
|
|
564
|
+
if (!usingRegex.test(testContent)) {
|
|
565
|
+
// Test file doesn't reference source namespace
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
// Second check: Extract the main type name from source file (public or internal)
|
|
569
|
+
// Supports: class, interface, struct, record (but not nested private/protected types)
|
|
570
|
+
// Priority: public > internal; match first public, fallback to first internal
|
|
571
|
+
// Handles modifiers: sealed, abstract, static, partial
|
|
572
|
+
const publicTypeMatch = sourceContent.match(new RegExp(`\\bpublic\\s+${MODIFIER_PATTERN}(?:class|interface|struct|record)\\s+(\\w+)`));
|
|
573
|
+
const internalTypeMatch = sourceContent.match(new RegExp(`\\binternal\\s+${MODIFIER_PATTERN}(?:class|interface|struct|record)\\s+(\\w+)`));
|
|
574
|
+
const sourceTypeMatch = publicTypeMatch || internalTypeMatch;
|
|
575
|
+
if (!sourceTypeMatch || !sourceTypeMatch[1]) {
|
|
576
|
+
// No public or internal type found - this file might be a utility/helper file
|
|
577
|
+
// without testable classes, so it shouldn't have a test file
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
const sourceTypeName = sourceTypeMatch[1];
|
|
581
|
+
// Check if test file references the source type name or its interface
|
|
582
|
+
// If source is an interface (direct interface definition), only match itself
|
|
583
|
+
// If source is a class/struct/record, also match interface (ITypeName)
|
|
584
|
+
const isSourceInterface = new RegExp(`\\b(?:public|internal)\\s+${MODIFIER_PATTERN}interface\\s+(?:\\w+)`).test(sourceContent);
|
|
585
|
+
let sourceCodeTypeReferenceRegex;
|
|
586
|
+
if (isSourceInterface) {
|
|
587
|
+
// Source is an interface, match only itself
|
|
588
|
+
sourceCodeTypeReferenceRegex = new RegExp(`\\b${sourceTypeName}\\b`, 'g');
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
// Source is a class/struct/record, match both TypeName and ITypeName
|
|
592
|
+
sourceCodeTypeReferenceRegex = new RegExp(`\\b(I)?${sourceTypeName}\\b`, 'g');
|
|
593
|
+
}
|
|
594
|
+
if (sourceCodeTypeReferenceRegex.test(testContent)) {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
// Namespace matches but class name not found in test file
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
console.error(`Failed to read file content for matching: ${err}`);
|
|
602
|
+
// Strict mode: if we can't verify the content, treat as non-matching
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
222
605
|
}
|
|
223
|
-
|
|
224
|
-
|
|
606
|
+
/**
|
|
607
|
+
* Find an existing test file from sibling source files in the same or parent directories.
|
|
608
|
+
* This helps determine the best location for a new test file by looking at where
|
|
609
|
+
* existing test files are located for nearby source files.
|
|
610
|
+
*
|
|
611
|
+
* @param sourceCodePath - Path to the source file
|
|
612
|
+
* @param testCsprojPath - Path to the test project's csproj file
|
|
613
|
+
* @param sourceCsprojPath - Path to the source project's csproj file
|
|
614
|
+
* @returns Object with siblingTestFilePath and sourceRelativeDepth if found, otherwise null
|
|
615
|
+
*/
|
|
616
|
+
function findSiblingTestFile(sourceCodePath, testCsprojPath, sourceCsprojPath) {
|
|
617
|
+
const sourceProjectDir = path.dirname(sourceCsprojPath);
|
|
618
|
+
let currentDir = path.dirname(sourceCodePath);
|
|
619
|
+
let depth = 0;
|
|
620
|
+
// Search from current directory up to source project root
|
|
621
|
+
while (currentDir.toLowerCase().startsWith(sourceProjectDir.toLowerCase()) && currentDir.length >= sourceProjectDir.length) {
|
|
622
|
+
try {
|
|
623
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
624
|
+
const csFiles = entries
|
|
625
|
+
.filter(e => e.isFile() && e.name.endsWith('.cs') && path.join(currentDir, e.name) !== sourceCodePath)
|
|
626
|
+
.map(e => path.join(currentDir, e.name));
|
|
627
|
+
for (const csFile of csFiles) {
|
|
628
|
+
// Try to find test file for this sibling source file
|
|
629
|
+
const result = findMatchingTestFileInProject(testCsprojPath, csFile, sourceCsprojPath);
|
|
630
|
+
if (result.testFilePath) {
|
|
631
|
+
return { siblingTestFilePath: result.testFilePath, sourceRelativeDepth: depth };
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
// Ignore errors for inaccessible directories
|
|
637
|
+
}
|
|
638
|
+
// Move to parent directory
|
|
639
|
+
const parentDir = path.dirname(currentDir);
|
|
640
|
+
if (parentDir === currentDir) {
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
currentDir = parentDir;
|
|
644
|
+
depth++;
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Determine the best test file path for a source file.
|
|
650
|
+
* This function only calculates the path, it does not create any directories.
|
|
651
|
+
*
|
|
652
|
+
* @param sourceCodePath - Path to the source file
|
|
653
|
+
* @param testCsprojPath - Path to the test project's csproj file (optional)
|
|
654
|
+
* @returns Object with testFilePath, testProjectPath, and testFileExist flag
|
|
655
|
+
*/
|
|
656
|
+
function determineTestFilePath(sourceCodePath, testCsprojPath) {
|
|
657
|
+
let testProjectPath = testCsprojPath;
|
|
225
658
|
if (!testProjectPath) {
|
|
226
659
|
console.error(`No test project found for ${sourceCodePath}`);
|
|
227
660
|
return { testFilePath: null, testProjectPath: null };
|
|
@@ -229,38 +662,280 @@ function createTestFile(sourceCodePath) {
|
|
|
229
662
|
const sourceFileName = path.basename(sourceCodePath, '.cs');
|
|
230
663
|
const testFileName = `${sourceFileName}Tests.cs`;
|
|
231
664
|
const sourceFileProjectPath = (0, getCodeStructurePath_1.getSourceFileProjectDir)(sourceCodePath);
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
665
|
+
const sourceCsprojPath = (0, getCodeStructurePath_1.getSourceFileCsprojFilePath)(sourceCodePath);
|
|
666
|
+
// Try to find sibling test file to determine the best location
|
|
667
|
+
const siblingResult = findSiblingTestFile(sourceCodePath, testProjectPath, sourceCsprojPath);
|
|
668
|
+
let testFilePath;
|
|
669
|
+
if (siblingResult) {
|
|
670
|
+
// Found sibling test file, calculate new test file path based on it
|
|
671
|
+
const siblingTestDir = path.dirname(siblingResult.siblingTestFilePath);
|
|
672
|
+
const sourceFileDir = path.dirname(sourceCodePath);
|
|
673
|
+
if (siblingResult.sourceRelativeDepth === 0) {
|
|
674
|
+
// Sibling is in the same directory, put test file in the same test directory
|
|
675
|
+
testFilePath = path.join(siblingTestDir, testFileName);
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
// Sibling is in a parent directory, calculate the relative path from sibling source to current source
|
|
679
|
+
let relativeParts = [];
|
|
680
|
+
let tempDir = sourceFileDir;
|
|
681
|
+
for (let i = 0; i < siblingResult.sourceRelativeDepth; i++) {
|
|
682
|
+
relativeParts.unshift(path.basename(tempDir));
|
|
683
|
+
tempDir = path.dirname(tempDir);
|
|
684
|
+
}
|
|
685
|
+
// The relative path from the found sibling source dir to our source file
|
|
686
|
+
const relativePath = relativeParts.join(path.sep);
|
|
687
|
+
testFilePath = path.join(siblingTestDir, relativePath, testFileName);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
// No sibling test file found, use original logic
|
|
692
|
+
const sourceFileDir = path.dirname(sourceCodePath);
|
|
693
|
+
const testFileRelativePath = sourceFileDir.replace(sourceFileProjectPath, "");
|
|
694
|
+
const testProjecDir = path.dirname(testProjectPath);
|
|
695
|
+
testFilePath = path.join(testProjecDir, testFileRelativePath, testFileName);
|
|
696
|
+
}
|
|
697
|
+
// Check if the test file already exists
|
|
698
|
+
const testFileExist = fs.existsSync(testFilePath);
|
|
699
|
+
if (testFileExist) {
|
|
700
|
+
console.log(`Test file already exists at ${testFilePath}`);
|
|
701
|
+
}
|
|
702
|
+
return { testFilePath, testProjectPath, testFileExist };
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Search for matching test file in a specific test project.
|
|
706
|
+
* Uses directory structure matching and file content analysis to find the best match.
|
|
707
|
+
*
|
|
708
|
+
* @param testCsprojPath - Path to the test project's csproj file
|
|
709
|
+
* @param sourceCodePath - Path to the source file
|
|
710
|
+
* @param sourceCsprojPath - Path to the source project's csproj file
|
|
711
|
+
* @returns Object with testFilePath and testProjectPath if found, otherwise both null
|
|
712
|
+
*/
|
|
713
|
+
function findMatchingTestFileInProject(testCsprojPath, sourceCodePath, sourceCsprojPath) {
|
|
714
|
+
const testProjectDir = path.dirname(testCsprojPath);
|
|
715
|
+
// Get source file's csproj directory
|
|
716
|
+
const sourceCsprojDir = path.dirname(sourceCsprojPath);
|
|
717
|
+
// Calculate relative path of source file from its csproj directory
|
|
718
|
+
const sourceRelativePath = path.relative(sourceCsprojDir, sourceCodePath);
|
|
719
|
+
// Get source file name for filtering
|
|
720
|
+
const sourceFileName = path.basename(sourceCodePath, '.cs');
|
|
721
|
+
// Find .cs files containing source file name (optimization to reduce file count)
|
|
722
|
+
const allTestFiles = findPossibleTestFiles(testProjectDir, sourceFileName);
|
|
723
|
+
// Filter by file name pattern first to reduce unnecessary file reads
|
|
724
|
+
const testPattern = getTestFileNamePattern(sourceFileName);
|
|
725
|
+
// Create list of candidates with their matching scores
|
|
726
|
+
const candidatesWithScores = allTestFiles
|
|
727
|
+
.filter(file => {
|
|
728
|
+
const testFileName = path.basename(file, '.cs');
|
|
729
|
+
return testPattern.test(testFileName);
|
|
730
|
+
})
|
|
731
|
+
.map(file => {
|
|
732
|
+
const testRelativePath = path.relative(testProjectDir, file);
|
|
733
|
+
const score = calculateMatchingScore(testRelativePath, sourceRelativePath);
|
|
734
|
+
return {
|
|
735
|
+
filePath: file,
|
|
736
|
+
relativePath: testRelativePath,
|
|
737
|
+
score: score
|
|
738
|
+
};
|
|
739
|
+
})
|
|
740
|
+
// Sort by continuous match first, then by total match
|
|
741
|
+
.sort((a, b) => {
|
|
742
|
+
if (b.score.continuous !== a.score.continuous) {
|
|
743
|
+
return b.score.continuous - a.score.continuous;
|
|
744
|
+
}
|
|
745
|
+
return b.score.total - a.score.total;
|
|
746
|
+
});
|
|
747
|
+
for (const candidate of candidatesWithScores) {
|
|
748
|
+
if (isTestFileForSourceFile(candidate.filePath, sourceCodePath)) {
|
|
749
|
+
return { testFilePath: candidate.filePath, testProjectPath: testCsprojPath };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return { testFilePath: null, testProjectPath: null };
|
|
753
|
+
}
|
|
754
|
+
function getTestFileFromChangedFiles(sourceCodePath, changedTestFiles) {
|
|
755
|
+
if (!changedTestFiles || changedTestFiles.length === 0) {
|
|
756
|
+
return { testFilePath: null, testProjectPath: null };
|
|
757
|
+
}
|
|
758
|
+
// Get the csproj of the source code
|
|
759
|
+
const sourceCsprojPath = (0, getCodeStructurePath_1.getSourceFileCsprojFilePath)(sourceCodePath);
|
|
760
|
+
if (!sourceCsprojPath) {
|
|
761
|
+
return { testFilePath: null, testProjectPath: null };
|
|
762
|
+
}
|
|
763
|
+
// Cache to store project references from csproj to avoid re-parsing
|
|
764
|
+
const csprojCache = new Map();
|
|
765
|
+
// Store test projects that reference the source project
|
|
766
|
+
const referencingTestProjects = new Set();
|
|
767
|
+
for (const testFilePath of changedTestFiles) {
|
|
768
|
+
if (!fs.existsSync(testFilePath)) {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
// Get the csproj of the test file
|
|
772
|
+
const testCsprojPath = (0, getCodeStructurePath_1.getSourceFileCsprojFilePath)(testFilePath);
|
|
773
|
+
if (!testCsprojPath) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
// Check cache first
|
|
777
|
+
let projectReferences = csprojCache.get(testCsprojPath);
|
|
778
|
+
if (!projectReferences) {
|
|
779
|
+
projectReferences = getProjectReferencesFromCsproj(testCsprojPath);
|
|
780
|
+
csprojCache.set(testCsprojPath, projectReferences);
|
|
781
|
+
}
|
|
782
|
+
// Check if test project references the source project
|
|
783
|
+
const normalizedSourceCsprojPath = path.normalize(sourceCsprojPath);
|
|
784
|
+
if (checkTestProjectReferencesSourceProject(testCsprojPath, normalizedSourceCsprojPath, projectReferences)) {
|
|
785
|
+
referencingTestProjects.add(testCsprojPath);
|
|
786
|
+
// Check if this test file is for the source file
|
|
787
|
+
if (isTestFileForSourceFile(testFilePath, sourceCodePath)) {
|
|
788
|
+
return { testFilePath, testProjectPath: testCsprojPath };
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// Fallback: search all test files in projects that reference the source project
|
|
793
|
+
for (const testCsprojPath of referencingTestProjects) {
|
|
794
|
+
const result = findMatchingTestFileInProject(testCsprojPath, sourceCodePath, sourceCsprojPath);
|
|
795
|
+
if (result.testFilePath) {
|
|
796
|
+
return result;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return { testFilePath: null, testProjectPath: null };
|
|
241
800
|
}
|
|
242
|
-
function getOrCreatTestFile(sourceCodePath, testFilePath,
|
|
801
|
+
function getOrCreatTestFile(sourceCodePath, testFilePath, changedTestFiles) {
|
|
243
802
|
let finalTestFilePath = testFilePath;
|
|
244
803
|
let finalTestProjectPath;
|
|
245
804
|
let testFileExist = !!testFilePath && fs.existsSync(testFilePath);
|
|
805
|
+
// If test file exists, get its project path and return directly
|
|
806
|
+
if (testFileExist) {
|
|
807
|
+
finalTestProjectPath = (0, getCodeStructurePath_1.getSourceFileCsprojFilePath)(testFilePath);
|
|
808
|
+
return { testFilePath: finalTestFilePath, testProjectPath: finalTestProjectPath, testFileExist };
|
|
809
|
+
}
|
|
246
810
|
// if outputPath param is not provided, try to find an existing test file
|
|
247
811
|
// if no existing test file found, create a new one in test project
|
|
248
812
|
if (!testFilePath || testFilePath.trim() === '') {
|
|
249
813
|
console.log(' Output path is missing. outputPath:', testFilePath, ', start looking for existing test project');
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
814
|
+
// Try to find test file from changed files first
|
|
815
|
+
let resultFromChangedFiles = getTestFileFromChangedFiles(sourceCodePath, changedTestFiles);
|
|
816
|
+
if (resultFromChangedFiles.testFilePath) {
|
|
817
|
+
return { testFilePath: resultFromChangedFiles.testFilePath, testProjectPath: resultFromChangedFiles.testProjectPath, testFileExist: true };
|
|
254
818
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
819
|
+
// Fallback: try to find existing test file
|
|
820
|
+
const possibleCSProjPaths = findPossibleTestProjectsFromCsFile(sourceCodePath);
|
|
821
|
+
if (!possibleCSProjPaths || possibleCSProjPaths.length === 0) {
|
|
822
|
+
console.error(` No test project found for source code: ${sourceCodePath}`);
|
|
823
|
+
return { testFilePath: null, testProjectPath: null, testFileExist: false };
|
|
824
|
+
}
|
|
825
|
+
// Get the source csproj path for matching
|
|
826
|
+
const sourceCsprojPath = (0, getCodeStructurePath_1.getSourceFileCsprojFilePath)(sourceCodePath);
|
|
827
|
+
if (!sourceCsprojPath) {
|
|
828
|
+
console.error(` Cannot find source csproj for: ${sourceCodePath}`);
|
|
829
|
+
return { testFilePath: null, testProjectPath: null, testFileExist: false };
|
|
830
|
+
}
|
|
831
|
+
// Try to find existing test file in each possible test project
|
|
832
|
+
for (const testCsprojPath of possibleCSProjPaths) {
|
|
833
|
+
const result = findMatchingTestFileInProject(testCsprojPath, sourceCodePath, sourceCsprojPath);
|
|
834
|
+
if (result.testFilePath) {
|
|
835
|
+
testFileExist = true;
|
|
836
|
+
finalTestFilePath = result.testFilePath;
|
|
837
|
+
finalTestProjectPath = result.testProjectPath;
|
|
838
|
+
return { testFilePath: finalTestFilePath, testProjectPath: finalTestProjectPath, testFileExist };
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// No existing test file found, determine test file path (may find existing)
|
|
842
|
+
const result = determineTestFilePath(sourceCodePath, possibleCSProjPaths[0]);
|
|
843
|
+
if (!result.testFilePath || !result.testProjectPath) {
|
|
844
|
+
console.error(`Failed to determine test file path for ${sourceCodePath}`);
|
|
845
|
+
return { testFilePath: null, testProjectPath: null, testFileExist: false };
|
|
258
846
|
}
|
|
259
847
|
finalTestFilePath = result.testFilePath;
|
|
260
848
|
finalTestProjectPath = result.testProjectPath;
|
|
849
|
+
testFileExist = result.testFileExist ?? false;
|
|
850
|
+
// Create directory if test file doesn't exist
|
|
851
|
+
if (!testFileExist) {
|
|
852
|
+
(0, getOrCreateDir_1.getOrCreateDir)(path.dirname(finalTestFilePath));
|
|
853
|
+
}
|
|
261
854
|
}
|
|
262
855
|
return { testFilePath: finalTestFilePath, testProjectPath: finalTestProjectPath, testFileExist };
|
|
263
856
|
}
|
|
857
|
+
// // Verify test file paths for all projects
|
|
858
|
+
// function verifyTestFilePaths(): void {
|
|
859
|
+
// const baseFolder = 'Q:\\src\\Microsoft.News.OneService';
|
|
860
|
+
// const outputFile = 'q:\\verifyTestFilePaths_results.txt';
|
|
861
|
+
// const results: string[] = [];
|
|
862
|
+
// try {
|
|
863
|
+
// // Recursively find all non test csproj files
|
|
864
|
+
// const getAllCsprojFiles = (dir: string): string[] => {
|
|
865
|
+
// const csprojFiles: string[] = [];
|
|
866
|
+
// try {
|
|
867
|
+
// const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
868
|
+
// for (const entry of entries) {
|
|
869
|
+
// const fullPath = path.join(dir, entry.name);
|
|
870
|
+
// if (entry.isDirectory() && entry.name !== 'bin' && entry.name !== 'obj' && entry.name !== '.vs') {
|
|
871
|
+
// csprojFiles.push(...getAllCsprojFiles(fullPath));
|
|
872
|
+
// } else if (entry.isFile() && entry.name.endsWith('.csproj')) {
|
|
873
|
+
// // Filter by test project naming patterns
|
|
874
|
+
// const rawName = path.basename(fullPath, '.csproj');
|
|
875
|
+
// const words = rawName
|
|
876
|
+
// .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
877
|
+
// .split(/[^a-zA-Z0-9]+/)
|
|
878
|
+
// .filter(Boolean)
|
|
879
|
+
// .map(w => w.toLowerCase());
|
|
880
|
+
// if (words.every(w => !w.endsWith('test') && !w.endsWith('tests'))) {
|
|
881
|
+
// csprojFiles.push(fullPath);
|
|
882
|
+
// }
|
|
883
|
+
// }
|
|
884
|
+
// }
|
|
885
|
+
// } catch (err) {
|
|
886
|
+
// // Skip inaccessible directories
|
|
887
|
+
// }
|
|
888
|
+
// return csprojFiles;
|
|
889
|
+
// };
|
|
890
|
+
// // Get first .cs file in a directory (recursive search)
|
|
891
|
+
// const getFirstCsFile = (dir: string): string | null => {
|
|
892
|
+
// try {
|
|
893
|
+
// const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
894
|
+
// // First pass: look for .cs files in current directory
|
|
895
|
+
// for (const entry of entries) {
|
|
896
|
+
// if (entry.isFile() && entry.name.endsWith('.cs')) {
|
|
897
|
+
// return path.join(dir, entry.name);
|
|
898
|
+
// }
|
|
899
|
+
// }
|
|
900
|
+
// // Second pass: recurse into subdirectories
|
|
901
|
+
// for (const entry of entries) {
|
|
902
|
+
// if (entry.isDirectory() && entry.name !== 'bin' && entry.name !== 'obj' && entry.name !== '.vs') {
|
|
903
|
+
// const fullPath = path.join(dir, entry.name);
|
|
904
|
+
// const result = getFirstCsFile(fullPath);
|
|
905
|
+
// if (result) {
|
|
906
|
+
// return result;
|
|
907
|
+
// }
|
|
908
|
+
// }
|
|
909
|
+
// }
|
|
910
|
+
// } catch (err) {
|
|
911
|
+
// // Skip if directory can't be read
|
|
912
|
+
// }
|
|
913
|
+
// return null;
|
|
914
|
+
// };
|
|
915
|
+
// // Find all csproj files
|
|
916
|
+
// const csprojFiles = getAllCsprojFiles(baseFolder);
|
|
917
|
+
// // Process each csproj
|
|
918
|
+
// for (const csprojPath of csprojFiles) {
|
|
919
|
+
// const csprojDir = path.dirname(csprojPath);
|
|
920
|
+
// const csFile = getFirstCsFile(csprojDir);
|
|
921
|
+
// if (csFile) {
|
|
922
|
+
// const result = getOrCreatTestFile(csFile, undefined);
|
|
923
|
+
// const testFilePath = result.testFilePath || 'NOT_FOUND';
|
|
924
|
+
// const testProjectPath = result.testProjectPath || 'NOT_FOUND';
|
|
925
|
+
// results.push(`${csprojPath}\t${csFile}\t${testFilePath}\t${testProjectPath}\t${result.testFileExist}`);
|
|
926
|
+
// console.log(`Processed: ${csprojPath}`);
|
|
927
|
+
// }
|
|
928
|
+
// }
|
|
929
|
+
// // Write results to file
|
|
930
|
+
// fs.writeFileSync(outputFile, results.join('\n'), 'utf-8');
|
|
931
|
+
// console.log(`Results written to ${outputFile}`);
|
|
932
|
+
// } catch (err) {
|
|
933
|
+
// console.error(`Error during verification: ${err}`);
|
|
934
|
+
// }
|
|
935
|
+
// }
|
|
936
|
+
// // Uncomment to run verification
|
|
937
|
+
// verifyTestFilePaths();
|
|
938
|
+
// getOrCreatTestFile("Q:\\src\\Microsoft.News.OneService\\Libraries\\Tests\\MockStrategies\\MockDataException.cs", undefined);
|
|
264
939
|
// Example usage
|
|
265
940
|
// const csFilePath = "D:\\code\\CS.Service.Fundamental\\SharedSegments\\SharedSegments\\SharedSegments.Plugins\\Workflows\\LocationDetection\\AudienceResponseParser.cs";
|
|
266
941
|
// const csFilePath = "D:\\code\\msnews-experiences\\experiences\\weather-right-rail\\src\\WeatherRightRail.connector.ts";
|