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