Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/test/mcp/src/application.ts
5241 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
6
import { getDevElectronPath, Quality, ConsoleLogger, FileLogger, Logger, MultiLogger, getBuildElectronPath, getBuildVersion, measureAndLog, Application } from '../../automation';
7
import * as path from 'path';
8
import * as fs from 'fs';
9
import * as os from 'os';
10
import * as vscodetest from '@vscode/test-electron';
11
import { createApp, retry, parseVersion } from './utils';
12
import { opts } from './options';
13
14
const rootPath = path.join(__dirname, '..', '..', '..');
15
const logsRootPath = path.join(rootPath, '.build', 'vscode-playwright-mcp', 'logs');
16
const crashesRootPath = path.join(rootPath, '.build', 'vscode-playwright-mcp', 'crashes');
17
const videoRootPath = path.join(rootPath, '.build', 'vscode-playwright-mcp', 'videos');
18
19
const logger = createLogger();
20
21
function createLogger(): Logger {
22
const loggers: Logger[] = [];
23
24
// Log to console if verbose
25
if (opts.verbose) {
26
loggers.push(new ConsoleLogger());
27
}
28
29
// Prepare logs rot path
30
fs.rmSync(logsRootPath, { recursive: true, force: true, maxRetries: 3 });
31
fs.mkdirSync(logsRootPath, { recursive: true });
32
33
// Always log to log file
34
loggers.push(new FileLogger(path.join(logsRootPath, 'smoke-test-runner.log')));
35
36
return new MultiLogger(loggers);
37
}
38
39
const testDataPath = path.join(os.tmpdir(), 'vscsmoke');
40
if (fs.existsSync(testDataPath)) {
41
fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10 });
42
}
43
fs.mkdirSync(testDataPath, { recursive: true });
44
process.once('exit', () => {
45
try {
46
fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10 });
47
} catch {
48
// noop
49
}
50
});
51
52
function fail(errorMessage): void {
53
logger.log(errorMessage);
54
if (!opts.verbose) {
55
console.error(errorMessage);
56
}
57
process.exit(1);
58
}
59
60
let quality: Quality;
61
let version: string | undefined;
62
63
function parseQuality(): Quality {
64
if (process.env.VSCODE_DEV === '1') {
65
return Quality.Dev;
66
}
67
68
const quality = process.env.VSCODE_QUALITY ?? '';
69
70
switch (quality) {
71
case 'stable':
72
return Quality.Stable;
73
case 'insider':
74
return Quality.Insiders;
75
case 'exploration':
76
return Quality.Exploration;
77
case 'oss':
78
return Quality.OSS;
79
default:
80
return Quality.Dev;
81
}
82
}
83
84
//
85
// #### Electron ####
86
//
87
if (!opts.web) {
88
let testCodePath = opts.build;
89
let electronPath: string | undefined;
90
91
if (testCodePath) {
92
electronPath = getBuildElectronPath(testCodePath);
93
version = getBuildVersion(testCodePath);
94
} else {
95
testCodePath = getDevElectronPath();
96
electronPath = testCodePath;
97
process.env.VSCODE_REPOSITORY = rootPath;
98
process.env.VSCODE_DEV = '1';
99
process.env.VSCODE_CLI = '1';
100
}
101
102
if (!fs.existsSync(electronPath || '')) {
103
fail(`Cannot find VSCode at ${electronPath}. Please run VSCode once first (scripts/code.sh, scripts\\code.bat) and try again.`);
104
}
105
106
quality = parseQuality();
107
108
if (opts.remote) {
109
logger.log(`Running desktop remote smoke tests against ${electronPath}`);
110
} else {
111
logger.log(`Running desktop smoke tests against ${electronPath}`);
112
}
113
}
114
115
//
116
// #### Web Smoke Tests ####
117
//
118
else {
119
const testCodeServerPath = opts.build || process.env.VSCODE_REMOTE_SERVER_PATH;
120
121
if (typeof testCodeServerPath === 'string') {
122
if (!fs.existsSync(testCodeServerPath)) {
123
fail(`Cannot find Code server at ${testCodeServerPath}.`);
124
} else {
125
logger.log(`Running web smoke tests against ${testCodeServerPath}`);
126
}
127
}
128
129
if (!testCodeServerPath) {
130
process.env.VSCODE_REPOSITORY = rootPath;
131
process.env.VSCODE_DEV = '1';
132
process.env.VSCODE_CLI = '1';
133
134
logger.log(`Running web smoke out of sources`);
135
}
136
137
quality = parseQuality();
138
}
139
140
logger.log(`VS Code product quality: ${quality}.`);
141
142
async function ensureStableCode(): Promise<void> {
143
let stableCodePath = opts['stable-build'];
144
if (!stableCodePath) {
145
const current = parseVersion(version!);
146
const versionsReq = await retry(() => measureAndLog(() => fetch('https://update.code.visualstudio.com/api/releases/stable'), 'versionReq', logger), 1000, 20);
147
148
if (!versionsReq.ok) {
149
throw new Error('Could not fetch releases from update server');
150
}
151
152
const versions: string[] = await measureAndLog(() => versionsReq.json(), 'versionReq.json()', logger);
153
const stableVersion = versions.find(raw => {
154
const version = parseVersion(raw);
155
return version.major < current.major || (version.major === current.major && version.minor < current.minor);
156
});
157
158
if (!stableVersion) {
159
throw new Error(`Could not find suitable stable version for ${version}`);
160
}
161
162
logger.log(`Found VS Code v${version}, downloading previous VS Code version ${stableVersion}...`);
163
164
let lastProgressMessage: string | undefined = undefined;
165
let lastProgressReportedAt = 0;
166
const stableCodeDestination = path.join(testDataPath, 's');
167
const stableCodeExecutable = await retry(() => measureAndLog(() => vscodetest.download({
168
cachePath: stableCodeDestination,
169
version: stableVersion,
170
extractSync: true,
171
reporter: {
172
report: report => {
173
let progressMessage = `download stable code progress: ${report.stage}`;
174
const now = Date.now();
175
if (progressMessage !== lastProgressMessage || now - lastProgressReportedAt > 10000) {
176
lastProgressMessage = progressMessage;
177
lastProgressReportedAt = now;
178
179
if (report.stage === 'downloading') {
180
progressMessage += ` (${report.bytesSoFar}/${report.totalBytes})`;
181
}
182
183
logger.log(progressMessage);
184
}
185
},
186
error: error => logger.log(`download stable code error: ${error}`)
187
}
188
}), 'download stable code', logger), 1000, 3, () => new Promise<void>((resolve, reject) => {
189
fs.rm(stableCodeDestination, { recursive: true, force: true, maxRetries: 10 }, error => {
190
if (error) {
191
reject(error);
192
} else {
193
resolve();
194
}
195
});
196
}));
197
198
if (process.platform === 'darwin') {
199
// Visual Studio Code.app/Contents/MacOS/Code
200
stableCodePath = path.dirname(path.dirname(path.dirname(stableCodeExecutable)));
201
} else {
202
// VSCode/Code.exe (Windows) | VSCode/code (Linux)
203
stableCodePath = path.dirname(stableCodeExecutable);
204
}
205
206
opts['stable-version'] = parseVersion(stableVersion);
207
}
208
209
if (!fs.existsSync(stableCodePath)) {
210
throw new Error(`Cannot find Stable VSCode at ${stableCodePath}.`);
211
}
212
213
logger.log(`Using stable build ${stableCodePath} for migration tests`);
214
215
opts['stable-build'] = stableCodePath;
216
}
217
218
async function setup(): Promise<void> {
219
logger.log('Preparing smoketest setup...');
220
221
if (!opts.web && !opts.remote && opts.build) {
222
// only enabled when running with --build and not in web or remote
223
await measureAndLog(() => ensureStableCode(), 'ensureStableCode', logger);
224
}
225
226
logger.log('Smoketest setup done!\n');
227
}
228
229
export async function getApplication({ recordVideo, workspacePath }: { recordVideo?: boolean; workspacePath?: string } = {}) {
230
const testCodePath = getDevElectronPath();
231
const electronPath = testCodePath;
232
if (!fs.existsSync(electronPath || '')) {
233
throw new Error(`Cannot find VSCode at ${electronPath}. Please run VSCode once first (scripts/code.sh, scripts\\code.bat) and try again.`);
234
}
235
process.env.VSCODE_REPOSITORY = rootPath;
236
process.env.VSCODE_DEV = '1';
237
process.env.VSCODE_CLI = '1';
238
delete process.env.ELECTRON_RUN_AS_NODE; // Ensure we run as Node.js
239
240
await setup();
241
const application = createApp({
242
quality,
243
version: parseVersion(version ?? '0.0.0'),
244
codePath: opts.build,
245
// Use provided workspace path, or fall back to rootPath on CI (GitHub Actions)
246
workspacePath: workspacePath ?? (process.env.GITHUB_ACTIONS ? rootPath : undefined),
247
logger,
248
logsPath: logsRootPath,
249
crashesPath: crashesRootPath,
250
videosPath: (recordVideo || opts.video) ? videoRootPath : undefined,
251
verbose: opts.verbose,
252
remote: opts.remote,
253
web: opts.web,
254
tracing: true,
255
headless: opts.headless,
256
browser: opts.browser,
257
extraArgs: (opts.electronArgs || '').split(' ').map(arg => arg.trim()).filter(arg => !!arg),
258
extensionDevelopmentPath: opts.extensionDevelopmentPath,
259
});
260
await application.start();
261
application.code.driver.currentPage.on('close', async () => {
262
fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10 });
263
});
264
return application;
265
}
266
267
export class ApplicationService {
268
private _application: Application | undefined;
269
private _closing: Promise<void> | undefined;
270
private _listeners: ((app: Application | undefined) => Promise<void> | void)[] = [];
271
272
onApplicationChange(listener: (app: Application | undefined) => Promise<void> | void): void {
273
this._listeners.push(listener);
274
}
275
276
removeApplicationChangeListener(listener: (app: Application | undefined) => void): void {
277
const index = this._listeners.indexOf(listener);
278
if (index >= 0) {
279
this._listeners.splice(index, 1);
280
}
281
}
282
283
get application(): Application | undefined {
284
return this._application;
285
}
286
287
async getOrCreateApplication({ recordVideo, workspacePath }: { recordVideo?: boolean; workspacePath?: string } = {}): Promise<Application> {
288
if (this._closing) {
289
await this._closing;
290
}
291
if (!this._application) {
292
this._application = await getApplication({ recordVideo, workspacePath });
293
this._application.code.driver.currentPage.on('close', () => {
294
this._closing = (async () => {
295
if (this._application) {
296
this._application.code.driver.browserContext.removeAllListeners();
297
await this._application.stop();
298
this._application = undefined;
299
await this._runAllListeners();
300
}
301
})();
302
});
303
await this._runAllListeners();
304
}
305
return this._application;
306
}
307
308
private async _runAllListeners() {
309
for (const listener of this._listeners) {
310
try {
311
await listener(this._application);
312
} catch (error) {
313
console.error('Error occurred in application change listener:', error);
314
}
315
}
316
}
317
}
318
319