/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /*eslint-env mocha*/ // @ts-check const fs = require('fs'); (function () { const originals = {}; let logging = false; let withStacks = false; globalThis.beginLoggingFS = (_withStacks) => { logging = true; withStacks = _withStacks || false; }; globalThis.endLoggingFS = () => { logging = false; withStacks = false; }; function createSpy(element, cnt) { return function (...args) { if (logging) { console.log(`calling ${element}: ` + args.slice(0, cnt).join(',') + (withStacks ? (`\n` + new Error().stack?.split('\n').slice(2).join('\n')) : '')); } return originals[element].call(this, ...args); }; } function intercept(element, cnt) { originals[element] = fs[element]; fs[element] = createSpy(element, cnt); } [ ['realpathSync', 1], ['readFileSync', 1], ['openSync', 3], ['readSync', 1], ['closeSync', 1], ['readFile', 2], ['mkdir', 1], ['lstat', 1], ['stat', 1], ['watch', 1], ['readdir', 1], ['access', 2], ['open', 2], ['write', 1], ['fdatasync', 1], ['close', 1], ['read', 1], ['unlink', 1], ['rmdir', 1], ].forEach((element) => { intercept(element[0], element[1]); }); })(); const { ipcRenderer } = require('electron'); const assert = require('assert'); const path = require('path'); const glob = require('glob'); const util = require('util'); const coverage = require('../coverage'); const { pathToFileURL } = require('url'); // Disabled custom inspect. See #38847 if (util.inspect && util.inspect['defaultOptions']) { util.inspect['defaultOptions'].customInspect = false; } // VSCODE_GLOBALS: package/product.json globalThis._VSCODE_PRODUCT_JSON = require('../../../product.json'); globalThis._VSCODE_PACKAGE_JSON = require('../../../package.json'); // Test file operations that are common across platforms. Used for test infra, namely snapshot tests Object.assign(globalThis, { __readFileInTests: path => fs.promises.readFile(path, 'utf-8'), __writeFileInTests: (path, contents) => fs.promises.writeFile(path, contents), __readDirInTests: path => fs.promises.readdir(path), __unlinkInTests: path => fs.promises.unlink(path), __mkdirPInTests: path => fs.promises.mkdir(path, { recursive: true }), }); const IS_CI = !!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || !!process.env.GITHUB_WORKSPACE; const _tests_glob = '**/test/**/*.test.js'; /** * Loads one or N modules. * @type {{ * (module: string|string[]): Promise|Promise; * _out: string; * }} */ let loadFn; const _loaderErrors = []; function initNls(opts) { if (opts.build) { // when running from `out-build`, ensure to load the default // messages file, because all `nls.localize` calls have their // english values removed and replaced by an index. globalThis._VSCODE_NLS_MESSAGES = require(`../../../out-build/nls.messages.json`); } } function initLoadFn(opts) { const outdir = opts.build ? 'out-build' : 'out'; const out = path.join(__dirname, `../../../${outdir}`); const baseUrl = pathToFileURL(path.join(__dirname, `../../../${outdir}/`)); globalThis._VSCODE_FILE_ROOT = baseUrl.href; // set loader function importModules(modules) { const moduleArray = Array.isArray(modules) ? modules : [modules]; const tasks = moduleArray.map(mod => { const url = new URL(`./${mod}.js`, baseUrl).href; return import(url).catch(err => { console.log(mod, url); console.log(err); _loaderErrors.push(err); throw err; }); }); return Array.isArray(modules) ? Promise.all(tasks) : tasks[0]; } importModules._out = out; loadFn = importModules; } async function createCoverageReport(opts) { if (!opts.coverage) { return undefined; } return coverage.createReport(opts.run || opts.runGlob); } async function loadModules(modules) { for (const file of modules) { mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, mocha); const m = await loadFn(file); mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_REQUIRE, m, file, mocha); mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, mocha); } } const globAsync = util.promisify(glob); async function loadTestModules(opts) { if (opts.run) { const files = Array.isArray(opts.run) ? opts.run : [opts.run]; const modules = files.map(file => { file = file.replace(/^src[\\/]/, ''); return file.replace(/\.[jt]s$/, ''); }); return loadModules(modules); } const pattern = opts.runGlob || _tests_glob; const files = await globAsync(pattern, { cwd: loadFn._out }); let modules = files.map(file => file.replace(/\.js$/, '')); if (opts.testSplit) { const [i, n] = opts.testSplit.split('/').map(Number); const chunkSize = Math.floor(modules.length / n); const start = (i - 1) * chunkSize; const end = i === n ? modules.length : i * chunkSize; modules = modules.slice(start, end); } return loadModules(modules); } /** @type Mocha.Test */ let currentTest; async function loadTests(opts) { //#region Unexpected Output const _allowedTestOutput = [ /The vm module of Node\.js is deprecated in the renderer process and will be removed./, ]; // allow snapshot mutation messages locally if (!IS_CI) { _allowedTestOutput.push(/Creating new snapshot in/); _allowedTestOutput.push(/Deleting [0-9]+ old snapshots/); } const perTestCoverage = opts['per-test-coverage'] ? await PerTestCoverage.init() : undefined; const _allowedTestsWithOutput = new Set([ 'creates a snapshot', // self-testing 'validates a snapshot', // self-testing 'cleans up old snapshots', // self-testing 'issue #149412: VS Code hangs when bad semantic token data is received', // https://github.com/microsoft/vscode/issues/192440 'issue #134973: invalid semantic tokens should be handled better', // https://github.com/microsoft/vscode/issues/192440 '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 'issue #149130: vscode freezes because of Bracket Pair Colorization', // https://github.com/microsoft/vscode/issues/192440 'property limits', // https://github.com/microsoft/vscode/issues/192443 'Error events', // https://github.com/microsoft/vscode/issues/192443 'fetch returns keybinding with user first if title and id matches', // 'throw ListenerLeakError' ]); const _allowedSuitesWithOutput = new Set([ 'InlineChatController' ]); let _testsWithUnexpectedOutput = false; for (const consoleFn of [console.log, console.error, console.info, console.warn, console.trace, console.debug]) { console[consoleFn.name] = function (msg) { if (!currentTest) { consoleFn.apply(console, arguments); } else if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title) && !_allowedSuitesWithOutput.has(currentTest.parent?.title ?? '')) { _testsWithUnexpectedOutput = true; consoleFn.apply(console, arguments); } }; } //#endregion //#region Unexpected / Loader Errors const _unexpectedErrors = []; const _allowedTestsWithUnhandledRejections = new Set([ // Lifecycle tests 'onWillShutdown - join with error is handled', 'onBeforeShutdown - veto with error is treated as veto', 'onBeforeShutdown - final veto with error is treated as veto', // Search tests 'Search Model: Search reports timed telemetry on search when error is called' ]); const errors = await loadFn('vs/base/common/errors'); const onUnexpectedError = function (err) { if (err.name === 'Canceled') { return; // ignore canceled errors that are common } let stack = (err ? err.stack : null); if (!stack) { stack = new Error().stack; } _unexpectedErrors.push((err && err.message ? err.message : err) + '\n' + stack); }; process.on('uncaughtException', error => onUnexpectedError(error)); process.on('unhandledRejection', (reason, promise) => { onUnexpectedError(reason); promise.catch(() => { }); }); window.addEventListener('unhandledrejection', event => { event.preventDefault(); // Do not log to test output, we show an error later when test ends event.stopPropagation(); if (!_allowedTestsWithUnhandledRejections.has(currentTest.title)) { onUnexpectedError(event.reason); } }); errors.setUnexpectedErrorHandler(onUnexpectedError); //#endregion const { assertCleanState } = await loadFn('vs/workbench/test/common/utils'); suite('Tests are using suiteSetup and setup correctly', () => { test('assertCleanState - check that registries are clean at the start of test running', () => { assertCleanState(); }); }); setup(async () => { await perTestCoverage?.startTest(); }); teardown(async () => { await perTestCoverage?.finishTest(currentTest.file, currentTest.fullTitle()); // should not have unexpected output if (_testsWithUnexpectedOutput && !opts.dev) { assert.ok(false, 'Error: Unexpected console output in test run. Please ensure no console.[log|error|info|warn] usage in tests or runtime errors.'); } // should not have unexpected errors const errors = _unexpectedErrors.concat(_loaderErrors); if (errors.length) { const msg = []; for (const error of errors) { console.error(`Error: Test run should not have unexpected errors:\n${error}`); msg.push(String(error)); } assert.ok(false, `Error: Test run should not have unexpected errors:\n${msg.join('\n')}`); } }); suiteTeardown(() => { // intentionally not in teardown because some tests only cleanup in suiteTeardown // should have cleaned up in registries assertCleanState(); }); return loadTestModules(opts); } function serializeSuite(suite) { return { root: suite.root, suites: suite.suites.map(serializeSuite), tests: suite.tests.map(serializeRunnable), title: suite.title, fullTitle: suite.fullTitle(), titlePath: suite.titlePath(), timeout: suite.timeout(), retries: suite.retries(), slow: suite.slow(), bail: suite.bail() }; } function serializeRunnable(runnable) { return { title: runnable.title, fullTitle: runnable.fullTitle(), titlePath: runnable.titlePath(), async: runnable.async, slow: runnable.slow(), speed: runnable.speed, duration: runnable.duration }; } function serializeError(err) { return { message: err.message, stack: err.stack, snapshotPath: err.snapshotPath, actual: safeStringify({ value: err.actual }), expected: safeStringify({ value: err.expected }), uncaught: err.uncaught, showDiff: err.showDiff, inspect: typeof err.inspect === 'function' ? err.inspect() : '' }; } function safeStringify(obj) { const seen = new Set(); return JSON.stringify(obj, (key, value) => { if (value === undefined) { return '[undefined]'; } if (isObject(value) || Array.isArray(value)) { if (seen.has(value)) { return '[Circular]'; } else { seen.add(value); } } return value; }); } function isObject(obj) { // The method can't do a type cast since there are type (like strings) which // are subclasses of any put not positively matched by the function. Hence type // narrowing results in wrong results. return typeof obj === 'object' && obj !== null && !Array.isArray(obj) && !(obj instanceof RegExp) && !(obj instanceof Date); } class IPCReporter { constructor(runner) { runner.on('start', () => ipcRenderer.send('start')); runner.on('end', () => ipcRenderer.send('end')); runner.on('suite', suite => ipcRenderer.send('suite', serializeSuite(suite))); runner.on('suite end', suite => ipcRenderer.send('suite end', serializeSuite(suite))); runner.on('test', test => ipcRenderer.send('test', serializeRunnable(test))); runner.on('test end', test => ipcRenderer.send('test end', serializeRunnable(test))); runner.on('hook', hook => ipcRenderer.send('hook', serializeRunnable(hook))); runner.on('hook end', hook => ipcRenderer.send('hook end', serializeRunnable(hook))); runner.on('pass', test => ipcRenderer.send('pass', serializeRunnable(test))); runner.on('fail', (test, err) => ipcRenderer.send('fail', serializeRunnable(test), serializeError(err))); runner.on('pending', test => ipcRenderer.send('pending', serializeRunnable(test))); } } const $globalThis = globalThis; const setTimeout0IsFaster = (typeof $globalThis.postMessage === 'function' && !$globalThis.importScripts); /** * See https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#:~:text=than%204%2C%20then-,set%20timeout%20to%204,-. * * Works similarly to `setTimeout(0)` but doesn't suffer from the 4ms artificial delay * that browsers set when the nesting level is > 5. */ const setTimeout0 = (() => { if (setTimeout0IsFaster) { const pending = []; $globalThis.addEventListener('message', (e) => { if (e.data && e.data.vscodeScheduleAsyncWork) { for (let i = 0, len = pending.length; i < len; i++) { const candidate = pending[i]; if (candidate.id === e.data.vscodeScheduleAsyncWork) { pending.splice(i, 1); candidate.callback(); return; } } } }); let lastId = 0; return (callback) => { const myId = ++lastId; pending.push({ id: myId, callback: callback }); $globalThis.postMessage({ vscodeScheduleAsyncWork: myId }, '*'); }; } return (callback) => setTimeout(callback); })(); async function runTests(opts) { // @ts-expect-error Mocha.Runner.immediately = setTimeout0; mocha.setup({ ui: 'tdd', // @ts-expect-error reporter: opts.dev ? 'html' : IPCReporter, grep: opts.grep, timeout: opts.timeout ?? (IS_CI ? 30000 : 5000), forbidOnly: IS_CI // disallow .only() when running on build machine }); // this *must* come before loadTests, or it doesn't work. if (opts.timeout !== undefined) { mocha.timeout(opts.timeout); } await loadTests(opts); const runner = mocha.run(async () => { await createCoverageReport(opts); ipcRenderer.send('all done'); }); runner.on('test', test => currentTest = test); if (opts.dev) { runner.on('fail', (test, err) => { console.error(test.fullTitle()); console.error(err.stack); }); } } ipcRenderer.on('run', async (_e, opts) => { initNls(opts); initLoadFn(opts); await Promise.resolve(globalThis._VSCODE_TEST_INIT); try { await runTests(opts); } catch (err) { if (typeof err !== 'string') { err = JSON.stringify(err); } console.error(err); ipcRenderer.send('error', err); } }); class PerTestCoverage { static async init() { await ipcRenderer.invoke('startCoverage'); return new PerTestCoverage(); } async startTest() { if (!this.didInit) { this.didInit = true; await ipcRenderer.invoke('snapshotCoverage'); } } async finishTest(file, fullTitle) { await ipcRenderer.invoke('snapshotCoverage', { file, fullTitle }); } }