chore - modernise test runner (#235511)

async/wait, remove AMD/ESM split, cleanup
pull/235520/head
Johannes Rieken 2024-12-06 18:53:29 +01:00 committed by GitHub
parent d89fdcbfa8
commit 17f6bcb7d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 114 additions and 122 deletions

View File

@ -5,6 +5,8 @@
/*eslint-env mocha*/ /*eslint-env mocha*/
// @ts-check
const fs = require('fs'); const fs = require('fs');
(function () { (function () {
@ -24,7 +26,7 @@ const fs = require('fs');
function createSpy(element, cnt) { function createSpy(element, cnt) {
return function (...args) { return function (...args) {
if (logging) { if (logging) {
console.log(`calling ${element}: ` + args.slice(0, cnt).join(',') + (withStacks ? (`\n` + new Error().stack.split('\n').slice(2).join('\n')) : '')); 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); return originals[element].call(this, ...args);
}; };
@ -88,9 +90,18 @@ Object.assign(globalThis, {
const IS_CI = !!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY; const IS_CI = !!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY;
const _tests_glob = '**/test/**/*.test.js'; const _tests_glob = '**/test/**/*.test.js';
let loader;
/**
* Loads one or N modules.
* @type {{
* (module: string|string[]): Promise<any>|Promise<any[]>;
* _out: string;
* }}
*/
let loadFn;
const _loaderErrors = []; const _loaderErrors = [];
let _out;
function initNls(opts) { function initNls(opts) {
if (opts.build) { if (opts.build) {
@ -101,20 +112,17 @@ function initNls(opts) {
} }
} }
function initLoader(opts) { function initLoadFn(opts) {
const outdir = opts.build ? 'out-build' : 'out'; const outdir = opts.build ? 'out-build' : 'out';
_out = path.join(__dirname, `../../../${outdir}`); const out = path.join(__dirname, `../../../${outdir}`);
const baseUrl = pathToFileURL(path.join(__dirname, `../../../${outdir}/`)); const baseUrl = pathToFileURL(path.join(__dirname, `../../../${outdir}/`));
globalThis._VSCODE_FILE_ROOT = baseUrl.href; globalThis._VSCODE_FILE_ROOT = baseUrl.href;
// set loader // set loader
/** function importModules(modules) {
* @param {string[]} modules const moduleArray = Array.isArray(modules) ? modules : [modules];
* @param {(...args:any[]) => void} callback const tasks = moduleArray.map(mod => {
*/
function esmRequire(modules, callback, errorback) {
const tasks = modules.map(mod => {
const url = new URL(`./${mod}.js`, baseUrl).href; const url = new URL(`./${mod}.js`, baseUrl).href;
return import(url).catch(err => { return import(url).catch(err => {
console.log(mod, url); console.log(mod, url);
@ -124,35 +132,33 @@ function initLoader(opts) {
}); });
}); });
Promise.all(tasks).then(modules => callback(...modules)).catch(errorback); return Array.isArray(modules)
? Promise.all(tasks)
: tasks[0];
} }
importModules._out = out;
loader = { require: esmRequire }; loadFn = importModules;
} }
function createCoverageReport(opts) { async function createCoverageReport(opts) {
if (opts.coverage) { if (!opts.coverage) {
return coverage.createReport(opts.run || opts.runGlob); return undefined;
} }
return Promise.resolve(undefined); return coverage.createReport(opts.run || opts.runGlob);
}
function loadWorkbenchTestingUtilsModule() {
return new Promise((resolve, reject) => {
loader.require(['vs/workbench/test/common/utils'], resolve, reject);
});
} }
async function loadModules(modules) { async function loadModules(modules) {
for (const file of modules) { for (const file of modules) {
mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, mocha); mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, mocha);
const m = await new Promise((resolve, reject) => loader.require([file], resolve, reject)); 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_REQUIRE, m, file, mocha);
mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, mocha); mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, mocha);
} }
} }
function loadTestModules(opts) { const globAsync = util.promisify(glob);
async function loadTestModules(opts) {
if (opts.run) { if (opts.run) {
const files = Array.isArray(opts.run) ? opts.run : [opts.run]; const files = Array.isArray(opts.run) ? opts.run : [opts.run];
@ -164,17 +170,9 @@ function loadTestModules(opts) {
} }
const pattern = opts.runGlob || _tests_glob; const pattern = opts.runGlob || _tests_glob;
const files = await globAsync(pattern, { cwd: loadFn._out });
return new Promise((resolve, reject) => { const modules = files.map(file => file.replace(/\.js$/, ''));
glob(pattern, { cwd: _out }, (err, files) => { return loadModules(modules);
if (err) {
reject(err);
return;
}
const modules = files.map(file => file.replace(/\.js$/, ''));
resolve(modules);
});
}).then(loadModules);
} }
/** @type Mocha.Test */ /** @type Mocha.Test */
@ -220,7 +218,7 @@ async function loadTests(opts) {
console[consoleFn.name] = function (msg) { console[consoleFn.name] = function (msg) {
if (!currentTest) { if (!currentTest) {
consoleFn.apply(console, arguments); consoleFn.apply(console, arguments);
} else if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title) && !_allowedSuitesWithOutput.has(currentTest.parent?.title)) { } else if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title) && !_allowedSuitesWithOutput.has(currentTest.parent?.title ?? '')) {
_testsWithUnexpectedOutput = true; _testsWithUnexpectedOutput = true;
consoleFn.apply(console, arguments); consoleFn.apply(console, arguments);
} }
@ -242,79 +240,74 @@ async function loadTests(opts) {
'Search Model: Search reports timed telemetry on search when error is called' 'Search Model: Search reports timed telemetry on search when error is called'
]); ]);
loader.require(['vs/base/common/errors'], function (errors) { const errors = await loadFn('vs/base/common/errors');
const onUnexpectedError = function (err) {
if (err.name === 'Canceled') {
return; // ignore canceled errors that are common
}
const onUnexpectedError = function (err) { let stack = (err ? err.stack : null);
if (err.name === 'Canceled') { if (!stack) {
return; // ignore canceled errors that are common stack = new Error().stack;
} }
let stack = (err ? err.stack : null); _unexpectedErrors.push((err && err.message ? err.message : err) + '\n' + stack);
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();
process.on('uncaughtException', error => onUnexpectedError(error)); if (!_allowedTestsWithUnhandledRejections.has(currentTest.title)) {
process.on('unhandledRejection', (reason, promise) => { onUnexpectedError(event.reason);
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);
}); });
errors.setUnexpectedErrorHandler(onUnexpectedError);
//#endregion //#endregion
return loadWorkbenchTestingUtilsModule().then((workbenchTestingModule) => { const { assertCleanState } = await loadFn('vs/workbench/test/common/utils');
const assertCleanState = workbenchTestingModule.assertCleanState;
suite('Tests are using suiteSetup and setup correctly', () => { suite('Tests are using suiteSetup and setup correctly', () => {
test('assertCleanState - check that registries are clean at the start of test running', () => { 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) {
for (const error of errors) {
console.error(`Error: Test run should not have unexpected errors:\n${error}`);
}
assert.ok(false, 'Error: Test run should not have unexpected errors.');
}
});
suiteTeardown(() => { // intentionally not in teardown because some tests only cleanup in suiteTeardown
// should have cleaned up in registries
assertCleanState(); assertCleanState();
}); });
return loadTestModules(opts);
}); });
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) {
for (const error of errors) {
console.error(`Error: Test run should not have unexpected errors:\n${error}`);
}
assert.ok(false, 'Error: Test run should not have unexpected errors.');
}
});
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) { function serializeSuite(suite) {
@ -403,42 +396,41 @@ class IPCReporter {
} }
} }
function runTests(opts) { async function runTests(opts) {
// this *must* come before loadTests, or it doesn't work. // this *must* come before loadTests, or it doesn't work.
if (opts.timeout !== undefined) { if (opts.timeout !== undefined) {
mocha.timeout(opts.timeout); mocha.timeout(opts.timeout);
} }
return loadTests(opts).then(() => { await loadTests(opts);
if (opts.grep) { if (opts.grep) {
mocha.grep(opts.grep); mocha.grep(opts.grep);
} }
if (!opts.dev) { if (!opts.dev) {
mocha.reporter(IPCReporter); // @ts-expect-error
} mocha.reporter(IPCReporter);
}
const runner = mocha.run(() => { const runner = mocha.run(async () => {
createCoverageReport(opts).then(() => { await createCoverageReport(opts)
ipcRenderer.send('all done'); 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);
});
}
}); });
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) => { ipcRenderer.on('run', async (_e, opts) => {
initNls(opts); initNls(opts);
initLoader(opts); initLoadFn(opts);
await Promise.resolve(globalThis._VSCODE_TEST_INIT); await Promise.resolve(globalThis._VSCODE_TEST_INIT);