Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/test/unit/electron/renderer.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
/*eslint-env mocha*/
7
8
// @ts-check
9
10
const fs = require('fs');
11
12
(function () {
13
const originals = {};
14
let logging = false;
15
let withStacks = false;
16
17
globalThis.beginLoggingFS = (_withStacks) => {
18
logging = true;
19
withStacks = _withStacks || false;
20
};
21
globalThis.endLoggingFS = () => {
22
logging = false;
23
withStacks = false;
24
};
25
26
function createSpy(element, cnt) {
27
return function (...args) {
28
if (logging) {
29
console.log(`calling ${element}: ` + args.slice(0, cnt).join(',') + (withStacks ? (`\n` + new Error().stack?.split('\n').slice(2).join('\n')) : ''));
30
}
31
return originals[element].call(this, ...args);
32
};
33
}
34
35
function intercept(element, cnt) {
36
originals[element] = fs[element];
37
fs[element] = createSpy(element, cnt);
38
}
39
40
[
41
['realpathSync', 1],
42
['readFileSync', 1],
43
['openSync', 3],
44
['readSync', 1],
45
['closeSync', 1],
46
['readFile', 2],
47
['mkdir', 1],
48
['lstat', 1],
49
['stat', 1],
50
['watch', 1],
51
['readdir', 1],
52
['access', 2],
53
['open', 2],
54
['write', 1],
55
['fdatasync', 1],
56
['close', 1],
57
['read', 1],
58
['unlink', 1],
59
['rmdir', 1],
60
].forEach((element) => {
61
intercept(element[0], element[1]);
62
});
63
})();
64
65
const { ipcRenderer } = require('electron');
66
const assert = require('assert');
67
const path = require('path');
68
const glob = require('glob');
69
const util = require('util');
70
const coverage = require('../coverage');
71
const { pathToFileURL } = require('url');
72
73
// Disabled custom inspect. See #38847
74
if (util.inspect && util.inspect['defaultOptions']) {
75
util.inspect['defaultOptions'].customInspect = false;
76
}
77
78
// VSCODE_GLOBALS: package/product.json
79
globalThis._VSCODE_PRODUCT_JSON = require('../../../product.json');
80
globalThis._VSCODE_PACKAGE_JSON = require('../../../package.json');
81
82
// Test file operations that are common across platforms. Used for test infra, namely snapshot tests
83
Object.assign(globalThis, {
84
__readFileInTests: path => fs.promises.readFile(path, 'utf-8'),
85
__writeFileInTests: (path, contents) => fs.promises.writeFile(path, contents),
86
__readDirInTests: path => fs.promises.readdir(path),
87
__unlinkInTests: path => fs.promises.unlink(path),
88
__mkdirPInTests: path => fs.promises.mkdir(path, { recursive: true }),
89
});
90
91
const IS_CI = !!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || !!process.env.GITHUB_WORKSPACE;
92
const _tests_glob = '**/test/**/*.test.js';
93
94
95
/**
96
* Loads one or N modules.
97
* @type {{
98
* (module: string|string[]): Promise<any>|Promise<any[]>;
99
* _out: string;
100
* }}
101
*/
102
let loadFn;
103
104
const _loaderErrors = [];
105
106
function initNls(opts) {
107
if (opts.build) {
108
// when running from `out-build`, ensure to load the default
109
// messages file, because all `nls.localize` calls have their
110
// english values removed and replaced by an index.
111
globalThis._VSCODE_NLS_MESSAGES = require(`../../../out-build/nls.messages.json`);
112
}
113
}
114
115
function initLoadFn(opts) {
116
const outdir = opts.build ? 'out-build' : 'out';
117
const out = path.join(__dirname, `../../../${outdir}`);
118
119
const baseUrl = pathToFileURL(path.join(__dirname, `../../../${outdir}/`));
120
globalThis._VSCODE_FILE_ROOT = baseUrl.href;
121
122
// set loader
123
function importModules(modules) {
124
const moduleArray = Array.isArray(modules) ? modules : [modules];
125
const tasks = moduleArray.map(mod => {
126
const url = new URL(`./${mod}.js`, baseUrl).href;
127
return import(url).catch(err => {
128
console.log(mod, url);
129
console.log(err);
130
_loaderErrors.push(err);
131
throw err;
132
});
133
});
134
135
return Array.isArray(modules)
136
? Promise.all(tasks)
137
: tasks[0];
138
}
139
importModules._out = out;
140
loadFn = importModules;
141
}
142
143
async function createCoverageReport(opts) {
144
if (!opts.coverage) {
145
return undefined;
146
}
147
return coverage.createReport(opts.run || opts.runGlob);
148
}
149
150
async function loadModules(modules) {
151
for (const file of modules) {
152
mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, mocha);
153
const m = await loadFn(file);
154
mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_REQUIRE, m, file, mocha);
155
mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, mocha);
156
}
157
}
158
159
const globAsync = util.promisify(glob);
160
161
async function loadTestModules(opts) {
162
163
if (opts.run) {
164
const files = Array.isArray(opts.run) ? opts.run : [opts.run];
165
const modules = files.map(file => {
166
file = file.replace(/^src[\\/]/, '');
167
return file.replace(/\.[jt]s$/, '');
168
});
169
return loadModules(modules);
170
}
171
172
const pattern = opts.runGlob || _tests_glob;
173
const files = await globAsync(pattern, { cwd: loadFn._out });
174
let modules = files.map(file => file.replace(/\.js$/, ''));
175
if (opts.testSplit) {
176
const [i, n] = opts.testSplit.split('/').map(Number);
177
const chunkSize = Math.floor(modules.length / n);
178
const start = (i - 1) * chunkSize;
179
const end = i === n ? modules.length : i * chunkSize;
180
modules = modules.slice(start, end);
181
}
182
return loadModules(modules);
183
}
184
185
/** @type Mocha.Test */
186
let currentTest;
187
188
async function loadTests(opts) {
189
190
//#region Unexpected Output
191
192
const _allowedTestOutput = [
193
/The vm module of Node\.js is deprecated in the renderer process and will be removed./,
194
];
195
196
// allow snapshot mutation messages locally
197
if (!IS_CI) {
198
_allowedTestOutput.push(/Creating new snapshot in/);
199
_allowedTestOutput.push(/Deleting [0-9]+ old snapshots/);
200
}
201
202
const perTestCoverage = opts['per-test-coverage'] ? await PerTestCoverage.init() : undefined;
203
204
const _allowedTestsWithOutput = new Set([
205
'creates a snapshot', // self-testing
206
'validates a snapshot', // self-testing
207
'cleans up old snapshots', // self-testing
208
'issue #149412: VS Code hangs when bad semantic token data is received', // https://github.com/microsoft/vscode/issues/192440
209
'issue #134973: invalid semantic tokens should be handled better', // https://github.com/microsoft/vscode/issues/192440
210
'issue #148651: VSCode UI process can hang if a semantic token with negative values is returned by language service', // https://github.com/microsoft/vscode/issues/192440
211
'issue #149130: vscode freezes because of Bracket Pair Colorization', // https://github.com/microsoft/vscode/issues/192440
212
'property limits', // https://github.com/microsoft/vscode/issues/192443
213
'Error events', // https://github.com/microsoft/vscode/issues/192443
214
'fetch returns keybinding with user first if title and id matches', //
215
'throw ListenerLeakError'
216
]);
217
218
const _allowedSuitesWithOutput = new Set([
219
'InlineChatController'
220
]);
221
222
let _testsWithUnexpectedOutput = false;
223
224
for (const consoleFn of [console.log, console.error, console.info, console.warn, console.trace, console.debug]) {
225
console[consoleFn.name] = function (msg) {
226
if (!currentTest) {
227
consoleFn.apply(console, arguments);
228
} else if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title) && !_allowedSuitesWithOutput.has(currentTest.parent?.title ?? '')) {
229
_testsWithUnexpectedOutput = true;
230
consoleFn.apply(console, arguments);
231
}
232
};
233
}
234
235
//#endregion
236
237
//#region Unexpected / Loader Errors
238
239
const _unexpectedErrors = [];
240
241
const _allowedTestsWithUnhandledRejections = new Set([
242
// Lifecycle tests
243
'onWillShutdown - join with error is handled',
244
'onBeforeShutdown - veto with error is treated as veto',
245
'onBeforeShutdown - final veto with error is treated as veto',
246
// Search tests
247
'Search Model: Search reports timed telemetry on search when error is called'
248
]);
249
250
const errors = await loadFn('vs/base/common/errors');
251
const onUnexpectedError = function (err) {
252
if (err.name === 'Canceled') {
253
return; // ignore canceled errors that are common
254
}
255
256
let stack = (err ? err.stack : null);
257
if (!stack) {
258
stack = new Error().stack;
259
}
260
261
_unexpectedErrors.push((err && err.message ? err.message : err) + '\n' + stack);
262
};
263
264
process.on('uncaughtException', error => onUnexpectedError(error));
265
process.on('unhandledRejection', (reason, promise) => {
266
onUnexpectedError(reason);
267
promise.catch(() => { });
268
});
269
window.addEventListener('unhandledrejection', event => {
270
event.preventDefault(); // Do not log to test output, we show an error later when test ends
271
event.stopPropagation();
272
273
if (!_allowedTestsWithUnhandledRejections.has(currentTest.title)) {
274
onUnexpectedError(event.reason);
275
}
276
});
277
278
errors.setUnexpectedErrorHandler(onUnexpectedError);
279
//#endregion
280
281
const { assertCleanState } = await loadFn('vs/workbench/test/common/utils');
282
283
suite('Tests are using suiteSetup and setup correctly', () => {
284
test('assertCleanState - check that registries are clean at the start of test running', () => {
285
assertCleanState();
286
});
287
});
288
289
setup(async () => {
290
await perTestCoverage?.startTest();
291
});
292
293
teardown(async () => {
294
await perTestCoverage?.finishTest(currentTest.file, currentTest.fullTitle());
295
296
// should not have unexpected output
297
if (_testsWithUnexpectedOutput && !opts.dev) {
298
assert.ok(false, 'Error: Unexpected console output in test run. Please ensure no console.[log|error|info|warn] usage in tests or runtime errors.');
299
}
300
301
// should not have unexpected errors
302
const errors = _unexpectedErrors.concat(_loaderErrors);
303
if (errors.length) {
304
const msg = [];
305
for (const error of errors) {
306
console.error(`Error: Test run should not have unexpected errors:\n${error}`);
307
msg.push(String(error))
308
}
309
assert.ok(false, `Error: Test run should not have unexpected errors:\n${msg.join('\n')}`);
310
}
311
});
312
313
suiteTeardown(() => { // intentionally not in teardown because some tests only cleanup in suiteTeardown
314
315
// should have cleaned up in registries
316
assertCleanState();
317
});
318
319
return loadTestModules(opts);
320
}
321
322
function serializeSuite(suite) {
323
return {
324
root: suite.root,
325
suites: suite.suites.map(serializeSuite),
326
tests: suite.tests.map(serializeRunnable),
327
title: suite.title,
328
fullTitle: suite.fullTitle(),
329
titlePath: suite.titlePath(),
330
timeout: suite.timeout(),
331
retries: suite.retries(),
332
slow: suite.slow(),
333
bail: suite.bail()
334
};
335
}
336
337
function serializeRunnable(runnable) {
338
return {
339
title: runnable.title,
340
fullTitle: runnable.fullTitle(),
341
titlePath: runnable.titlePath(),
342
async: runnable.async,
343
slow: runnable.slow(),
344
speed: runnable.speed,
345
duration: runnable.duration
346
};
347
}
348
349
function serializeError(err) {
350
return {
351
message: err.message,
352
stack: err.stack,
353
snapshotPath: err.snapshotPath,
354
actual: safeStringify({ value: err.actual }),
355
expected: safeStringify({ value: err.expected }),
356
uncaught: err.uncaught,
357
showDiff: err.showDiff,
358
inspect: typeof err.inspect === 'function' ? err.inspect() : ''
359
};
360
}
361
362
function safeStringify(obj) {
363
const seen = new Set();
364
return JSON.stringify(obj, (key, value) => {
365
if (value === undefined) {
366
return '[undefined]';
367
}
368
369
if (isObject(value) || Array.isArray(value)) {
370
if (seen.has(value)) {
371
return '[Circular]';
372
} else {
373
seen.add(value);
374
}
375
}
376
return value;
377
});
378
}
379
380
function isObject(obj) {
381
// The method can't do a type cast since there are type (like strings) which
382
// are subclasses of any put not positively matched by the function. Hence type
383
// narrowing results in wrong results.
384
return typeof obj === 'object'
385
&& obj !== null
386
&& !Array.isArray(obj)
387
&& !(obj instanceof RegExp)
388
&& !(obj instanceof Date);
389
}
390
391
class IPCReporter {
392
393
constructor(runner) {
394
runner.on('start', () => ipcRenderer.send('start'));
395
runner.on('end', () => ipcRenderer.send('end'));
396
runner.on('suite', suite => ipcRenderer.send('suite', serializeSuite(suite)));
397
runner.on('suite end', suite => ipcRenderer.send('suite end', serializeSuite(suite)));
398
runner.on('test', test => ipcRenderer.send('test', serializeRunnable(test)));
399
runner.on('test end', test => ipcRenderer.send('test end', serializeRunnable(test)));
400
runner.on('hook', hook => ipcRenderer.send('hook', serializeRunnable(hook)));
401
runner.on('hook end', hook => ipcRenderer.send('hook end', serializeRunnable(hook)));
402
runner.on('pass', test => ipcRenderer.send('pass', serializeRunnable(test)));
403
runner.on('fail', (test, err) => ipcRenderer.send('fail', serializeRunnable(test), serializeError(err)));
404
runner.on('pending', test => ipcRenderer.send('pending', serializeRunnable(test)));
405
}
406
}
407
408
const $globalThis = globalThis;
409
const setTimeout0IsFaster = (typeof $globalThis.postMessage === 'function' && !$globalThis.importScripts);
410
411
/**
412
* See https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#:~:text=than%204%2C%20then-,set%20timeout%20to%204,-.
413
*
414
* Works similarly to `setTimeout(0)` but doesn't suffer from the 4ms artificial delay
415
* that browsers set when the nesting level is > 5.
416
*/
417
const setTimeout0 = (() => {
418
if (setTimeout0IsFaster) {
419
const pending = [];
420
421
$globalThis.addEventListener('message', (e) => {
422
if (e.data && e.data.vscodeScheduleAsyncWork) {
423
for (let i = 0, len = pending.length; i < len; i++) {
424
const candidate = pending[i];
425
if (candidate.id === e.data.vscodeScheduleAsyncWork) {
426
pending.splice(i, 1);
427
candidate.callback();
428
return;
429
}
430
}
431
}
432
});
433
let lastId = 0;
434
return (callback) => {
435
const myId = ++lastId;
436
pending.push({
437
id: myId,
438
callback: callback
439
});
440
$globalThis.postMessage({ vscodeScheduleAsyncWork: myId }, '*');
441
};
442
}
443
return (callback) => setTimeout(callback);
444
})();
445
446
async function runTests(opts) {
447
// @ts-expect-error
448
Mocha.Runner.immediately = setTimeout0;
449
450
mocha.setup({
451
ui: 'tdd',
452
// @ts-expect-error
453
reporter: opts.dev ? 'html' : IPCReporter,
454
grep: opts.grep,
455
timeout: opts.timeout ?? (IS_CI ? 30000 : 5000),
456
forbidOnly: IS_CI // disallow .only() when running on build machine
457
});
458
459
// this *must* come before loadTests, or it doesn't work.
460
if (opts.timeout !== undefined) {
461
mocha.timeout(opts.timeout);
462
}
463
464
await loadTests(opts);
465
466
const runner = mocha.run(async () => {
467
await createCoverageReport(opts)
468
ipcRenderer.send('all done');
469
});
470
471
runner.on('test', test => currentTest = test);
472
473
if (opts.dev) {
474
runner.on('fail', (test, err) => {
475
console.error(test.fullTitle());
476
console.error(err.stack);
477
});
478
}
479
}
480
481
ipcRenderer.on('run', async (_e, opts) => {
482
initNls(opts);
483
initLoadFn(opts);
484
485
await Promise.resolve(globalThis._VSCODE_TEST_INIT);
486
487
try {
488
await runTests(opts);
489
} catch (err) {
490
if (typeof err !== 'string') {
491
err = JSON.stringify(err);
492
}
493
console.error(err);
494
ipcRenderer.send('error', err);
495
}
496
});
497
498
class PerTestCoverage {
499
static async init() {
500
await ipcRenderer.invoke('startCoverage');
501
return new PerTestCoverage();
502
}
503
504
async startTest() {
505
if (!this.didInit) {
506
this.didInit = true;
507
await ipcRenderer.invoke('snapshotCoverage');
508
}
509
}
510
511
async finishTest(file, fullTitle) {
512
await ipcRenderer.invoke('snapshotCoverage', { file, fullTitle });
513
}
514
}
515
516