Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/test/unit/electron/index.js
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
//@ts-check
7
'use strict';
8
9
// mocha disables running through electron by default. Note that this must
10
// come before any mocha imports.
11
process.env.MOCHA_COLORS = '1';
12
13
const { app, BrowserWindow, ipcMain, crashReporter, session } = require('electron');
14
const product = require('../../../product.json');
15
const { tmpdir } = require('os');
16
const { existsSync, mkdirSync } = require('fs');
17
const path = require('path');
18
const mocha = require('mocha');
19
const events = require('events');
20
const MochaJUnitReporter = require('mocha-junit-reporter');
21
const url = require('url');
22
const net = require('net');
23
const createStatsCollector = require('mocha/lib/stats-collector');
24
const { applyReporter, importMochaReporter } = require('../reporter');
25
26
const minimist = require('minimist');
27
28
/**
29
* @type {{
30
* grep: string;
31
* run: string;
32
* runGlob: string;
33
* testSplit: string;
34
* dev: boolean;
35
* reporter: string;
36
* 'reporter-options': string;
37
* 'waitServer': string;
38
* timeout: string;
39
* 'crash-reporter-directory': string;
40
* tfs: string;
41
* build: boolean;
42
* coverage: boolean;
43
* coveragePath: string;
44
* coverageFormats: string | string[];
45
* 'per-test-coverage': boolean;
46
* help: boolean;
47
* }}
48
*/
49
const args = minimist(process.argv.slice(2), {
50
string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs', 'coveragePath', 'coverageFormats', 'testSplit'],
51
boolean: ['build', 'coverage', 'help', 'dev', 'per-test-coverage'],
52
alias: {
53
'grep': ['g', 'f'],
54
'runGlob': ['glob', 'runGrep'],
55
'dev': ['dev-tools', 'devTools'],
56
'help': 'h'
57
},
58
default: {
59
'reporter': 'spec',
60
'reporter-options': ''
61
}
62
});
63
64
if (args.help) {
65
console.log(`Usage: node ${process.argv[1]} [options]
66
67
Options:
68
--grep, -g, -f <pattern> only run tests matching <pattern>
69
--run <file> only run tests from <file>
70
--runGlob, --glob, --runGrep <file_pattern> only run tests matching <file_pattern>
71
--testSplit <i>/<n> split tests into <n> parts and run the <i>th part
72
--build run with build output (out-build)
73
--coverage generate coverage report
74
--per-test-coverage generate a per-test V8 coverage report, only valid with the full-json-stream reporter
75
--dev, --dev-tools, --devTools <window> open dev tools, keep window open, reuse app data
76
--reporter <reporter> the mocha reporter (default: "spec")
77
--reporter-options <options> the mocha reporter options (default: "")
78
--waitServer <port> port to connect to and wait before running tests
79
--timeout <ms> timeout for tests
80
--crash-reporter-directory <path> crash reporter directory
81
--tfs <url> TFS server URL
82
--help, -h show the help`);
83
process.exit(0);
84
}
85
86
let crashReporterDirectory = args['crash-reporter-directory'];
87
if (crashReporterDirectory) {
88
crashReporterDirectory = path.normalize(crashReporterDirectory);
89
90
if (!path.isAbsolute(crashReporterDirectory)) {
91
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`);
92
app.exit(1);
93
}
94
95
if (!existsSync(crashReporterDirectory)) {
96
try {
97
mkdirSync(crashReporterDirectory);
98
} catch (error) {
99
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`);
100
app.exit(1);
101
}
102
}
103
104
// Crashes are stored in the crashDumps directory by default, so we
105
// need to change that directory to the provided one
106
console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`);
107
app.setPath('crashDumps', crashReporterDirectory);
108
109
crashReporter.start({
110
companyName: 'Microsoft',
111
productName: process.env['VSCODE_DEV'] ? `${product.nameShort} Dev` : product.nameShort,
112
uploadToServer: false,
113
compress: true
114
});
115
}
116
117
if (!args.dev) {
118
app.setPath('userData', path.join(tmpdir(), `vscode-tests-${Date.now()}`));
119
}
120
121
function deserializeSuite(suite) {
122
return {
123
root: suite.root,
124
suites: suite.suites,
125
tests: suite.tests,
126
title: suite.title,
127
titlePath: () => suite.titlePath,
128
fullTitle: () => suite.fullTitle,
129
timeout: () => suite.timeout,
130
retries: () => suite.retries,
131
slow: () => suite.slow,
132
bail: () => suite.bail
133
};
134
}
135
136
function deserializeRunnable(runnable) {
137
return {
138
title: runnable.title,
139
titlePath: () => runnable.titlePath,
140
fullTitle: () => runnable.fullTitle,
141
async: runnable.async,
142
slow: () => runnable.slow,
143
speed: runnable.speed,
144
duration: runnable.duration,
145
currentRetry: () => runnable.currentRetry
146
};
147
}
148
149
function deserializeError(err) {
150
const inspect = err.inspect;
151
err.inspect = () => inspect;
152
// Unfortunately, mocha rewrites and formats err.actual/err.expected.
153
// This formatting is hard to reverse, so err.*JSON includes the unformatted value.
154
if (err.actual) {
155
err.actual = JSON.parse(err.actual).value;
156
err.actualJSON = err.actual;
157
}
158
if (err.expected) {
159
err.expected = JSON.parse(err.expected).value;
160
err.expectedJSON = err.expected;
161
}
162
return err;
163
}
164
165
class IPCRunner extends events.EventEmitter {
166
167
constructor(win) {
168
super();
169
170
this.didFail = false;
171
this.didEnd = false;
172
173
ipcMain.on('start', () => this.emit('start'));
174
ipcMain.on('end', () => {
175
this.didEnd = true;
176
this.emit('end');
177
});
178
ipcMain.on('suite', (e, suite) => this.emit('suite', deserializeSuite(suite)));
179
ipcMain.on('suite end', (e, suite) => this.emit('suite end', deserializeSuite(suite)));
180
ipcMain.on('test', (e, test) => this.emit('test', deserializeRunnable(test)));
181
ipcMain.on('test end', (e, test) => this.emit('test end', deserializeRunnable(test)));
182
ipcMain.on('hook', (e, hook) => this.emit('hook', deserializeRunnable(hook)));
183
ipcMain.on('hook end', (e, hook) => this.emit('hook end', deserializeRunnable(hook)));
184
ipcMain.on('pass', (e, test) => this.emit('pass', deserializeRunnable(test)));
185
ipcMain.on('fail', (e, test, err) => {
186
this.didFail = true;
187
this.emit('fail', deserializeRunnable(test), deserializeError(err));
188
});
189
ipcMain.on('pending', (e, test) => this.emit('pending', deserializeRunnable(test)));
190
191
ipcMain.handle('startCoverage', async () => {
192
win.webContents.debugger.attach();
193
await win.webContents.debugger.sendCommand('Debugger.enable');
194
await win.webContents.debugger.sendCommand('Profiler.enable');
195
await win.webContents.debugger.sendCommand('Profiler.startPreciseCoverage', {
196
detailed: true,
197
allowTriggeredUpdates: false,
198
});
199
});
200
201
const coverageScriptsReported = new Set();
202
ipcMain.handle('snapshotCoverage', async (_, test) => {
203
const coverage = await win.webContents.debugger.sendCommand('Profiler.takePreciseCoverage');
204
await Promise.all(coverage.result.map(async (r) => {
205
if (!coverageScriptsReported.has(r.scriptId)) {
206
coverageScriptsReported.add(r.scriptId);
207
const src = await win.webContents.debugger.sendCommand('Debugger.getScriptSource', { scriptId: r.scriptId });
208
r.source = src.scriptSource;
209
}
210
}));
211
212
if (!test) {
213
this.emit('coverage init', coverage);
214
} else {
215
this.emit('coverage increment', test, coverage);
216
}
217
});
218
}
219
}
220
221
app.on('ready', () => {
222
223
// needed when loading resources from the renderer, e.g xterm.js or the encoding lib
224
session.defaultSession.protocol.registerFileProtocol('vscode-file', (request, callback) => {
225
const path = new URL(request.url).pathname;
226
callback({ path });
227
});
228
229
ipcMain.on('error', (_, err) => {
230
if (!args.dev) {
231
console.error(err);
232
app.exit(1);
233
}
234
});
235
236
// We need to provide a basic `ISandboxConfiguration`
237
// for our preload script to function properly because
238
// some of our types depend on it (e.g. product.ts).
239
ipcMain.handle('vscode:test-vscode-window-config', async () => {
240
return {
241
product: {
242
version: '1.x.y',
243
nameShort: 'Code - OSS Dev',
244
nameLong: 'Code - OSS Dev',
245
applicationName: 'code-oss',
246
dataFolderName: '.vscode-oss',
247
urlProtocol: 'code-oss',
248
}
249
};
250
});
251
252
// No-op since invoke the IPC as part of IIFE in the preload.
253
ipcMain.handle('vscode:fetchShellEnv', event => { });
254
255
const win = new BrowserWindow({
256
height: 600,
257
width: 800,
258
show: false,
259
webPreferences: {
260
preload: path.join(__dirname, 'preload.js'), // ensure similar environment as VSCode as tests may depend on this
261
additionalArguments: [`--vscode-window-config=vscode:test-vscode-window-config`],
262
nodeIntegration: true,
263
contextIsolation: false,
264
enableWebSQL: false,
265
spellcheck: false
266
}
267
});
268
269
win.webContents.on('did-finish-load', () => {
270
if (args.dev) {
271
win.show();
272
win.webContents.openDevTools();
273
}
274
275
if (args.waitServer) {
276
waitForServer(Number(args.waitServer)).then(sendRun);
277
} else {
278
sendRun();
279
}
280
});
281
282
async function waitForServer(port) {
283
let timeout;
284
let socket;
285
286
return new Promise(resolve => {
287
socket = net.connect(port, '127.0.0.1');
288
socket.on('error', e => {
289
console.error('error connecting to waitServer', e);
290
resolve(undefined);
291
});
292
293
socket.on('close', () => {
294
resolve(undefined);
295
});
296
297
timeout = setTimeout(() => {
298
console.error('timed out waiting for before starting tests debugger');
299
resolve(undefined);
300
}, 15000);
301
}).finally(() => {
302
if (socket) {
303
socket.end();
304
}
305
clearTimeout(timeout);
306
});
307
}
308
309
function sendRun() {
310
win.webContents.send('run', args);
311
}
312
313
const target = url.pathToFileURL(path.join(__dirname, 'renderer.html'));
314
target.searchParams.set('argv', JSON.stringify(args));
315
win.loadURL(target.href);
316
317
const runner = new IPCRunner(win);
318
createStatsCollector(runner);
319
320
// Handle renderer crashes, #117068
321
win.webContents.on('render-process-gone', (evt, details) => {
322
if (!runner.didEnd) {
323
console.error(`Renderer process crashed with: ${JSON.stringify(details)}`);
324
app.exit(1);
325
}
326
});
327
328
const reporters = [];
329
330
if (args.tfs) {
331
const testResultsRoot = process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE;
332
reporters.push(
333
new mocha.reporters.Spec(runner),
334
new MochaJUnitReporter(runner, {
335
reporterOptions: {
336
testsuitesTitle: `${args.tfs} ${process.platform}`,
337
mochaFile: testResultsRoot ? path.join(testResultsRoot, `test-results/${process.platform}-${process.arch}-${args.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined
338
}
339
}),
340
);
341
} else {
342
// mocha patches symbols to use windows escape codes, but it seems like
343
// Electron mangles these in its output.
344
if (process.platform === 'win32') {
345
Object.assign(importMochaReporter('base').symbols, {
346
ok: '+',
347
err: 'X',
348
dot: '.',
349
});
350
}
351
352
reporters.push(applyReporter(runner, args));
353
}
354
355
if (!args.dev) {
356
ipcMain.on('all done', async () => {
357
await Promise.all(reporters.map(r => r.drain?.()));
358
app.exit(runner.didFail ? 1 : 0);
359
});
360
}
361
});
362
363