Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/codeMapper/codeMapper.stest.ts
13388 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 type { MappedEditsResponseStream } from 'vscode';
7
import { Intent } from '../../src/extension/common/constants';
8
import { IDocumentContext } from '../../src/extension/prompt/node/documentContext';
9
import { CodeMapper, ICodeMapperExistingDocument } from '../../src/extension/prompts/node/codeMapper/codeMapper';
10
import { WorkingCopyOriginalDocument } from '../../src/extension/prompts/node/inline/workingCopies';
11
import { ITabsAndEditorsService } from '../../src/platform/tabs/common/tabsAndEditorsService';
12
import { ITestingServicesAccessor, TestingServiceCollection } from '../../src/platform/test/node/services';
13
import { IFile, SimulationWorkspace } from '../../src/platform/test/node/simulationWorkspace';
14
import { isEqual } from '../../src/util/vs/base/common/resources';
15
import { IInstantiationService } from '../../src/util/vs/platform/instantiation/common/instantiation';
16
import { CancellationTokenSource, Selection, TextEdit } from '../../src/vscodeTypes';
17
import { Configuration, ISimulationTestRuntime, ssuite, stest } from '../base/stest';
18
import { KnownDiagnosticProviders } from '../simulation/diagnosticProviders';
19
import { setupSimulationWorkspace, teardownSimulationWorkspace, toIRange } from '../simulation/inlineChatSimulator';
20
import { assertJSON, assertNoElidedCodeComments, getWorkspaceDiagnostics, validateConsistentIndentation } from '../simulation/outcomeValidators';
21
import { INLINE_CHANGED_DOC_TAG, INLINE_INITIAL_DOC_TAG, INLINE_STATE_TAG, IWorkspaceState } from '../simulation/shared/sharedTypes';
22
import { fromFixture } from '../simulation/stestUtil';
23
import { OutcomeAnnotation } from '../simulation/types';
24
25
26
function forEditsAndAgent(callback: (variant: string | undefined, model: string | undefined, configurations: Configuration<any>[] | undefined) => void): void {
27
callback('', undefined, undefined);
28
}
29
30
forEditsAndAgent((variant, model, configurations) => {
31
32
ssuite({ title: 'codeMapper' + variant, location: 'context', configurations }, () => {
33
stest({ description: 'add new function at location 1', language: 'typescript' }, async (testingServiceCollection) => {
34
return simulateCodeMapper(testingServiceCollection, {
35
files: [fromFixture('codeMapper/fibonacci_recursive.ts')],
36
activeDocument: 'fibonacci_recursive.ts',
37
selection: [5, 0, 5, 0],
38
focusLocations: [],
39
codeBlock: [
40
'function fibonacci_iterative(n: number): number {',
41
' if (n === 1) return 0;',
42
' if (n === 2) return 1;',
43
' let a = 0, b = 1, sum = 0;',
44
' for (let i = 2; i < n; i++) {',
45
' sum = a + b;',
46
' a = b;',
47
' b = sum;',
48
' }',
49
' return sum;',
50
'}'
51
],
52
validate: async (outcome, workspace, accessor) => {
53
assert.ok(outcome.appliedEdits.length, 'has edits');
54
assert.ok(outcome.editedFile.includes('function fibonacci_iterative(n: number): number {'), 'has fibonacci_iterative');
55
assertNoElidedCodeComments(outcome.editedFile);
56
validateConsistentIndentation(outcome.editedFile, false, outcome.annotations);
57
}
58
});
59
});
60
stest({ description: 'modify function', language: 'typescript' }, async (testingServiceCollection) => {
61
return simulateCodeMapper(testingServiceCollection, {
62
files: [fromFixture('codeMapper/fibonacci_recursive.ts')],
63
activeDocument: 'fibonacci_recursive.ts',
64
selection: [0, 0, 4, 1],
65
focusLocations: [],
66
codeBlock: [
67
'function fibonacci_recursive(n: number): number {',
68
' // Base case: if n is 1, the first number in the Fibonacci sequence is returned (0).',
69
' if (n === 1) return 0;',
70
' // Base case: if n is 2, the second number in the Fibonacci sequence is returned (1).',
71
' if (n === 2) return 1;',
72
' // Recursive case: return the sum of the two preceding numbers in the sequence.',
73
' // This is done by calling fibonacci_recursive for (n - 1) and (n - 2) and adding their results.',
74
' return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2);',
75
'}'
76
],
77
validate: async (outcome, workspace, accessor) => {
78
assert.equal(numOccurrences(outcome.editedFile, 'function fibonacci_recursive'), 1, 'only once occurrence of fibonacci_recursive');
79
assert.ok(outcome.appliedEdits.length, 'has edits');
80
assert.ok(outcome.editedFile.includes('// Base case: if n is 1'), 'has comments added');
81
assertNoElidedCodeComments(outcome.editedFile);
82
validateConsistentIndentation(outcome.editedFile, false, outcome.annotations);
83
}
84
});
85
});
86
stest({ description: 'replace statement with ident change', language: 'typescript' }, async (testingServiceCollection) => {
87
return simulateCodeMapper(testingServiceCollection, {
88
files: [fromFixture('codeMapper/fibonacci_recursive.ts')],
89
activeDocument: 'fibonacci_recursive.ts',
90
selection: [7, 0, 7, 93],
91
focusLocations: [],
92
codeBlock: [
93
'// More readable version of generating a Fibonacci series',
94
'const fibonacci_series = Array.from({ length: n }, (value, index) => {',
95
' return fibonacci_recursive(index + 1);',
96
'});'
97
],
98
validate: async (outcome, workspace, accessor) => {
99
assert.ok(outcome.appliedEdits.length, 'has edits');
100
assert.ok(outcome.editedFile.includes('return fibonacci_recursive(index + 1);'), 'has new statement');
101
assertNoElidedCodeComments(outcome.editedFile);
102
validateConsistentIndentation(outcome.editedFile, false, outcome.annotations);
103
}
104
});
105
});
106
stest({ description: 'move to class', language: 'typescript' }, async (testingServiceCollection) => {
107
return simulateCodeMapper(testingServiceCollection, {
108
files: [fromFixture('codeMapper/fibonacci_recursive.ts')],
109
activeDocument: 'fibonacci_recursive.ts',
110
selection: [0, 0, 10, 0],
111
focusLocations: [],
112
codeBlock: [
113
'class FibonacciGenerator {',
114
' static fibonacci_recursive(n: number): number {',
115
' if (n === 1) return 0;',
116
' if (n === 2) return 1;',
117
' return FibonacciGenerator.fibonacci_recursive(n - 1) + FibonacciGenerator.fibonacci_recursive(n - 2);',
118
' }',
119
'',
120
' static generate_fibonacci_recursive(n: number): number[] {',
121
' const fibonacci_series = Array.from({ length: n }, (_, i) => FibonacciGenerator.fibonacci_recursive(i + 1));',
122
' return fibonacci_series;',
123
' }',
124
'}'
125
],
126
validate: async (outcome, workspace, accessor) => {
127
assert.ok(outcome.appliedEdits.length, 'has edits');
128
assert.ok(outcome.editedFile.includes('class FibonacciGenerator {'), 'has class');
129
assert.equal(numOccurrences(outcome.editedFile, 'console.log(function generate_fibonacci_recursive'), 0, 'no references to the old function');
130
assert.equal(numOccurrences(outcome.editedFile, 'console.log(FibonacciGenerator.generate_fibonacci_recursive'), 1, 'references to the new method');
131
assertNoElidedCodeComments(outcome.editedFile);
132
validateConsistentIndentation(outcome.editedFile, false, outcome.annotations);
133
}
134
});
135
});
136
stest({ description: 'add import', language: 'typescript' }, async (testingServiceCollection) => {
137
return simulateCodeMapper(testingServiceCollection, {
138
files: [fromFixture('codeMapper/fibonacci_recursive.ts')],
139
activeDocument: 'fibonacci_recursive.ts',
140
selection: [11, 0, 17, 0],
141
focusLocations: [],
142
codeBlock: [
143
'import { readFileSync } from \'fs\';',
144
'',
145
'// Assuming the mock_data is stored in a file named \'data.json\' with the structure { "number": 10 }',
146
'const data = JSON.parse(readFileSync(\'data.json\', \'utf8\'));',
147
'',
148
'console.log(\'\\nFibonacci Series using recursion:\');',
149
'console.log(generate_fibonacci_recursive(data.number));'
150
],
151
validate: async (outcome, workspace, accessor) => {
152
assert.ok(outcome.appliedEdits.length, 'has edits');
153
assert.equal(numOccurrences(outcome.editedFile, 'import { readFileSync } from \'fs\';'), 1, 'has import');
154
assertNoElidedCodeComments(outcome.editedFile);
155
validateConsistentIndentation(outcome.editedFile, false, outcome.annotations);
156
}
157
});
158
});
159
stest({ description: 'break up function, large file', language: 'typescript' }, async (testingServiceCollection) => {
160
return simulateCodeMapper(testingServiceCollection, {
161
files: [fromFixture('codeMapper/scanner.ts'), fromFixture('codeMapper/scannerTypes.ts')],
162
activeDocument: 'scanner.ts',
163
selection: [25, 0, 50, 0],
164
focusLocations: [],
165
codeBlock: [
166
'// Checks if a character code is a numeric hex digit (0-9)',
167
'function isNumericHexDigit(ch: number): boolean {',
168
' return ch >= CharacterCodes._0 && ch <= CharacterCodes._9;',
169
'}',
170
'',
171
'// Checks if a character code is an alphabetic hex digit (A-F or a-f)',
172
'function isAlphabeticHexDigit(ch: number): boolean {',
173
' return (ch >= CharacterCodes.A && ch <= CharacterCodes.F) || (ch >= CharacterCodes.a && ch <= CharacterCodes.f);',
174
'}',
175
'',
176
'// Calculates the numerical value of a hex digit character code',
177
'function calculateHexValue(ch: number): number {',
178
' if (isNumericHexDigit(ch)) {',
179
' return ch - CharacterCodes._0;',
180
' } else if (ch >= CharacterCodes.A && ch <= CharacterCodes.F) {',
181
' return 10 + ch - CharacterCodes.A;',
182
' } else if (ch >= CharacterCodes.a && ch <= CharacterCodes.f) {',
183
' return 10 + ch - CharacterCodes.a;',
184
' }',
185
' // Return -1 if not a hex digit, though this case should be handled before calling this function',
186
' return -1;',
187
'}',
188
'',
189
'// The main scanning function, refactored to use the helper functions',
190
'function scanHexDigits(count: number, exact?: boolean): number {',
191
' let digits = 0;',
192
' let value = 0;',
193
' while (digits < count || !exact) {',
194
' let ch = text.charCodeAt(pos);',
195
' if (isNumericHexDigit(ch) || isAlphabeticHexDigit(ch)) {',
196
' value = value * 16 + calculateHexValue(ch);',
197
' } else {',
198
' break;',
199
' }',
200
' pos++;',
201
' digits++;',
202
' }',
203
' if (digits < count) {',
204
' value = -1;',
205
' }',
206
' return value;',
207
'}'
208
],
209
validate: async (outcome, workspace, accessor) => {
210
assert.ok(outcome.appliedEdits.length, 'has edits');
211
assert.equal(numOccurrences(outcome.editedFile, '\t// Checks if a character code is a numeric hex digit (0-9)'), 1, 'inserted with indent');
212
assert.deepEqual(await getWorkspaceDiagnostics(accessor, workspace, KnownDiagnosticProviders.tsc), [], 'no diagnostics');
213
assertNoElidedCodeComments(outcome.editedFile);
214
validateConsistentIndentation(outcome.editedFile, false, outcome.annotations);
215
}
216
});
217
});
218
219
stest({ description: 'make changes in package.json', language: 'json' }, async (testingServiceCollection) => {
220
return simulateCodeMapper(testingServiceCollection, {
221
files: [fromFixture('codeMapper/package.json')],
222
activeDocument: 'package.json',
223
selection: [11, 0, 17, 0],
224
focusLocations: [],
225
codeBlock: [
226
`"keywords": [ 'ai' ],`,
227
`"devDependencies": {`,
228
` "@microsoft/tiktokenizer": "^1.0.6",`,
229
` "@types/node": "^20.11.30",`,
230
` "@vscode/test-cli": "^0.0.9",`,
231
` "@vscode/test-electron": "^2.4.1",`,
232
` "@types/vscode": "^1.89.0",`,
233
` "esbuild": "0.25.0",`,
234
` "npm-dts": "^1.3.12",`, // removed mocha
235
` "prettier": "^2.8.7",`,
236
` "tsx": "^4.6.2",`,
237
` "typescript": "^5.5.0",`, // update version
238
` "vitest": "^0.34.0"`, // inserted
239
`},`,
240
`"scripts": {`,
241
` "test": "vitest"`,
242
`}`
243
],
244
validate: async (outcome, workspace, accessor) => {
245
assert.ok(outcome.appliedEdits.length, 'has edits');
246
const pgkJSON = assertJSON(outcome.editedFile);
247
assert.equal(pgkJSON.devDependencies['vitest'], '^0.34.0', 'has vitest');
248
assert.equal(pgkJSON.devDependencies['mocha'], undefined, 'no longer has mocha');
249
assert.deepEqual(pgkJSON.keywords, ['ai'], 'keyword added');
250
assert.equal(pgkJSON.scripts['test'], 'vitest', 'has test script');
251
assertNoElidedCodeComments(outcome.editedFile);
252
validateConsistentIndentation(outcome.editedFile, false, outcome.annotations);
253
}
254
});
255
});
256
257
stest({ description: 'does not delete random parts of code (big file)', language: 'typescript' }, (testingServiceCollection) => {
258
return simulateCodeMapper(testingServiceCollection, {
259
files: [fromFixture('codeMapper/quickInput.ts')],
260
activeDocument: 'quickInput.ts',
261
selection: [0, 0, 25, 0],
262
focusLocations: [],
263
textBeforeCodeBlock: 'Add a utility function to calculate Fibonacci numbers.',
264
codeBlock: [
265
`// ...existing code...`,
266
``,
267
`/**`,
268
` * Calculates the nth Fibonacci number.`,
269
` * @param n - The position in the Fibonacci sequence (0-based).`,
270
` * @returns The nth Fibonacci number.`,
271
` */`,
272
`function fibonacci(n: number): number {`,
273
` if (n <= 1) {`,
274
` return n;`,
275
` }`,
276
` let a = 0, b = 1;`,
277
` for (let i = 2; i <= n; i++) {`,
278
` const temp = a + b;`,
279
` a = b;`,
280
` b = temp;`,
281
` }`,
282
` return b;`,
283
`}`,
284
``,
285
`// ...existing code...`,
286
],
287
validate: async (outcome, workspace, accessor) => {
288
assert.ok(outcome.appliedEdits.length, 'has edits');
289
const newText = outcome.editedFile;
290
assert.ok(newText.includes('function fibonacci'), 'fibonacci function not added');
291
assert.ok(newText.length > outcome.originalFileContent.length, 'deleted code');
292
assertNoElidedCodeComments(outcome.editedFile);
293
validateConsistentIndentation(outcome.editedFile, false, outcome.annotations);
294
}
295
});
296
});
297
298
stest({ description: 'does not remove stale imports #11766', language: 'typescript' }, (testingServiceCollection) => {
299
return simulateCodeMapper(testingServiceCollection, {
300
files: [fromFixture('codeMapper/index.ts')],
301
activeDocument: 'index.ts',
302
selection: [0, 0, 25, 0],
303
focusLocations: [],
304
textBeforeCodeBlock: 'Update saveCategorization endpoint to use the new CSV utility function.',
305
codeBlock: [
306
`import { saveCategorizationToCSV } from '$lib/csvUtils';`,
307
``,
308
`export const POST: RequestHandler = async ({ request }) => {`,
309
` const { respondentId, categories, notActionable, highlight } = await request.json();`,
310
` const filePath = 'data/export.csv';`,
311
``,
312
` saveCategorizationToCSV(filePath, respondentId, categories, notActionable, highlight);`,
313
``,
314
` return new Response(null, { status: 200 });`,
315
`};`,
316
],
317
validate: async (outcome, workspace, accessor) => {
318
assert.ok(outcome.appliedEdits.length, 'has edits');
319
const newText = outcome.editedFile;
320
assert.ok(!newText.includes(`'path'`), 'rewritten file contains unused imports');
321
assert.ok(!newText.includes(`'fs'`), 'rewritten file contains unused imports');
322
assertNoElidedCodeComments(outcome.editedFile);
323
validateConsistentIndentation(outcome.editedFile, false, outcome.annotations);
324
}
325
});
326
});
327
stest({ description: 'sorted product icons (60kb) - modify & insert', language: 'markdown', model }, (testingServiceCollection) => {
328
const inputFile = fromFixture('codeMapper/product-icons-sorted.md');
329
return simulateCodeMapper(testingServiceCollection, {
330
files: [inputFile],
331
activeDocument: inputFile.fileName,
332
selection: [0, 0, 0, 0],
333
focusLocations: [],
334
textBeforeCodeBlock: '',
335
codeBlock: [
336
`<!-- ... existing content ... -->`,
337
`## Icon Listing`,
338
``,
339
`Below is a complete listing of the built-in product icons by identifier.`, // modified line
340
``,
341
`The ID of the icon identifies the location where the icon is used. The default codicon ID describes which icon from the codicon library is used by default, and the preview shows what that icon looks like.`,
342
`<!-- ... existing content ... -->`,
343
`|<i class="codicon codicon-wrench"></i>|wrench|`,
344
`|<i class="codicon codicon-wrench-extra"></i>|wrench-extra|`, // added line
345
`|<i class="codicon codicon-x"></i>|ports-stop-forward-icon|x|Icon for the stop forwarding action.|`,
346
`<!-- ... existing content ... -->`,
347
],
348
validate: async (outcome, workspace, accessor) => {
349
assert.ok(outcome.appliedEdits.length, 'has edits');
350
const newText = outcome.editedFile;
351
const expectedText = inputFile.fileContents
352
.replace('Below is a listing', 'Below is a complete listing')
353
.replace('|<i class="codicon codicon-wrench"></i>|wrench|', '|<i class="codicon codicon-wrench"></i>|wrench|\n|<i class="codicon codicon-wrench-extra"></i>|wrench-extra|');
354
355
assert.equal(newText, expectedText, 'rewritten file is as expected: ');
356
assertNoElidedCodeComments(outcome.editedFile);
357
}
358
});
359
});
360
361
stest({ description: 'mixed product icons (60kb) - modify & insert', language: 'markdown', model }, (testingServiceCollection) => {
362
const inputFile = fromFixture('codeMapper/product-icons-mixed.md');
363
return simulateCodeMapper(testingServiceCollection, {
364
files: [inputFile],
365
activeDocument: inputFile.fileName,
366
selection: [0, 0, 0, 0],
367
focusLocations: [],
368
textBeforeCodeBlock: '',
369
codeBlock: [
370
`<!-- ... existing content ... -->`,
371
`## Icon Listing`,
372
``,
373
`Below is a complete listing of the built-in product icons by identifier.`, // modified line
374
``,
375
`The ID of the icon identifies the location where the icon is used. The default codicon ID describes which icon from the codicon library is used by default, and the preview shows what that icon looks like.`,
376
`<!-- ... existing content ... -->`,
377
`|<i class="codicon codicon-wrench-subaction"></i>|wrench-subaction|`,
378
`|<i class="codicon codicon-wrench-extra"></i>|wrench-extra|`, // added line
379
`|<i class="codicon codicon-x"></i>|x|`,
380
`<!-- ... existing content ... -->`,
381
],
382
validate: async (outcome, workspace, accessor) => {
383
assert.ok(outcome.appliedEdits.length, 'has edits');
384
const newText = outcome.editedFile;
385
const expectedText = inputFile.fileContents
386
.replace('Below is a listing', 'Below is a complete listing')
387
.replace('|<i class="codicon codicon-wrench-subaction"></i>|wrench-subaction|', '|<i class="codicon codicon-wrench-subaction"></i>|wrench-subaction|\n|<i class="codicon codicon-wrench-extra"></i>|wrench-extra|');
388
389
assert.equal(newText, expectedText, 'rewritten file is as expected: ');
390
assertNoElidedCodeComments(outcome.editedFile);
391
}
392
});
393
});
394
395
stest({ description: 'other tests updated similarly', language: 'typescript', model }, (testingServiceCollection) => {
396
// the code clock contains a comment: 'other tests updated similarly'
397
const inputFile = fromFixture('codeMapper/extHostExtensionActivator.test.ts');
398
return simulateCodeMapper(testingServiceCollection, {
399
files: [inputFile],
400
activeDocument: inputFile.fileName,
401
selection: [0, 0, 0, 0],
402
focusLocations: [],
403
textBeforeCodeBlock: '',
404
codeBlock: [
405
`suite('ExtensionsActivator', () => {`,
406
``,
407
` ensureNoDisposablesAreLeakedInTestSuite();`,
408
``,
409
` let activator: ExtensionsActivator | undefined;`,
410
``,
411
` teardown(() => {`,
412
` if (activator) {`,
413
` activator.dispose();`,
414
` activator = undefined;`,
415
` }`,
416
` });`,
417
``,
418
` const idA = new ExtensionIdentifier(\`a\`);`,
419
` const idB = new ExtensionIdentifier(\`b\`);`,
420
` const idC = new ExtensionIdentifier(\`c\`);`,
421
``,
422
` test('calls activate only once with sequential activations', async () => {`,
423
` const host = new SimpleExtensionsActivatorHost();`,
424
` activator = createActivator(host, [`,
425
` desc(idA)`,
426
` ]);`,
427
``,
428
` await activator.activateByEvent('*', false);`,
429
` assert.deepStrictEqual(host.activateCalls, [idA]);`,
430
``,
431
` await activator.activateByEvent('*', false);`,
432
` assert.deepStrictEqual(host.activateCalls, [idA]);`,
433
` });`,
434
``,
435
` test('calls activate only once with parallel activations', async () => {`,
436
` const extActivation = new ExtensionActivationPromiseSource();`,
437
` const host = new PromiseExtensionsActivatorHost([`,
438
` [idA, extActivation]`,
439
` ]);`,
440
` activator = createActivator(host, [`,
441
` desc(idA, [], ['evt1', 'evt2'])`,
442
` ]);`,
443
``,
444
` const activate1 = activator.activateByEvent('evt1', false);`,
445
` const activate2 = activator.activateByEvent('evt2', false);`,
446
``,
447
` extActivation.resolve();`,
448
``,
449
` await activate1;`,
450
` await activate2;`,
451
``,
452
` assert.deepStrictEqual(host.activateCalls, [idA]);`,
453
` });`,
454
``,
455
` // ...other tests updated similarly...`,
456
``,
457
`});`,
458
],
459
validate: async (outcome, workspace, accessor) => {
460
assert.ok(outcome.appliedEdits.length, 'has edits');
461
assert.equal(numRegexOccurrences(outcome.editedFile, /(?!const\s*)activator = createActivator/g), 9);
462
assert.equal(numOccurrences(outcome.editedFile, 'activator.dispose();'), 1);
463
assertNoElidedCodeComments(outcome.editedFile);
464
validateConsistentIndentation(outcome.editedFile, false, outcome.annotations);
465
}
466
});
467
});
468
469
stest({ description: 'looping in short yaml file', language: 'yaml' }, (testingServiceCollection) => {
470
const inputFile = fromFixture('codeMapper/product-build-linux.yml');
471
return simulateCodeMapper(testingServiceCollection, {
472
files: [inputFile],
473
activeDocument: inputFile.fileName,
474
selection: [363, 0, 387, 0],
475
focusLocations: [],
476
textBeforeCodeBlock: `I'll modify the snap building section to split it into two parts - preparation and building within the FOO container.`,
477
codeBlock: [
478
` - script: |`,
479
` set -e`,
480
` npm run gulp "vscode-linux-$(VSCODE_ARCH)-prepare-snap"`,
481
``,
482
` # Create a tarball of the snap content`,
483
` SNAP_ROOT="$(pwd)/.build/linux/snap/$(VSCODE_ARCH)"`,
484
` SNAP_TARBALL_PATH="$(pwd)/.build/linux/snap-tarball"`,
485
` mkdir -p "$SNAP_TARBALL_PATH"`,
486
` tar -czf "$SNAP_TARBALL_PATH/snap-$(VSCODE_ARCH).tar.gz" -C "$SNAP_ROOT" .`,
487
` displayName: Prepare snap package`,
488
``,
489
` - task: 1ES.PublishPipelineArtifact@2`,
490
` displayName: "Publish snap tarball for container build"`,
491
` inputs:`,
492
` targetPath: .build/linux/snap-tarball`,
493
` artifact: snap-$(VSCODE_ARCH)`,
494
``,
495
` - task: DownloadPipelineArtifact@2`,
496
` displayName: "Download snap tarball for container build"`,
497
` inputs:`,
498
` artifact: snap-$(VSCODE_ARCH)`,
499
` path: .build/linux/snap-tarball`,
500
``,
501
` - script: |`,
502
` set -e`,
503
``,
504
` # Define variables`,
505
` SNAP_ROOT="$(pwd)/.build/linux/snap/$(VSCODE_ARCH)"`,
506
``,
507
` # Extract snap tarball content if it doesn't exist`,
508
` mkdir -p "$SNAP_ROOT"`,
509
``,
510
` # Run build steps inside the FOO container`,
511
` sudo docker run \\`,
512
` --rm \\`,
513
` -v "$(pwd):/workspace" \\`,
514
` -e VSCODE_QUALITY \\`,
515
` -e VSCODE_ARCH \\`,
516
` FOO \\`,
517
` /bin/bash -c "cd /workspace && \\`,
518
` # Get snapcraft version`,
519
` snapcraft --version && \\`,
520
``,
521
` # Make sure we get latest packages`,
522
` apt-get update && \\`,
523
` apt-get upgrade -y && \\`,
524
` apt-get install -y curl apt-transport-https ca-certificates && \\`,
525
``,
526
` # Define variables`,
527
` SNAP_ROOT='/workspace/.build/linux/snap/\$VSCODE_ARCH' && \\`,
528
``,
529
` # Unpack snap tarball artifact, in order to preserve file perms`,
530
` (cd /workspace/.build/linux && tar -xzf snap-tarball/snap-\$VSCODE_ARCH.tar.gz) && \\`,
531
``,
532
` # Create snap package`,
533
` BUILD_VERSION=\$(date +%s) && \\`,
534
` SNAP_FILENAME=code-\$VSCODE_QUALITY-\$VSCODE_ARCH-\$BUILD_VERSION.snap && \\`,
535
` SNAP_PATH=\$SNAP_ROOT/\$SNAP_FILENAME && \\`,
536
` case \$VSCODE_ARCH in \\`,
537
` x64) SNAPCRAFT_TARGET_ARGS=\"\" ;; \\`,
538
` *) SNAPCRAFT_TARGET_ARGS=\"--target-arch \$VSCODE_ARCH\" ;; \\`,
539
` esac && \\`,
540
` (cd \$SNAP_ROOT/code-* && snapcraft snap \$SNAPCRAFT_TARGET_ARGS --output \"\$SNAP_PATH\") && \\`,
541
``,
542
` # Fix permissions for files created inside container`,
543
` chown -R $(id -u):$(id -g) /workspace/.build/linux/snap"`,
544
``,
545
` # Find the generated snap file`,
546
` SNAP_PATH=$(find .build/linux/snap/$(VSCODE_ARCH) -name "*.snap" | head -n 1)`,
547
` echo "##vso[task.setvariable variable=SNAP_PATH]$SNAP_PATH"`,
548
``,
549
` # Save the directory path for SBOM generation`,
550
` SNAP_EXTRACTED_PATH=$(find .build/linux/snap/$(VSCODE_ARCH) -maxdepth 1 -type d -name 'code-*')`,
551
` echo "##vso[task.setvariable variable=SNAP_EXTRACTED_PATH]$SNAP_EXTRACTED_PATH"`,
552
` env:`,
553
` VSCODE_QUALITY: \${{ parameters.VSCODE_QUALITY }}`,
554
` VSCODE_ARCH: $(VSCODE_ARCH)`,
555
` displayName: Build snap package inside FOO container`,
556
],
557
validate: async (outcome, workspace, accessor) => {
558
assert.ok(outcome.appliedEdits.length, 'has edits');
559
const originalFileLineCount = outcome.originalFileContent.split('\n').length;
560
const editedFileLineCount = outcome.editedFile.split('\n').length;
561
assert.ok(editedFileLineCount >= originalFileLineCount, 'deleted code');
562
assertNoElidedCodeComments(outcome.editedFile);
563
}
564
});
565
});
566
567
});
568
569
function numOccurrences(str: string, substr: string): number {
570
return str.split(substr).length - 1;
571
}
572
function numRegexOccurrences(str: string, regex: RegExp): number {
573
const matches = str.match(regex);
574
return matches ? matches.length : 0;
575
}
576
577
interface IOutcome {
578
appliedEdits: TextEdit[];
579
annotations: OutcomeAnnotation[];
580
originalFileContent: string;
581
editedFile: string;
582
}
583
584
interface ICodeMapperScenario {
585
files: IFile[];
586
activeDocument: string;
587
selection: [number, number, number, number];
588
focusLocations: [number, number, number, number][];
589
textBeforeCodeBlock?: string;
590
codeBlock: string[];
591
validate: (outcome: IOutcome, workspace: SimulationWorkspace, accessor: ITestingServicesAccessor) => Promise<void>;
592
}
593
594
async function simulateCodeMapper(testingServiceCollection: TestingServiceCollection, scenario: ICodeMapperScenario): Promise<void> {
595
const workspace = setupSimulationWorkspace(testingServiceCollection, { files: scenario.files });
596
const accessor = testingServiceCollection.createTestingAccessor();
597
const testRuntime = accessor.get(ISimulationTestRuntime);
598
const states: IWorkspaceState[] = [];
599
const source = new CancellationTokenSource();
600
try {
601
const document = workspace.getDocument(scenario.activeDocument).document;
602
workspace.setCurrentDocument(document.uri);
603
const selection = new Selection(...scenario.selection);
604
workspace.setCurrentSelection(selection);
605
606
const workspacePath = workspace.getFilePath(document.uri);
607
states.push({
608
kind: 'initial',
609
file: {
610
workspacePath,
611
relativeDiskPath: await testRuntime.writeFile(workspacePath + '.txt', document.getText(), INLINE_INITIAL_DOC_TAG),
612
languageId: document.languageId
613
},
614
additionalFiles: [],
615
languageId: document.languageId,
616
selection: toIRange(selection),
617
range: toIRange(selection),
618
diagnostics: [],
619
});
620
621
const editor = accessor.get(ITabsAndEditorsService).activeTextEditor!;
622
const documentContext = IDocumentContext.fromEditor(editor);
623
const codeMapper = accessor.get(IInstantiationService).createInstance(CodeMapper);
624
625
const workingCopyDocument = new WorkingCopyOriginalDocument(document.getText());
626
const response: MappedEditsResponseStream = {
627
textEdit(target, edits) {
628
if (isEqual(target, document.uri)) {
629
edits = Array.isArray(edits) ? edits : [edits];
630
workspace.applyEdits(document.uri, edits);
631
const offsetEdits = workingCopyDocument.transformer.toOffsetEdit(edits);
632
workingCopyDocument.applyOffsetEdits(offsetEdits);
633
} else {
634
throw new Error('Unexpected target: ' + target);
635
}
636
},
637
notebookEdit(target, edits) {
638
edits = Array.isArray(edits) ? edits : [edits];
639
workspace.applyNotebookEdits(target, edits);
640
},
641
};
642
643
const codeMap = scenario.codeBlock.join('\n');
644
const input: ICodeMapperExistingDocument = { createNew: false, codeBlock: codeMap, uri: document.uri, markdownBeforeBlock: scenario.textBeforeCodeBlock, existingDocument: documentContext.document };
645
const originalFileContent = document.getText();
646
const result = await codeMapper.mapCode(input, response, undefined, source.token);
647
if (!result) {
648
return; // cacnelled
649
}
650
651
652
testRuntime.setOutcome({
653
kind: 'edit',
654
files: [{ srcUri: workspacePath, post: workspacePath }],
655
annotations: result.annotations
656
});
657
658
states.push({
659
kind: 'interaction',
660
changedFiles: [{
661
workspacePath,
662
relativeDiskPath: await testRuntime.writeFile(workspacePath, document.getText(), INLINE_CHANGED_DOC_TAG),
663
languageId: document.languageId
664
}],
665
annotations: result.annotations,
666
fileName: workspace.getFilePath(editor.document.uri),
667
languageId: document.languageId,
668
diagnostics: {},
669
selection: toIRange(editor.selection),
670
range: toIRange(editor.selection),
671
interaction: {
672
query: '',
673
actualIntent: Intent.Unknown,
674
detectedIntent: undefined,
675
},
676
requestCount: 0
677
});
678
679
const appliedEdits = workingCopyDocument.transformer.toTextEdits(workingCopyDocument.appliedEdits);
680
681
await scenario.validate({
682
appliedEdits,
683
annotations: result.annotations,
684
originalFileContent,
685
editedFile: document.getText(),
686
}, workspace, accessor);
687
} finally {
688
source.dispose();
689
await teardownSimulationWorkspace(accessor, workspace);
690
await testRuntime.writeFile('inline-simulator.txt', JSON.stringify(states, undefined, 2), INLINE_STATE_TAG);
691
692
}
693
}
694
});
695
696