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