mirror of
https://github.com/microsoft/vscode.git
synced 2026-06-29 02:45:58 +01:00
bf2334eeee
* ci: add Node.js diagnostic reports for test crash investigation Enable Node.js diagnostic reporting for the node.js unit test runner so that native crashes (like the zlib crash in build 447204) produce actionable diagnostic reports instead of a silent exit code 1. Changes: - Configure process.report in test/unit/node/index.js to write JSON diagnostic reports to .build/crashes on fatal errors and uncaught exceptions - Add unhandledRejection handler (was missing, unlike Electron tests) - Set NODE_OPTIONS in CI pipeline steps (win32, linux, darwin) with --report-on-fatalerror and --report-uncaught-exception flags - Reports are picked up by the existing crash-dump artifact collection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: remove process.exit(1) from exception handlers The uncaughtException handler intentionally does NOT exit — it logs the error and lets Mocha continue running. The test suite has a dedicated 'Errors' suite that asserts on collected unexpected errors at the end. Calling process.exit(1) kills the test runner on the first uncaught exception, failing all Electron tests on all platforms. Keep the writeReport() calls for diagnostic purposes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove writeReport() from exception handlers The manual writeReport() calls fire on benign unhandled rejections (e.g. Canceled errors during test teardown in UserDataSyncService) and block the event loop writing JSON reports to disk, causing subsequent faked-timer tests to exceed their 2000ms timeout. Report generation is already handled by process.report config and NODE_OPTIONS flags (--report-on-fatalerror, --report-uncaught-exception) which only fire on actual fatal errors, not on every caught rejection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
255 lines
8.4 KiB
JavaScript
255 lines
8.4 KiB
JavaScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
//@ts-check
|
|
'use strict';
|
|
|
|
process.env.MOCHA_COLORS = '1'; // Force colors (note that this must come before any mocha imports)
|
|
|
|
import * as assert from 'assert';
|
|
import Mocha from 'mocha';
|
|
import * as path from 'path';
|
|
import * as fs from 'fs';
|
|
import glob from 'glob';
|
|
import minimatch from 'minimatch';
|
|
import minimist from 'minimist';
|
|
import * as module from 'module';
|
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
import semver from 'semver';
|
|
|
|
/**
|
|
* @type {{ build: boolean; run: string; runGlob: string; coverage: boolean; help: boolean; coverageFormats: string | string[]; coveragePath: string; }}
|
|
*/
|
|
const args = minimist(process.argv.slice(2), {
|
|
boolean: ['build', 'coverage', 'help'],
|
|
string: ['run', 'coveragePath', 'coverageFormats'],
|
|
alias: {
|
|
h: 'help'
|
|
},
|
|
default: {
|
|
build: false,
|
|
coverage: false,
|
|
help: false
|
|
},
|
|
description: {
|
|
build: 'Run from out-build',
|
|
run: 'Run a single file',
|
|
coverage: 'Generate a coverage report',
|
|
coveragePath: 'Path to coverage report to generate',
|
|
coverageFormats: 'Coverage formats to generate',
|
|
help: 'Show help'
|
|
}
|
|
});
|
|
|
|
if (args.help) {
|
|
console.log(`Usage: node test/unit/node/index [options]
|
|
|
|
Options:
|
|
--build Run from out-build
|
|
--run <file> Run a single file
|
|
--coverage Generate a coverage report
|
|
--help Show help`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const TEST_GLOB = '**/test/**/*.test.js';
|
|
|
|
const excludeGlobs = [
|
|
'**/{browser,electron-browser,electron-main,electron-utility}/**/*.test.js',
|
|
'**/vs/platform/environment/test/node/nativeModules.test.js', // native modules are compiled against Electron and this test would fail with node.js
|
|
'**/vs/base/parts/storage/test/node/storage.test.js', // same as above, due to direct dependency to sqlite native module
|
|
'**/vs/workbench/contrib/testing/test/**', // flaky (https://github.com/microsoft/vscode/issues/137853)
|
|
'**/vs/sessions/test/web.test.js', // web-only E2E test that imports CSS — cannot run in Node
|
|
];
|
|
|
|
const REPO_ROOT = fileURLToPath(new URL('../../../', import.meta.url));
|
|
const out = args.build ? 'out-build' : 'out';
|
|
const src = path.join(REPO_ROOT, out);
|
|
const baseUrl = pathToFileURL(src);
|
|
|
|
//@ts-ignore
|
|
const requiredNodeVersion = semver.parse(/^target="(.*)"$/m.exec(fs.readFileSync(path.join(REPO_ROOT, 'remote', '.npmrc'), 'utf8'))[1]);
|
|
const currentNodeVersion = semver.parse(process.version);
|
|
//@ts-ignore
|
|
if (currentNodeVersion?.major < requiredNodeVersion?.major) {
|
|
console.error(`node.js unit tests require a major node.js version of ${requiredNodeVersion?.major} (your version is: ${currentNodeVersion?.major})`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function main() {
|
|
|
|
// VSCODE_GLOBALS: package/product.json
|
|
const _require = module.createRequire(import.meta.url);
|
|
globalThis._VSCODE_PRODUCT_JSON = _require(`${REPO_ROOT}/product.json`);
|
|
globalThis._VSCODE_PACKAGE_JSON = _require(`${REPO_ROOT}/package.json`);
|
|
|
|
// VSCODE_GLOBALS: file root
|
|
globalThis._VSCODE_FILE_ROOT = baseUrl.href;
|
|
|
|
if (args.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(`${REPO_ROOT}/${out}/nls.messages.json`);
|
|
}
|
|
|
|
// Test file operations that are common across platforms. Used for test infra, namely snapshot tests
|
|
Object.assign(globalThis, {
|
|
// __analyzeSnapshotInTests: takeSnapshotAndCountClasses,
|
|
__readFileInTests: (/** @type {string} */ path) => fs.promises.readFile(path, 'utf-8'),
|
|
__writeFileInTests: (/** @type {string} */ path, /** @type {BufferEncoding} */ contents) => fs.promises.writeFile(path, contents),
|
|
__readDirInTests: (/** @type {string} */ path) => fs.promises.readdir(path),
|
|
__unlinkInTests: (/** @type {string} */ path) => fs.promises.unlink(path),
|
|
__mkdirPInTests: (/** @type {string} */ path) => fs.promises.mkdir(path, { recursive: true }),
|
|
});
|
|
|
|
// Configure Node.js diagnostic reports for crash investigation.
|
|
// Reports are written to .build/crashes so the existing CI artifact
|
|
// collection picks them up alongside Electron crash dumps.
|
|
const crashDir = path.join(REPO_ROOT, '.build', 'crashes');
|
|
fs.mkdirSync(crashDir, { recursive: true });
|
|
if (process.report) {
|
|
process.report.directory = crashDir;
|
|
process.report.reportOnFatalError = true;
|
|
process.report.reportOnUncaughtException = true;
|
|
}
|
|
|
|
process.on('uncaughtException', function(e) {
|
|
console.error(e.stack || e);
|
|
});
|
|
|
|
process.on('unhandledRejection', function(reason) {
|
|
console.error('Unhandled promise rejection:');
|
|
console.error(reason && (/** @type {Error} */ (reason)).stack || reason);
|
|
});
|
|
|
|
/**
|
|
* @param modules
|
|
* @param onLoad
|
|
* @param onError
|
|
*/
|
|
const loader = function(modules, onLoad, onError) {
|
|
const loads = modules.map(mod => import(`${baseUrl}/${mod}.js`).catch(err => {
|
|
console.error(`FAILED to load ${mod} as ${baseUrl}/${mod}.js`);
|
|
throw err;
|
|
}));
|
|
Promise.all(loads).then(onLoad, onError);
|
|
};
|
|
|
|
|
|
let didErr = false;
|
|
const write = process.stderr.write;
|
|
process.stderr.write = function(...args) {
|
|
didErr = didErr || !!args[0];
|
|
return write.apply(process.stderr, args);
|
|
};
|
|
|
|
|
|
const runner = new Mocha({
|
|
ui: 'tdd'
|
|
});
|
|
|
|
/**
|
|
* @param modules
|
|
*/
|
|
async function loadModules(modules) {
|
|
for (const file of modules) {
|
|
runner.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, runner);
|
|
const m = await new Promise((resolve, reject) => loader([file], resolve, reject));
|
|
runner.suite.emit(Mocha.Suite.constants.EVENT_FILE_REQUIRE, m, file, runner);
|
|
runner.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, runner);
|
|
}
|
|
}
|
|
|
|
/** @type { null|((callback:(err:any)=>void)=>void) } */
|
|
let loadFunc = null;
|
|
|
|
if (args.runGlob) {
|
|
loadFunc = (cb) => {
|
|
const doRun = /** @param tests */(tests) => {
|
|
const modulesToLoad = tests.map(test => {
|
|
if (path.isAbsolute(test)) {
|
|
test = path.relative(src, path.resolve(test));
|
|
}
|
|
|
|
return test.replace(/(\.js)|(\.d\.ts)|(\.js\.map)$/, '');
|
|
});
|
|
loadModules(modulesToLoad).then(() => cb(null), cb);
|
|
};
|
|
|
|
glob(args.runGlob, { cwd: src }, function(err, files) { doRun(files); });
|
|
};
|
|
} else if (args.run) {
|
|
const tests = (typeof args.run === 'string') ? [args.run] : args.run;
|
|
const modulesToLoad = tests.map(function(test) {
|
|
test = test.replace(/^src/, 'out');
|
|
test = test.replace(/\.ts$/, '.js');
|
|
return path.relative(src, path.resolve(test)).replace(/(\.js)|(\.js\.map)$/, '').replace(/\\/g, '/');
|
|
});
|
|
loadFunc = (cb) => {
|
|
loadModules(modulesToLoad).then(() => cb(null), cb);
|
|
};
|
|
} else {
|
|
loadFunc = (cb) => {
|
|
glob(TEST_GLOB, { cwd: src }, function(err, files) {
|
|
/** @type {string[]} */
|
|
const modules = [];
|
|
for (const file of files) {
|
|
if (!excludeGlobs.some(excludeGlob => minimatch(file, excludeGlob))) {
|
|
modules.push(file.replace(/\.js$/, ''));
|
|
}
|
|
}
|
|
loadModules(modules).then(() => cb(null), cb);
|
|
});
|
|
};
|
|
}
|
|
|
|
loadFunc(function(err) {
|
|
if (err) {
|
|
console.error(err);
|
|
return process.exit(1);
|
|
}
|
|
|
|
process.stderr.write = write;
|
|
|
|
if (!args.run && !args.runGlob) {
|
|
// set up last test
|
|
Mocha.suite('Loader', function() {
|
|
test('should not explode while loading', function() {
|
|
assert.ok(!didErr, `should not explode while loading: ${didErr}`);
|
|
});
|
|
});
|
|
}
|
|
|
|
// report failing test for every unexpected error during any of the tests
|
|
const unexpectedErrors = [];
|
|
Mocha.suite('Errors', function() {
|
|
test('should not have unexpected errors in tests', function() {
|
|
if (unexpectedErrors.length) {
|
|
unexpectedErrors.forEach(function(stack) {
|
|
console.error('');
|
|
console.error(stack);
|
|
});
|
|
|
|
assert.ok(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// replace the default unexpected error handler to be useful during tests
|
|
import(`${baseUrl}/vs/base/common/errors.js`).then(errors => {
|
|
errors.setUnexpectedErrorHandler(function(err) {
|
|
const stack = (err && err.stack) || (new Error().stack);
|
|
unexpectedErrors.push((err && err.message ? err.message : err) + '\n' + stack);
|
|
});
|
|
|
|
// fire up mocha
|
|
runner.run(failures => process.exit(failures ? 1 : 0));
|
|
});
|
|
});
|
|
}
|
|
|
|
main();
|