Skip to content

Commit a032678

Browse files
committed
Add commands for recording of scenario
This will add the commands and the implementation in the `MockGitHubApiServer` for the recording of a scenario.
1 parent dcac6f5 commit a032678

4 files changed

Lines changed: 165 additions & 7 deletions

File tree

extensions/ql-vscode/package.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,18 @@
645645
"command": "codeQL.gotoQL",
646646
"title": "CodeQL: Go to QL Code",
647647
"enablement": "codeql.hasQLSource"
648+
},
649+
{
650+
"command": "codeQL.mockGitHubApiServer.startRecording",
651+
"title": "CodeQL Mock GitHub API Server: Start Scenario Recording"
652+
},
653+
{
654+
"command": "codeQL.mockGitHubApiServer.saveScenario",
655+
"title": "CodeQL Mock GitHub API Server: Save Scenario"
656+
},
657+
{
658+
"command": "codeQL.mockGitHubApiServer.cancelRecording",
659+
"title": "CodeQL Mock GitHub API Server: Cancel Scenario"
648660
}
649661
],
650662
"menus": {
@@ -1104,6 +1116,18 @@
11041116
{
11051117
"command": "codeQLTests.showOutputDifferences",
11061118
"when": "false"
1119+
},
1120+
{
1121+
"command": "codeQL.mockGitHubApiServer.startRecording",
1122+
"when": "config.codeQL.variantAnalysis.mockGitHubApiServer && !codeQL.mockGitHubApiServer.recording"
1123+
},
1124+
{
1125+
"command": "codeQL.mockGitHubApiServer.saveScenario",
1126+
"when": "codeQL.mockGitHubApiServer.recording"
1127+
},
1128+
{
1129+
"command": "codeQL.mockGitHubApiServer.cancelRecording",
1130+
"when": "codeQL.mockGitHubApiServer.recording"
11071131
}
11081132
],
11091133
"editor/context": [

extensions/ql-vscode/src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,3 +449,9 @@ export class MockGitHubApiConfigListener extends ConfigListener implements MockG
449449
return !!MOCK_GH_API_SERVER.getValue<boolean>();
450450
}
451451
}
452+
453+
const MOCK_GH_API_SERVER_SCENARIOS_PATH = new Setting('mockGitHubApiServerScenariosPath', REMOTE_QUERIES_SETTING);
454+
455+
export function getMockGitHubApiServerScenariosPath(): string | undefined {
456+
return MOCK_GH_API_SERVER_SCENARIOS_PATH.getValue<string>();
457+
}

extensions/ql-vscode/src/extension.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ import {
116116
} from './remote-queries/gh-api/variant-analysis';
117117
import { VariantAnalysisManager } from './remote-queries/variant-analysis-manager';
118118
import { createVariantAnalysisContentProvider } from './remote-queries/variant-analysis-content-provider';
119+
import { MockGitHubApiServer } from './mocks/mock-gh-api-server';
119120

