Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/stestUtil.ts
13389 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
import assert from 'assert';
6
import * as fs from 'fs';
7
import * as path from 'path';
8
import { IFile, IQualifiedFile, IRelativeFile } from '../../src/platform/test/node/simulationWorkspace';
9
import { timeout } from '../../src/util/vs/base/common/async';
10
import { URI } from '../../src/util/vs/base/common/uri';
11
import { generateUuid } from '../../src/util/vs/base/common/uuid';
12
import { SIMULATION_FOLDER_NAME } from './shared/sharedTypes';
13
import { IConversationalOutcome, IEmptyOutcome, IInlineEditOutcome, IOutcome, IWorkspaceEditOutcome } from './types';
14
15
export function forEachModel(models: readonly string[], func: (model: string) => void) {
16
return () => models.forEach(func);
17
}
18
19
interface FixtureFileInfo {
20
readonly kind: 'relativeFile';
21
readonly fileName: string;
22
readonly fileContents: string;
23
}
24
25
/** See https://github.com/microsoft/vscode-ts-file-path-support */
26
type RelativeFilePath<T extends string> = string & { baseDir?: T };
27
28
/** This function allows [tools](https://github.com/microsoft/vscode-ts-file-path-support/tree/main) to inline/extract the file content. */
29
export function toFile(data: { filePath: string | FixtureFileInfo } | { fileName: string; fileContents: string } | { uri: URI; fileContents: string }): IFile {
30
if ('filePath' in data) {
31
if (typeof data.filePath === 'string') {
32
return fromFixture(data.filePath);
33
} else {
34
return data.filePath;
35
}
36
} else if ('fileName' in data) {
37
return {
38
kind: 'relativeFile',
39
fileName: data.fileName,
40
fileContents: data.fileContents,
41
} satisfies IRelativeFile;
42
} else {
43
return {
44
kind: 'qualifiedFile',
45
uri: data.uri,
46
fileContents: data.fileContents,
47
} satisfies IQualifiedFile;
48
}
49
}
50
51
let _fixturesDir: string | undefined;
52
53
export function getFixturesDir() {
54
if (!_fixturesDir) {
55
_fixturesDir = [
56
path.join(__dirname, '../test/simulation/fixtures'), // after bundling with esbuild
57
path.join(__dirname, './fixtures'), // when running from sources
58
].filter(p => fs.existsSync(p))[0];
59
if (!_fixturesDir) {
60
throw new Error('Could not find fixtures directory');
61
}
62
}
63
return _fixturesDir;
64
}
65
66
export function fromFixture(pathWithinFixturesDir: RelativeFilePath<'$dir/fixtures'>): FixtureFileInfo;
67
export function fromFixture(dirnameWithinFixturesDir: string, relativePathWithinBaseDir: string): FixtureFileInfo;
68
export function fromFixture(pathOrDirnameWithinFixturesDir: string, relativePathWithinBaseDir?: string): FixtureFileInfo {
69
70
let filePath: string;
71
let baseDirname: string;
72
if (relativePathWithinBaseDir === undefined) {
73
filePath = path.join(getFixturesDir(), pathOrDirnameWithinFixturesDir);
74
baseDirname = path.dirname(filePath);
75
} else {
76
baseDirname = path.join(getFixturesDir(), pathOrDirnameWithinFixturesDir);
77
filePath = path.join(baseDirname, relativePathWithinBaseDir);
78
}
79
80
const fileName = path.relative(baseDirname, filePath);
81
const fileContents = fs.readFileSync(filePath).toString();
82
return { kind: 'relativeFile' as const, fileName, fileContents };
83
}
84
85
export function fromFixtureDir(dirnameWithinFixturesDir: string, dirnameWithinDir?: string): FixtureFileInfo[] {
86
const files = fs.readdirSync(
87
path.join(getFixturesDir(),
88
dirnameWithinFixturesDir,
89
dirnameWithinDir ?? '',
90
),
91
{ withFileTypes: true },
92
);
93
94
const out: FixtureFileInfo[] = [];
95
for (const file of files) {
96
const nested = path.join(dirnameWithinDir ?? '', file.name);
97
if (file.isFile()) {
98
out.push(fromFixture(dirnameWithinFixturesDir, nested));
99
} else if (file.isDirectory()) {
100
out.push(...fromFixtureDir(dirnameWithinFixturesDir, nested));
101
}
102
}
103
104
return out;
105
}
106
107
/**
108
* Asserts that one of the given assertions passes.
109
*
110
* @template T - The type of the value returned by the assertions.
111
* @param assertions - An array of functions that represent the assertions to be checked.
112
* @returns - The value returned by the assertion that passes.
113
* @throws {assert.AssertionError} - If none of the assertions pass.
114
*/
115
export function assertOneOf<T>(assertions: (() => T)[]): T {
116
for (const assertion of assertions) {
117
try {
118
return assertion();
119
} catch (e) {
120
if (!(e instanceof assert.AssertionError)) {
121
throw e; // surface unexpected errors
122
}
123
}
124
}
125
throw new assert.AssertionError({ message: 'none of the assertions passed' });
126
}
127
128
export interface IInlineReplaceEdit {
129
kind: 'replaceEdit';
130
originalStartLine: number;
131
originalEndLine: number;
132
modifiedStartLine: number;
133
modifiedEndLine: number;
134
changedOriginalLines: string[];
135
changedModifiedLines: string[];
136
allOriginalLines: string[];
137
allModifiedLines: string[];
138
}
139
140
export function assertInlineEdit(outcome: IOutcome): asserts outcome is IInlineEditOutcome {
141
assert.strictEqual(outcome.type, 'inlineEdit', `'${outcome.type}' === 'inlineEdit'`);
142
}
143
144
export function assertNoErrorOutcome(outcome: IOutcome): asserts outcome is IInlineEditOutcome | IWorkspaceEditOutcome | IConversationalOutcome | IEmptyOutcome {
145
assert.notEqual(outcome.type, 'error', `no error outcome expected`);
146
}
147
148
export function assertConversationalOutcome(outcome: IOutcome): asserts outcome is IConversationalOutcome {
149
assert.strictEqual(outcome.type, 'conversational', `'${outcome.type}' === 'conversational'`);
150
}
151
152
export function assertWorkspaceEdit(outcome: IOutcome): asserts outcome is IWorkspaceEditOutcome {
153
assert.strictEqual(outcome.type, 'workspaceEdit', `'${outcome.type}' === 'workspaceEdit'`);
154
}
155
156
/**
157
* returns null if the files are identical
158
*/
159
export function extractInlineReplaceEdits(outcome: IInlineEditOutcome): IInlineReplaceEdit | null {
160
const originalLines = outcome.originalFileContents.split(/\r\n|\r|\n/g);
161
const modifiedLines = outcome.fileContents.split(/\r\n|\r|\n/g);
162
163
let ostart = 0;
164
let mstart = 0;
165
while (ostart < originalLines.length && mstart < modifiedLines.length && originalLines[ostart] === modifiedLines[mstart]) {
166
ostart++;
167
mstart++;
168
}
169
170
if (ostart === originalLines.length && mstart === modifiedLines.length) {
171
// identical files
172
return null;
173
}
174
175
let ostop = originalLines.length - 1;
176
let mstop = modifiedLines.length - 1;
177
while (ostop >= ostart && mstop >= mstart && originalLines[ostop] === modifiedLines[mstop]) {
178
ostop--;
179
mstop--;
180
}
181
182
const changedOriginalLines = originalLines.slice(ostart, ostop + 1);
183
const changedModifiedLines = modifiedLines.slice(mstart, mstop + 1);
184
185
return {
186
kind: 'replaceEdit',
187
originalStartLine: ostart,
188
originalEndLine: ostop,
189
modifiedStartLine: mstart,
190
modifiedEndLine: mstop,
191
changedOriginalLines,
192
changedModifiedLines,
193
allOriginalLines: originalLines,
194
allModifiedLines: modifiedLines,
195
};
196
}
197
198
export interface IInlineEditShape {
199
line: number;
200
originalLength: number;
201
modifiedLength: number | undefined;
202
}
203
204
export function assertInlineEditShape(outcome: IOutcome, _expected: IInlineEditShape | IInlineEditShape[]): IInlineReplaceEdit {
205
assertInlineEdit(outcome);
206
const actual = extractInlineReplaceEdits(outcome);
207
assert.ok(actual, 'unexpected identical files');
208
const actualLines = {
209
line: actual.originalStartLine,
210
originalLength: actual.originalEndLine - actual.originalStartLine + 1,
211
modifiedLength: actual.modifiedEndLine - actual.modifiedStartLine + 1,
212
};
213
const originalLineCount = outcome.originalFileContents.split(/\r\n|\r|\n/g).length;
214
const _expectedArr = Array.isArray(_expected) ? _expected : [_expected];
215
const expectedArr = _expectedArr.map((expected) => {
216
const line = (
217
expected.line < 0 ? actual.allOriginalLines.length - ~expected.line : expected.line
218
);
219
const originalLength = expected.originalLength;
220
const modifiedLength = (
221
typeof expected.modifiedLength === 'undefined'
222
? (actual.allModifiedLines.length + originalLength - originalLineCount)
223
: expected.modifiedLength
224
);
225
return { line, originalLength, modifiedLength };
226
});
227
let err: Error | undefined;
228
for (const expected of expectedArr) {
229
try {
230
assert.deepStrictEqual(actualLines, expected);
231
return actual;
232
} catch (e) {
233
// Let's try the next one
234
err = e;
235
}
236
}
237
// No options matched
238
// console.log(`\n`, JSON.stringify(actualLines), '\n', JSON.stringify(expectedArr));
239
throw err;
240
}
241
242
export function assertQualifiedFile(file: IFile | { srcUri: string; post: string }): asserts file is IQualifiedFile {
243
if ('srcUri' in file && 'post' in file) {
244
// New format - nothing to assert, it's already a qualified file equivalent
245
return;
246
}
247
// Old format - check the kind
248
assert.strictEqual(file.kind, 'qualifiedFile', `'${file.kind}' === 'qualifiedFile'`);
249
}
250
251
252
/**
253
* Asserts that at least `n` out of `expected.length` strings are present in `actual` string.
254
*
255
* If `n` is not given, `n = Math.floor(1, expected.length / 2)` is used.
256
*/
257
export function assertSomeStrings(actual: string, expected: string[], n?: number) {
258
assert.ok(expected.length > 0, 'Need to expect at least one string');
259
260
if (n === undefined) {
261
n = Math.max(1, Math.floor(expected.length / 2));
262
}
263
264
let seen = 0;
265
for (const item of expected) {
266
if (actual.includes(item)) {
267
seen++;
268
}
269
}
270
271
assert.ok(seen >= n, `Expected to see at least ${n} of ${expected.join(',')}, but only saw ${seen} in ${actual}`);
272
}
273
274
export function assertNoStrings(actual: string, expected: string[],) {
275
assertSomeStrings(actual, expected, 0);
276
}
277
278
export function assertOccursOnce(hay: string, needle: string) {
279
const firstOccurrence = hay.indexOf(needle);
280
assert(firstOccurrence > -1, `assertOccursOnce: no occurrence\n${JSON.stringify({ hay, needle }, null, '\t')}`);
281
assert(hay.indexOf(needle, firstOccurrence + needle.length) === -1, `assertOccursOnce: more than 1 occurrence\n${JSON.stringify({ hay, needle }, null, '\t')}`);
282
}
283
284
export function assertNoOccurrence(hay: string, needles: string | string[]): void {
285
needles = Array.isArray(needles) ? needles : [needles];
286
for (const needle of needles) {
287
assert(hay.indexOf(needle) === -1, `assertDoesNotOccur: occurrence\n${JSON.stringify({ hay, needle }, null, '\t')}`);
288
}
289
}
290
291
function generateTempDirPath(): string {
292
return path.join(__dirname, `../${SIMULATION_FOLDER_NAME}/tmp-${generateUuid()}`);
293
}
294
295
export async function createTempDir(): Promise<string> {
296
const folderPath = generateTempDirPath();
297
await fs.promises.mkdir(folderPath, { recursive: true });
298
return folderPath;
299
}
300
301
export async function cleanTempDir(folderPath: string): Promise<void> {
302
await fs.promises.rm(folderPath, { recursive: true, force: true });
303
}
304
305
export async function cleanTempDirWithRetry(path: string, retry = 3): Promise<void> {
306
// On windows, sometimes the tsc process holds locks on the directory even after it exits.
307
// This tries to delete the folder a few times with a delay in between.
308
let err = null;
309
for (let i = 0; i < retry; i++) {
310
try {
311
await cleanTempDir(path);
312
return;
313
} catch (e) {
314
err = e;
315
await timeout(1000);
316
// Ignore error
317
}
318
}
319
320
console.error(`Failed to delete ${path} after ${retry} attempts.`, err);
321
}
322
323