Path: blob/main/extensions/copilot/test/simulation/nesCoffeTests.ts
13389 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 * as fs from 'fs';5import * as path from 'path';6import { IRecordingInformation } from '../../src/extension/inlineEdits/common/observableWorkspaceRecordingReplayer';7import { LogEntry, serializeEdit } from '../../src/platform/workspaceRecorder/common/workspaceLog';8import { assert } from '../../src/util/vs/base/common/assert';9import { assertDefined } from '../../src/util/vs/base/common/types';10import { StringEdit, StringReplacement } from '../../src/util/vs/editor/common/core/edits/stringEdit';11import { OffsetRange } from '../../src/util/vs/editor/common/core/ranges/offsetRange';12import { SimulationOptions } from '../base/simulationOptions';13import { Configuration, SimulationSuite, SimulationTest } from '../base/stest';14import { InlineEditTester } from './inlineEdit/inlineEditTester';15import { CompletionStests } from './nesCoffeTestsTypes';16import { nesOptionsToConfigurations } from './nesOptionsToConfigurations';1718const TEST_FILE_SUFFIX = '.completion.yml';19const RESULT_FILE_SUFFIX = '.response.json';2021export async function discoverCoffeTests(rootFolder: string, options: SimulationOptions) {22const rootFolderContents = await fs.promises.readdir(rootFolder, { withFileTypes: true });2324const recordingFiles = rootFolderContents.filter(fileEntry => fileEntry.isFile() && fileEntry.name.endsWith(TEST_FILE_SUFFIX));2526const tester = new InlineEditTester();2728const configurations = nesOptionsToConfigurations(options);2930const rootSuite = new SimulationSuite({ title: 'NES', location: 'external' });3132let tests = recordingFiles.map((file) => generateExternalStestFromRecording(file, rootSuite, tester, configurations));3334tests = tests.sort((a, b) => a.fullName.localeCompare(b.fullName));3536rootSuite.tests.push(...tests);3738return rootSuite;39}4041function generateExternalStestFromRecording(file: fs.Dirent<string>, containingSuite: SimulationSuite, tester: InlineEditTester, configurations: Configuration<unknown>[]): SimulationTest {4243const basename = file.name;44const testName = basename.slice(0, -TEST_FILE_SUFFIX.length); // strip suffix45const filePath = path.join(file.parentPath, basename);464748const stest = new SimulationTest({ description: testName, configurations }, {}, containingSuite, async (collection) => {49const accessor = collection.createTestingAccessor();5051const fileContents = fs.readFileSync(filePath, 'utf8');52const testInput = CompletionStests.parseTestInput(fileContents);5354const recordingLog: LogEntry[] = [55{56documentType: '[email protected]',57kind: 'header',58repoRootUri: 'file:///Users/john/myProject/',59time: Date.now(),60uuid: 'random-uuid-1234',61},62];6364const filesWithoutTargetFile = testInput.state.openFiles.filter(f => f.uri !== testInput.completion.uri);6566const targetFile = testInput.state.openFiles.find(f => f.uri === testInput.completion.uri);67assertDefined(targetFile, `Target file ${testInput.completion.uri} not found in open files.`);6869const targetFileId = filesWithoutTargetFile.length; // careful: needs to be in sync with loop logic7071const { targetFileBeforeEdit, edit } = computeTargetFileBeforeEditAndEdit(targetFile);7273let id = 0;74for (const openFile of [...filesWithoutTargetFile, targetFile]) {75const currentFileId = id++;76const date = Date.now();77recordingLog.push({78kind: 'documentEncountered',79id: currentFileId,80relativePath: openFile.uri,81time: date,82});8384recordingLog.push({85kind: 'setContent',86id: currentFileId,87v: 1,88content: openFile === targetFile ? targetFileBeforeEdit : openFile.text,89time: date,90});9192recordingLog.push({93kind: 'opened',94id: currentFileId,95time: date,96});97}9899recordingLog.push({100kind: 'changed',101id: targetFileId,102time: Date.now(),103v: 2,104edit: serializeEdit(edit)105});106107const recording: IRecordingInformation = {108log: recordingLog,109};110111const r = await tester.runTestFromRecording(accessor, recording);112113const completions: CompletionStests.TestCompletion[] = [];114115if (r.aiRootedEdit && r.aiRootedEdit.edit.replacements.length > 0) {116const rootedEdit = r.aiRootedEdit;117const singleEdit = rootedEdit.edit.replacements[0];118119const baseTrans = rootedEdit.base.getTransformer();120const start = baseTrans.getPosition(singleEdit.replaceRange.start);121const end = baseTrans.getPosition(singleEdit.replaceRange.endExclusive);122123const trimmedEdit = rootedEdit.edit.removeCommonSuffixAndPrefix(rootedEdit.base.value);124125completions.push({126insertText: singleEdit.newText,127displayText: trimmedEdit.replacements.at(0)?.newText ?? '<edits disappeared during trimming>',128range: {129start: {130line: start.lineNumber - 1,131character: start.column - 1,132},133end: {134line: end.lineNumber - 1,135character: end.column - 1136}137},138});139}140141const completionsOutput: CompletionStests.TestOutput = {142completions,143};144145const resultFilePath = path.join(file.parentPath, `${testName}${RESULT_FILE_SUFFIX}`);146147await fs.promises.writeFile(resultFilePath, JSON.stringify(completionsOutput, null, 2));148});149150return stest;151}152153function computeTargetFileBeforeEditAndEdit(targetFile: CompletionStests.TestDocument): { targetFileBeforeEdit: string; edit: StringEdit } {154const cursorOffset = targetFile.text.indexOf('⮑');155assert(cursorOffset !== -1, 'Cursor marker ⮑ not found in target file text.');156157const targetFileWithoutCursor = targetFile.text.replace('⮑', '');158let wordAtCursorStartOffset = cursorOffset - 1;159while (wordAtCursorStartOffset > 0 && /(\w|\.)/.test(targetFileWithoutCursor[wordAtCursorStartOffset - 1])) {160wordAtCursorStartOffset--;161}162const editToRemoveWordAtCursor = StringEdit.create([StringReplacement.delete(new OffsetRange(wordAtCursorStartOffset, cursorOffset))]);163const editToInsertWordAtCursor = editToRemoveWordAtCursor.inverse(targetFileWithoutCursor);164const targetFileBeforeEdit = editToRemoveWordAtCursor.apply(targetFileWithoutCursor);165return { targetFileBeforeEdit, edit: editToInsertWordAtCursor };166}167168169