Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/node/testFiles.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type { CancellationToken } from 'vscode';
7
import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';
8
import { ISearchService } from '../../../platform/search/common/searchService';
9
import { ITabsAndEditorsService } from '../../../platform/tabs/common/tabsAndEditorsService';
10
import { isMatch } from '../../../util/common/glob';
11
import { Schemas } from '../../../util/vs/base/common/network';
12
import * as resources from '../../../util/vs/base/common/resources';
13
import { URI } from '../../../util/vs/base/common/uri';
14
15
type TestHint = {
16
prefix?: string;
17
suffixes?: string[];
18
location: 'sameFolder' | 'testFolder';
19
};
20
21
const nullTestHint: Required<TestHint> = {
22
location: 'sameFolder',
23
prefix: 'test_',
24
suffixes: ['.test', '.spec', '_test', 'Test', '_spec', '_test', 'Tests', '.Tests', 'Spec'],
25
};
26
27
const testHintsByLanguage: Record<string, TestHint> = {
28
csharp: { suffixes: ['Test'], location: 'testFolder' },
29
dart: { suffixes: ['_test'], location: 'testFolder' },
30
go: { suffixes: ['_test'], location: 'sameFolder' },
31
java: { suffixes: ['Test'], location: 'testFolder' },
32
javascript: { suffixes: ['.test', '.spec'], location: 'sameFolder' },
33
javascriptreact: { suffixes: ['.test', '.spec'], location: 'sameFolder' },
34
kotlin: { suffixes: ['Test'], location: 'testFolder' },
35
php: { suffixes: ['Test'], location: 'testFolder' },
36
powershell: { suffixes: ['.Tests'], location: 'testFolder' },
37
python: { prefix: 'test_', suffixes: ['_test'], location: 'testFolder' },
38
ruby: { suffixes: ['_test', '_spec'], location: 'testFolder' },
39
rust: { suffixes: [''], location: 'testFolder' }, // same file`
40
swift: { suffixes: ['Tests'], location: 'testFolder' },
41
typescript: { suffixes: ['.test', '.spec'], location: 'sameFolder' },
42
typescriptreact: { suffixes: ['.test', '.spec'], location: 'sameFolder' },
43
};
44
45
export const suffix2Language: Record<string, keyof typeof testHintsByLanguage> = {
46
cs: 'csharp',
47
dart: 'dart',
48
go: 'go',
49
java: 'java',
50
js: 'javascriptreact',
51
kt: 'kotlin',
52
php: 'php',
53
ps1: 'powershell',
54
py: 'python',
55
rb: 'ruby',
56
rs: 'rust',
57
swift: 'swift',
58
ts: 'typescript',
59
tsx: 'typescriptreact',
60
};
61
62
const testHintsBySuffix: { [key: string]: TestHint } = (function () {
63
const result: { [key: string]: TestHint } = {};
64
for (const [suffix, langId] of Object.entries(suffix2Language)) {
65
result[suffix] = <TestHint>testHintsByLanguage[langId];
66
}
67
return result;
68
})();
69
70
/**
71
* @remark does NOT respect copilot-ignore
72
*/
73
export class TestFileFinder {
74
75
constructor(
76
@ISearchService private readonly _search: ISearchService,
77
@ITabsAndEditorsService private readonly _tabs: ITabsAndEditorsService
78
) {
79
}
80
81
private _findTabMatchingPattern(pattern: string): URI | undefined {
82
83
const tab = this._tabs.tabs.find(info => {
84
// return a tab which uri matches the pattern
85
return info.uri && info.uri.scheme !== Schemas.untitled && isMatch(info.uri, pattern);
86
});
87
88
return tab?.uri;
89
}
90
91
/**
92
* Given a source file, find the corresponding test file.
93
*/
94
async findTestFileForSourceFile(document: TextDocumentSnapshot, token: CancellationToken): Promise<URI | undefined> {
95
96
if (document.isUntitled) {
97
return undefined;
98
}
99
100
const basename = resources.basename(document.uri);
101
const ext = resources.extname(document.uri);
102
103
const testHint = testHintsByLanguage[document.languageId] ?? nullTestHint;
104
105
const testNameCandidates: string[] = [];
106
if (testHint.prefix) {
107
testNameCandidates.push(testHint.prefix + basename);
108
}
109
if (testHint.suffixes) {
110
for (const suffix of testHint.suffixes ?? []) {
111
const testName = basename.replace(`${ext}`, `${suffix}${ext}`);
112
testNameCandidates.push(testName);
113
}
114
}
115
116
const pattern =
117
testNameCandidates.length === 1
118
? `**/${testNameCandidates[0]}` // @ulugbekna: there must be at least two sub-patterns within braces for the glob to work
119
: `**/{${testNameCandidates.join(',')}}`;
120
121
// try open editors/tabs first
122
// use search service as fallback
123
124
let result = this._findTabMatchingPattern(pattern);
125
126
if (!result) {
127
if (document.languageId === 'python') {
128
result = await this._search.findFilesWithExcludes(pattern, '**/*.pyc', 1, token);
129
} else {
130
result = await this._search.findFilesWithDefaultExcludes(pattern, 1, token);
131
}
132
}
133
134
return result;
135
}
136
137
/**
138
* Given a source file, find any test file (for the same language)
139
*/
140
async findAnyTestFileForSourceFile(document: TextDocumentSnapshot, token: CancellationToken): Promise<URI | undefined> {
141
142
const testHint = testHintsByLanguage[document.languageId] ?? nullTestHint;
143
144
const patterns: string[] = [];
145
if (testHint.prefix) {
146
patterns.push(`${testHint.prefix}*`);
147
}
148
if (testHint.suffixes) {
149
const ext = resources.extname(document.uri);
150
for (const suffix of testHint.suffixes ?? []) {
151
patterns.push(`*${suffix}${ext}`);
152
}
153
}
154
155
const pattern =
156
patterns.length === 1
157
? `**/${patterns[0]}` // @ulugbekna: there must be at least two sub-patterns within braces for the glob to work
158
: `**/{${patterns.join(',')}}`;
159
160
// try open editors/tabs first
161
// use search service as fallback
162
let result = this._findTabMatchingPattern(pattern);
163
if (!result) {
164
if (document.languageId === 'python') {
165
result = await this._search.findFilesWithExcludes(pattern, '**/*.pyc', 1, token);
166
} else {
167
result = await this._search.findFilesWithDefaultExcludes(pattern, 1, token);
168
}
169
170
}
171
return result;
172
}
173
174
/**
175
* Given a test file, find the corresponding source file.
176
*/
177
async findFileForTestFile(document: TextDocumentSnapshot, token: CancellationToken): Promise<URI | undefined> {
178
179
const testHint = testHintsByLanguage[document.languageId] ?? nullTestHint;
180
181
const basename = resources.basename(document.uri);
182
const parts: string[] = [];
183
184
// collect potential suffixes and prefixes
185
if (testHint.suffixes) {
186
parts.splice(0, 0, ...testHint.suffixes);
187
}
188
if (testHint.prefix) {
189
parts.splice(0, 0, testHint.prefix);
190
}
191
192
for (const part of parts) {
193
const candidate = basename.replace(part, '');
194
if (candidate !== basename) {
195
const pattern = `**/${candidate}`;
196
197
let result = this._findTabMatchingPattern(pattern);
198
if (!result) {
199
result = await this._search.findFilesWithDefaultExcludes(pattern, 1, token);
200
}
201
if (result) {
202
return result;
203
}
204
}
205
}
206
207
return undefined;
208
}
209
}
210
211
export function isTestFile(candidate: URI | TextDocumentSnapshot): boolean {
212
213
let testHint: TestHint | undefined;
214
if (candidate instanceof TextDocumentSnapshot) {
215
testHint = testHintsByLanguage[candidate.languageId];
216
candidate = candidate.uri;
217
}
218
219
const sourceFileName = resources.basename(candidate);
220
const sourceFileExtension = resources.extname(candidate);
221
testHint ??= testHintsBySuffix[sourceFileExtension.replace('.', '')];
222
223
if (testHint) {
224
225
if (testHint.suffixes) {
226
const foundSuffixMatch = testHint.suffixes.some(suffix =>
227
sourceFileName.endsWith(suffix + sourceFileExtension)
228
);
229
if (foundSuffixMatch) {
230
return true;
231
}
232
}
233
if (testHint.prefix && sourceFileName.startsWith(testHint.prefix)) {
234
return true;
235
}
236
237
} else {
238
const foundSuffixMatch = nullTestHint.suffixes.some(suffix => sourceFileName.endsWith(suffix + sourceFileExtension));
239
if (foundSuffixMatch) {
240
return true;
241
}
242
if (sourceFileName.startsWith(nullTestHint.prefix)) {
243
return true;
244
}
245
}
246
return false;
247
}
248
249
export function suggestTestFileBasename(document: TextDocumentSnapshot): string {
250
const testHint = testHintsByLanguage[document.languageId] ?? nullTestHint;
251
const basename = resources.basename(document.uri);
252
253
if (testHint.prefix) {
254
return testHint.prefix + basename;
255
}
256
257
const ext = resources.extname(document.uri);
258
const suffix = testHint.suffixes && testHint.suffixes.length > 0
259
? testHint.suffixes[0]
260
: '.test';
261
262
return basename.replace(`${ext}`, `${suffix}${ext}`);
263
}
264
265
266
export function suggestTestFileDir(document: TextDocumentSnapshot): URI {
267
const srcFileLocation = resources.joinPath(document.uri, '..'); // same folder
268
if (document.languageId === 'java') { // Java
269
/*
270
* According to the standard project structure of Maven, the corresponding test file for
271
* `$module/src/main/java/...$packages/$Class.java` is usually `$module/src/test/java/...$packages/${Class}Test.java`.
272
* Yet, it's worth noting that this structure might be altered by the user (though it's rare). In such cases, we can
273
* only obtain the accurate path from a language extension installed by the user, like `redhat.java`, for instance. But
274
* for simplicity's sake, we always assume the user is sticking to the standard project structure mentioned above at
275
* this stage.
276
*/
277
const srcFilePath = srcFileLocation.path;
278
if (srcFilePath.includes('/src/main/')) {
279
const testFilePath = srcFilePath.replace('/src/main/', '/src/test/');
280
return srcFileLocation.with({ path: testFilePath });
281
}
282
}
283
return srcFileLocation; // same folder
284
}
285
286
export function suggestUntitledTestFileLocation(document: TextDocumentSnapshot): URI {
287
const newBasename = suggestTestFileBasename(document);
288
const newLocation = suggestTestFileDir(document);
289
const testFileUri = URI.joinPath(newLocation, newBasename).with({ scheme: Schemas.untitled });
290
return testFileUri;
291
}
292
293