120121
/**
121122
* extension.ts
@@ -1190,6 +1191,27 @@ async function activateWithInstalledDistribution(
11901191
)
11911192
);
11921193

1194+
const mockServer = new MockGitHubApiServer();
1195+
ctx.subscriptions.push(mockServer);
1196+
ctx.subscriptions.push(
1197+
commandRunner(
1198+
'codeQL.mockGitHubApiServer.startRecording',
1199+
async () => await mockServer.recordScenario(),
1200+
)
1201+
);
1202+
ctx.subscriptions.push(
1203+
commandRunner(
1204+
'codeQL.mockGitHubApiServer.saveScenario',
1205+
async () => await mockServer.saveScenario(),
1206+
)
1207+
);
1208+
ctx.subscriptions.push(
1209+
commandRunner(
1210+
'codeQL.mockGitHubApiServer.cancelRecording',
1211+
async () => await mockServer.cancelRecording(),
1212+
)
1213+
);
1214+
11931215
await commands.executeCommand('codeQLDatabases.removeOrphanedDatabases');
11941216

11951217
void logger.log('Successfully finished extension initialization.');

extensions/ql-vscode/src/mocks/mock-gh-api-server.ts

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,44 @@
11
import { MockGitHubApiConfigListener } from '../config';
22

3+
import { setupServer, SetupServerApi } from 'msw/node';
4+
import { Recorder } from './recorder';
5+
import { commands, env, Uri, window } from 'vscode';
6+
import { DisposableObject } from '../pure/disposable-object';
7+
import { getMockGitHubApiServerScenariosPath } from '../config';
8+
39
/**
410
* Enables mocking of the GitHub API server via HTTP interception, using msw.
511
*/
6-
export class MockGitHubApiServer {
12+
export class MockGitHubApiServer extends DisposableObject {
713
private isListening: boolean;
814
private config: MockGitHubApiConfigListener;
915

16+
private readonly server: SetupServerApi;
17+
private readonly recorder: Recorder;
18+
1019
constructor() {
20+
super();
1121
this.isListening = false;
1222
this.config = new MockGitHubApiConfigListener();
23+
24+
this.server = setupServer();
25+
this.recorder = this.push(new Recorder(this.server));
26+
1327
this.setupConfigListener();
1428
}
1529

1630
public startServer(): void {
17-
this.isListening = true;
31+
if (this.isListening) {
32+
return;
33+
}
1834

19-
// TODO: Enable HTTP interception.
35+
this.server.listen();
36+
this.isListening = true;
2037
}
2138

2239
public stopServer(): void {
40+
this.server.close();
2341
this.isListening = false;
24-
25-
// TODO: Disable HTTP interception.
2642
}
2743

2844
public loadScenario(): void {
@@ -33,8 +49,98 @@ export class MockGitHubApiServer {
3349
// TODO: Implement logic to list all available scenarios.
3450
}
3551

36-
public recordScenario(): void {
37-
// TODO: Implement logic to record a new scenario to a directory.
52+
public async recordScenario(): Promise<void> {
53+
if (this.recorder.isRecording) {
54+
void window.showErrorMessage('A scenario is already being recorded. Use the "Save Scenario" or "Cancel Scenario" commands to finish recording.');
55+
return;
56+
}
57+
58+
this.recorder.start();
59+
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', true);
60+
61+
await window.showInformationMessage('Recording scenario. To save the scenario, use the "CodeQL Mock GitHub API Server: Save Scenario" command.');
62+
}
63+
64+
public async saveScenario(): Promise<void> {
65+
const scenariosDirectory = await this.getScenariosDirectory();
66+
if (!scenariosDirectory) {
67+
return;
68+
}
69+
70+
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false);
71+
72+
if (!this.recorder.isRecording) {
73+
void window.showErrorMessage('No scenario is currently being recorded.');
74+
return;
75+
}
76+
if (this.recorder.scenarioRequestCount === 0) {
77+
void window.showWarningMessage('No requests were recorded. Cancelling scenario.');
78+
79+
await this.stopRecording();
80+
81+
return;
82+
}
83+
84+
const name = await window.showInputBox({
85+
title: 'Save scenario',
86+
prompt: 'Enter a name for the scenario.',
87+
placeHolder: 'successful-run',
88+
});
89+
if (!name) {
90+
return;
91+
}
92+
93+
const filepath = await this.recorder.save(scenariosDirectory, name);
94+
95+
await this.stopRecording();
96+
97+
const action = await window.showInformationMessage(`Scenario saved to ${filepath}`, 'Open directory');
98+
if (action === 'Open directory') {
99+
await env.openExternal(Uri.file(filepath));
100+
}
101+
}
102+
103+
public async cancelRecording(): Promise<void> {
104+
if (!this.recorder.isRecording) {
105+
void window.showErrorMessage('No scenario is currently being recorded.');
106+
return;
107+
}
108+
109+
await this.stopRecording();
110+
111+
void window.showInformationMessage('Recording cancelled.');
112+
}
113+
114+
private async stopRecording(): Promise<void> {
115+
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false);
116+
117+
await this.recorder.stop();
118+
await this.recorder.clear();
119+
}
120+
121+
private async getScenariosDirectory(): Promise<string | undefined> {
122+
const scenariosDirectory = getMockGitHubApiServerScenariosPath();
123+
if (scenariosDirectory) {
124+
return scenariosDirectory;
125+
}
126+
127+
const directories = await window.showOpenDialog({
128+
canSelectFolders: true,
129+
canSelectFiles: false,
130+
canSelectMany: false,
131+
openLabel: 'Select scenarios directory',
132+
title: 'Select scenarios directory',
133+
});
134+
if (directories === undefined || directories.length === 0) {
135+
void window.showErrorMessage('No scenarios directory selected.');
136+
return undefined;
137+
}
138+
139+
// Unfortunately, we cannot save the directory in the configuration because that requires
140+
// the configuration to be registered. If we do that, it would be visible to all users; there
141+
// is no "when" clause that would allow us to only show it to users who have enabled the feature flag.
142+
143+
return directories[0].fsPath;
38144
}
39145

40146
private setupConfigListener(): void {

0 commit comments

Comments
 (0)