Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/externalScenarios.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 * as fs from 'fs';
7
import * as path from 'path';
8
import { Intent } from '../../src/extension/common/constants';
9
import { ILanguageFeaturesService } from '../../src/platform/languages/common/languageFeaturesService';
10
import { IDeserializedWorkspaceState } from '../../src/platform/test/node/promptContextModel';
11
import { TestingServiceCollection } from '../../src/platform/test/node/services';
12
import { SimulationWorkspace } from '../../src/platform/test/node/simulationWorkspace';
13
import { ITestProvider } from '../../src/platform/testing/common/testProvider';
14
import { assertType } from '../../src/util/vs/base/common/types';
15
import { URI } from '../../src/util/vs/base/common/uri';
16
import { SyncDescriptor } from '../../src/util/vs/platform/instantiation/common/descriptors';
17
import { Range } from '../../src/vscodeTypes';
18
import { ISimulationTestRuntime, SimulationSuite, SimulationTest, SimulationTestFunction, SimulationTestRuntime } from '../base/stest';
19
import { fetchConversationScenarios, IConversationTestCase, Scenario } from '../e2e/scenarioLoader';
20
import { generateScenarioTestRunner } from '../e2e/scenarioTest';
21
import { simulateInlineChat } from './inlineChatSimulator';
22
import { LSIFLanguageFeaturesService } from './language/lsifLanguageFeatureService';
23
import { simulatePanelCodeMapper } from './panelCodeMapperSimulator';
24
import { INLINE_CHANGED_DOC_TAG, SIDEBAR_RAW_RESPONSE_TAG } from './shared/sharedTypes';
25
import { SimulationTestProvider } from './simulationTestProvider';
26
import { EditTestStrategy, IDeserializedWorkspaceStateBasedScenario } from './types';
27
28
export interface ITestDiscoveryOptions {
29
chatKind: 'inline' | 'panel';
30
applyChatCodeBlocks?: boolean;
31
}
32
33
/**
34
* Discovers test scenarios in a given root folder.
35
*
36
* This function recursively searches through the root folder and its subfolders to find any '.conversation.json' files.
37
* If a '.conversation.json' file is found in a folder, a simulation suite is created for that folder.
38
* If no '.conversation.json' file is found, the function will recursively search through the subfolders.
39
*
40
* @param rootFolder - The root folder to start the search from.
41
* @param chatKind - The type of chat to be simulated, either 'inline' or 'panel'.
42
*
43
* @returns A promise that resolves to an array of SimulationSuite objects, each representing a test scenario.
44
*/
45
export async function discoverTests(rootFolder: string, options: ITestDiscoveryOptions): Promise<SimulationSuite[]> {
46
47
const rootFolderContents = await fs.promises.readdir(rootFolder, { withFileTypes: true });
48
49
const containsConversationFile = rootFolderContents.some(f => f.isFile() && f.name.endsWith('.conversation.json'));
50
51
if (containsConversationFile) {
52
return [createSimulationSuite(rootFolder, options)];
53
} else {
54
const foldersWithScenarios = rootFolderContents.filter(f => f.isDirectory()).map(f => path.join(rootFolder, f.name));
55
if (foldersWithScenarios.length === 0) {
56
return [];
57
}
58
const scenarios = await Promise.all(foldersWithScenarios.map(f => discoverTests(f, options)));
59
return scenarios.flat();
60
}
61
}
62
63
function createSimulationSuite(folderWithScenarios: string, options: ITestDiscoveryOptions): SimulationSuite {
64
const suiteName = path.basename(folderWithScenarios);
65
const chatKind = options.chatKind ?? 'panel';
66
const suite = new SimulationSuite({ title: suiteName, location: chatKind });
67
const scenarios = fetchConversationScenarios(folderWithScenarios);
68
69
for (const scenario of scenarios) {
70
if (chatKind === 'inline') {
71
for (const conversation of scenario) {
72
const runner = generateInlineScenarioTestRunner(conversation);
73
const testName = conversation.name.replace(/.conversation\.json$/, '');
74
const conversationPath = path.join(conversation.scenarioFolderPath, conversation.name);
75
suite.tests.push(new SimulationTest({ description: testName }, { conversationPath, scenarioFolderPath: conversation.scenarioFolderPath, stateFile: conversation.stateFile }, suite, runner));
76
}
77
} else {
78
const isSlashEdit = scenario[0].question.startsWith(`/${Intent.Edit}`) || scenario[0].question.startsWith(`/${Intent.Agent}`);
79
const testName = scenario[0].name.replace(/.conversation\.json$/, '');
80
const conversationPath = path.join(scenario[0].scenarioFolderPath, scenario[0].name);
81
let runner: SimulationTestFunction;
82
83
for (const conversation of scenario) {
84
if (options.applyChatCodeBlocks) {
85
conversation.applyChatCodeBlocks = true;
86
}
87
}
88
89
if (isSlashEdit) {
90
// /edit in the sidebar needs more special handling
91
runner = generateSlashEditScenarioTestRunner(scenario);
92
} else {
93
runner = generateScenarioTestRunner(scenario, async (accessor, question, userVisibleAnswer, rawResponse) => {
94
accessor.get(ISimulationTestRuntime).writeFile(`${testName}.md`, rawResponse, SIDEBAR_RAW_RESPONSE_TAG);
95
return { success: true };
96
});
97
}
98
99
suite.tests.push(new SimulationTest({ description: testName }, { conversationPath, scenarioFolderPath: scenario[0].scenarioFolderPath, stateFile: scenario[0].stateFile, }, suite, runner));
100
}
101
}
102
103
return suite;
104
}
105
106
function generateSlashEditScenarioTestRunner(scenario: Scenario): SimulationTestFunction {
107
return async (testingServiceCollection) => {
108
109
assert(scenario.length > 0, 'Expected at least 1 conversation in the scenario');
110
assertType(scenario[0].getState !== undefined, 'Expected state to be defined in the first conversation test case');
111
for (let i = 1; i < scenario.length; i++) {
112
assertType(scenario[i].getState === undefined, 'Expected state to be undefined in subsequent conversations');
113
}
114
115
const state = scenario[0].getState();
116
117
const scenario2: IDeserializedWorkspaceStateBasedScenario = {
118
workspaceState: state,
119
scenarioFolderPath: scenario[0].scenarioFolderPath,
120
queries: scenario.map((conversation, index) => {
121
return {
122
query: conversation.question,
123
expectedIntent: undefined,
124
validate: async (outcome, workspace, accessor) => assert.ok(true),
125
};
126
}),
127
extraWorkspaceSetup: (workspace) => extraWorkspaceSetup(testingServiceCollection, state, workspace),
128
};
129
130
await simulatePanelCodeMapper(testingServiceCollection, scenario2, EditTestStrategy.Edits);
131
};
132
}
133
134
function generateInlineScenarioTestRunner(conversation: IConversationTestCase): (testingServiceCollection: TestingServiceCollection) => Promise<void> {
135
return async (testingServiceCollection) => {
136
137
assertType(conversation.getState !== undefined, 'Expected state to be defined in conversation test case');
138
139
const state = conversation.getState();
140
141
const scenario: IDeserializedWorkspaceStateBasedScenario = {
142
workspaceState: state,
143
scenarioFolderPath: conversation.scenarioFolderPath,
144
queries: [{
145
query: conversation.question,
146
expectedIntent: undefined,
147
validate: async (outcome, workspace, accessor) => assert.ok(true),
148
}],
149
extraWorkspaceSetup: (workspace) => extraWorkspaceSetup(testingServiceCollection, state, workspace),
150
onBeforeStart: async (accessor) => {
151
const testContext = accessor.get(ISimulationTestRuntime);
152
const dataToLog = [
153
`The conversation input contained the following data.`,
154
`Name: ${conversation.name}`,
155
`Query: ${conversation.question}`,
156
`State: \n${JSON.stringify(state)}`
157
].join('\n');
158
testContext.log(dataToLog);
159
}
160
};
161
162
await simulateInlineChat(testingServiceCollection, scenario);
163
};
164
}
165
166
function extraWorkspaceSetup(testingServiceCollection: TestingServiceCollection, state: IDeserializedWorkspaceState, workspace: SimulationWorkspace): void {
167
if (state.lsifIndex) {
168
testingServiceCollection.define(ILanguageFeaturesService, new SyncDescriptor(
169
LSIFLanguageFeaturesService,
170
[
171
workspace,
172
path.join(state.workspaceFolders![0].fsPath, state.lsifIndex),
173
]
174
));
175
}
176
if (state.testFailures && state.workspaceFolders) {
177
testingServiceCollection.define(ITestProvider, new SimulationTestProvider(state.testFailures.map(f => ({
178
message: f.message,
179
testRange: new Range(f.line, f.column, f.line, f.column),
180
uri: URI.file(path.join(state.workspaceFolders![0].fsPath, f.file_path))
181
}))));
182
}
183
}
184
185
export class ExternalSimulationTestRuntime extends SimulationTestRuntime {
186
187
constructor(
188
baseDir: string,
189
testOutcomeDir: string,
190
runNumber: number
191
) {
192
super(baseDir, testOutcomeDir, runNumber);
193
}
194
195
override async writeFile(filename: string, contents: Uint8Array | string, tag: string): Promise<string> {
196
if (tag === INLINE_CHANGED_DOC_TAG) {
197
// This is a write file for a workspace file, we'll rename it to <basename>.post.<ext>
198
const ext = path.extname(filename);
199
const basename = path.basename(filename, ext);
200
filename = `${basename}.post${ext}`;
201
}
202
return super.writeFile(filename, contents, tag);
203
}
204
205
protected override massageFilename(filename: string): string {
206
const ext = path.extname(filename);
207
const basename = path.basename(filename, ext);
208
return `${basename}-${this.runNumber}${ext}`;
209
}
210
}
211
212