Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: parallel test running in workers for node and the browser #1682

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"files": [
"bin/",
"src/cli/",
"src/workers/",
"qunit/qunit.js",
"qunit/qunit.css",
"LICENSE.txt"
Expand Down
144 changes: 87 additions & 57 deletions src/cli/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const { findReporter } = require( "./find-reporter" );

const DEBOUNCE_WATCH_LENGTH = 60;
const DEBOUNCE_RESTART_LENGTH = 200 - DEBOUNCE_WATCH_LENGTH;
const os = require( "os" );

const changedPendingPurge = [];

Expand All @@ -20,15 +21,27 @@ async function run( args, options ) {
// Default to non-zero exit code to avoid false positives
process.exitCode = 1;

const files = utils.getFilesFromArgs( args );

QUnit = requireQUnit();

let globalConfig = {};

// TODO: Enable mode where QUnit is not auto-injected, but other setup is
// still done automatically.
if ( global.QUnit && global.QUnit.config ) {
globalConfig = global.QUnit.config;
}
global.QUnit = QUnit;

Object.keys( globalConfig ).forEach( function( key ) {
QUnit.config[ key ] = globalConfig[ key ];
} );

if ( options.filter ) {
QUnit.config.filter = options.filter;
}

const seed = options.seed;

if ( seed ) {
if ( seed === true ) {
QUnit.config.seed = Math.random().toString( 36 ).slice( 2 );
Expand All @@ -39,65 +52,80 @@ async function run( args, options ) {
console.log( `Running tests with seed: ${QUnit.config.seed}` );
}

// TODO: Enable mode where QUnit is not auto-injected, but other setup is
// still done automatically.
global.QUnit = QUnit;
if ( !options.noReporter ) {
findReporter( options.reporter, QUnit.reporters ).init( QUnit );
}

options.requires.forEach( requireFromCWD );
if ( !QUnit.config.isWorker ) {
QUnit.config.maxThreads = os.cpus().length;
QUnit.config.workerType = "NodeWorker";
QUnit.config.files = args;

// eslint-disable-next-line node/no-unsupported-features/es-syntax
const nodeWorkerModule = await import( "../workers/node-worker.mjs" );
const NodeWorker = nodeWorkerModule.default;

QUnit.WorkerFactory.registerWorkerClass( NodeWorker );

} else {
options.requires.forEach( requireFromCWD );

const files = utils.getFilesFromArgs( args );

for ( let i = 0; i < files.length; i++ ) {
const filePath = path.resolve( process.cwd(), files[ i ] );
delete require.cache[ filePath ];

// Node.js 12.0.0 has node_module_version=72
// https://nodejs.org/en/download/releases/
const nodeVint = process.config.variables.node_module_version;

findReporter( options.reporter, QUnit.reporters ).init( QUnit );

for ( let i = 0; i < files.length; i++ ) {
const filePath = path.resolve( process.cwd(), files[ i ] );
delete require.cache[ filePath ];

// Node.js 12.0.0 has node_module_version=72
// https://nodejs.org/en/download/releases/
const nodeVint = process.config.variables.node_module_version;

try {

// QUnit supports passing ESM files to the 'qunit' command when used on
// Node.js 12 or later. The dynamic import() keyword supports both CommonJS files
// (.js, .cjs) and ESM files (.mjs), so we could simply use that unconditionally on
// newer Node versions, regardless of the given file path.
//
// But:
// - Node.js 12 emits a confusing "ExperimentalWarning" when using import(),
// even if just to load a non-ESM file. So we should try to avoid it on non-ESM.
// - This Node.js feature is still considered experimental so to avoid unexpected
// breakage we should continue using require(). Consider flipping once stable and/or
// as part of QUnit 3.0.
// - Plugins and CLI bootstrap scripts may be hooking into require.extensions to modify
// or transform code as it gets loaded. For compatibility with that, we should
// support that until at least QUnit 3.0.
// - File extensions are not sufficient to differentiate between CJS and ESM.
// Use of ".mjs" is optional, as a package may configure Node to default to ESM
// and optionally use ".cjs" for CJS files.
//
// https://nodejs.org/docs/v12.7.0/api/modules.html#modules_addenda_the_mjs_extension
// https://nodejs.org/docs/v12.7.0/api/esm.html#esm_code_import_code_expressions
// https://github.com/qunitjs/qunit/issues/1465
try {
require( filePath );
} catch ( e ) {
if ( ( e.code === "ERR_REQUIRE_ESM" ||
( e instanceof SyntaxError &&
e.message === "Cannot use import statement outside a module" ) ) &&
( !nodeVint || nodeVint >= 72 ) ) {

// filePath is an absolute file path here (per path.resolve above).
// On Windows, Node.js enforces that absolute paths via ESM use valid URLs,
// e.g. file-protocol) https://github.com/qunitjs/qunit/issues/1667
await import( url.pathToFileURL( filePath ) ); // eslint-disable-line node/no-unsupported-features/es-syntax
} else {
throw e;

// QUnit supports passing ESM files to the 'qunit' command when used on
// Node.js 12 or later. The dynamic import() keyword supports both CommonJS files
// (.js, .cjs) and ESM files (.mjs), so we could simply use that unconditionally on
// newer Node versions, regardless of the given file path.
//
// But:
// - Node.js 12 emits a confusing "ExperimentalWarning" when using import(),
// even if just to load a non-ESM file. So we should try to avoid it on non-ESM.
// - This Node.js feature is still considered experimental so to avoid unexpected
// breakage we should continue using require(). Consider flipping once stable and/or
// as part of QUnit 3.0.
// - Plugins and CLI bootstrap scripts may be hooking into require.extensions to modify
// or transform code as it gets loaded. For compatibility with that, we should
// support that until at least QUnit 3.0.
// - File extensions are not sufficient to differentiate between CJS and ESM.
// Use of ".mjs" is optional, as a package may configure Node to default to ESM
// and optionally use ".cjs" for CJS files.
//
// https://nodejs.org/docs/v12.7.0/api/modules.html#modules_addenda_the_mjs_extension
// https://nodejs.org/docs/v12.7.0/api/esm.html#esm_code_import_code_expressions
// https://github.com/qunitjs/qunit/issues/1465
try {
require( filePath );
} catch ( e ) {
if ( ( e.code === "ERR_REQUIRE_ESM" ||
( e instanceof SyntaxError &&
e.message === "Cannot use import statement outside a module" ) ) &&
( !nodeVint || nodeVint >= 72 ) ) {

// filePath is an absolute file path here (per path.resolve above).
// On Windows, Node.js enforces that absolute paths via ESM use valid URLs,
// e.g. file-protocol) https://github.com/qunitjs/qunit/issues/1667
await import( url.pathToFileURL( filePath ) ); // eslint-disable-line node/no-unsupported-features/es-syntax
} else {
throw e;
}
}
} catch ( e ) {
const error = new Error(
`Failed to load file ${files[ i ]}\n${e.name}: ${e.message}`
);
error.stack = e.stack;
QUnit.onUncaughtException( error );
}
} catch ( e ) {
const error = new Error( `Failed to load file ${files[ i ]}\n${e.name}: ${e.message}` );
error.stack = e.stack;
QUnit.onUncaughtException( error );
}
}

Expand Down Expand Up @@ -141,7 +169,9 @@ async function run( args, options ) {
}
} );

QUnit.start();
if ( !QUnit.config.isWorker ) {
QUnit.start();
}
}

run.restart = function( args ) {
Expand Down
Loading