repo_description: Node.js test runner that lets you develop with confidence 🚀
repo_topics: assert async async-functions ava cli cli-app concurrency javascript node nodejs performance tap tdd test-framework test-runner testing unicorns unit-testing
code: import {mkdir} from 'node:fs/promises';
  import {createRequire} from 'node:module';
  import path from 'node:path';
  import process from 'node:process';
  import {pathToFileURL} from 'node:url';
  import {workerData} from 'node:worker_threads';
  
  import setUpCurrentlyUnhandled from 'currently-unhandled';
  import writeFileAtomic from 'write-file-atomic';
  
  import {set as setChalk} from '../chalk.js';
  import nowAndTimers from '../now-and-timers.cjs';
  import providerManager from '../provider-manager.js';
  import Runner from '../runner.js';
  import serializeError from '../serialize-error.js';
  
  import channel from './channel.cjs';
  import {runCompletionHandlers} from './completion-handlers.js';
  import lineNumberSelection from './line-numbers.js';
  import {set as setOptions} from './options.cjs';
  import {flags, refs, sharedWorkerTeardowns} from './state.cjs';
  import {isRunningInThread, isRunningInChildProcess} from './utils.cjs';
  
  const currentlyUnhandled = setUpCurrentlyUnhandled();
  let runner;
  
  let expectingExit = false;
  
  const forceExit = () => {
  	expectingExit = true;
  	process.exit(1);
  };
  
  const avaIsDone = () => {
  	expectingExit = true;
  	runCompletionHandlers();
  };
  
  // Override process.exit with an undetectable replacement
  // to report when it is called from a test (which it should never be).
  const handleProcessExit = (target, thisArg, args) => {
  	if (!expectingExit) {
  		const error = new Error('Unexpected process.exit()');
  		Error.captureStackTrace(error, handleProcessExit);
  		channel.send({type: 'process-exit', stack: error.stack});
  	}
  
  	target.apply(thisArg, args);
  };
  
  process.exit = new Proxy(process.exit, {
  	apply: handleProcessExit,
  });
  
  const run = async options => {
  	setOptions(options);
  	setChalk(options.chalkOptions);
  
  	if (options.chalkOptions.level > 0) {
  		const {stdout, stderr} = process;
  		globalThis.console = Object.assign(globalThis.console, new console.Console({stdout, stderr, colorMode: true}));
  	}
  
  	let checkSelectedByLineNumbers;
  	try {
  		checkSelectedByLineNumbers = lineNumberSelection({
  			file: options.file,
  			lineNumbers: options.lineNumbers,
  		});
  	} catch (error) {
  		channel.send({type: 'line-number-selection-error', err: serializeError(error)});
  		checkSelectedByLineNumbers = () => false;
  	}
  
  	runner = new Runner({
  		checkSelectedByLineNumbers,
  		experiments: options.experiments,
  		failFast: options.failFast,
  		failWithoutAssertions: options.failWithoutAssertions,
  		file: options.file,
  		match: options.match,
  		projectDir: options.projectDir,
  		recordNewSnapshots: options.recordNewSnapshots,
  		serial: options.serial,
  		snapshotDir: options.snapshotDir,
  		updateSnapshots: options.updateSnapshots,
  	});
  
  	refs.runnerChain = runner.chain;
  
  	channel.peerFailed.then(() => {
  		runner.interrupt();
  	});
  
  	runner.on('accessed-snapshots', ({data: filename}) => channel.send({type: 'accessed-snapshots', filename}));
  	runner.on('stateChange', ({data: state}) => channel.send(state));
  
  	runner.on('error', ({data: error}) => {
  		channel.send({type: 'internal-error', err: serializeError(error)});
  		forceExit();
  	});
  
  	runner.on('finish', async () => {
  		try {
  			const {touchedFiles} = await runner.saveSnapshotState();
  			if (touchedFiles) {
  				channel.send({type: 'touched-files', files: touchedFiles});
  			}
  		} catch (error) {
  			channel.send({type: 'internal-error', err: serializeError(error)});
  			forceExit();
  			return;
  		}
  
  		try {
  			await Promise.all(sharedWorkerTeardowns.map(fn => fn()));
  		} catch (error) {
  			channel.send({type: 'uncaught-exception', err: serializeError(error)});
  			forceExit();
  			return;
  		}
  
  		channel.send({type: 'worker-finished'});
  
  		// Reference the channel until the worker is freed. This should prevent Node.js from terminating the child process
  		// prematurely, which has been witnessed on Windows. See discussion at
  		// <https://github.com/avajs/ava/issues/3390#issuecomment-3056119361>.
  		channel.ref();
  		await channel.workerFreed;
  		channel.unref();
  
  		nowAndTimers.setImmediate(() => {
  			const unhandled = currentlyUnhandled();
  			if (unhandled.length === 0) {
  				return avaIsDone();
  			}
  
  			for (const rejection of unhandled) {
  				channel.send({type: 'unhandled-rejection', err: serializeError(rejection.reason, {testFile: options.file})});
  			}
  
  			forceExit();
  		});
  	});
  
  	process.on('uncaughtException', error => {
  		channel.send({type: 'uncaught-exception', err: serializeError(error, {testFile: options.file})});
  		forceExit();
  	});
  
  	// Store value to prevent required modules from modifying it.
  	const testPath = options.file;
  
  	const extensionsToLoadAsModules = Object.entries(options.moduleTypes)
  		.filter(([, type]) => type === 'module')
  		.map(([extension]) => extension);
  
  	// Install before processing options.require, so if helpers are added to the
  	// require configuration the *compiled* helper will be loaded.
  	const {projectDir, providerStates = []} = options;
  	const providers = [];
  	await Promise.all(providerStates.map(async ({type, state, protocol}) => {
  		if (type === 'typescript') {
  			const provider = await providerManager.typescript(projectDir, {protocol});
  			providers.push(provider.worker({extensionsToLoadAsModules, state}));
  		}
  	}));
  
  	const require = createRequire(import.meta.url);
  	const load = async ref => {
  		for (const provider of providers) {
  			if (provider.canLoad(ref)) {
  				return provider.load(ref, {requireFn: require});
  			}
  		}
  
  		for (const extension of extensionsToLoadAsModules) {
  			if (ref.endsWith(`.${extension}`)) {
  				return import(pathToFileURL(ref));
  			}
  		}
  
  		// We still support require() since it's more easily monkey-patched.
  		return require(ref);
  	};
  
  	const loadRequiredModule = async ref => {
  		// If the provider can load the module, assume it's a local file and not a
  		// dependency.
  		for (const provider of providers) {
  			if (provider.canLoad(ref)) {
  				return provider.load(ref, {requireFn: require});
  			}
  		}
  
  		// Try to load the module as a file, relative to the project directory.
  		// Match load() behavior.
  		const fullPath = path.resolve(projectDir, ref);
  		try {
  			for (const extension of extensionsToLoadAsModules) {
  				if (fullPath.endsWith(`.${extension}`)) {
  					return await import(pathToFileURL(fullPath)); // eslint-disable-line no-await-in-loop
  				}
  			}
  
  			return require(fullPath);
  		} catch (error) {
  			// If the module could not be found, assume it's not a file but a dependency.
  			if (error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_FOUND') {
  				return importFromProject(ref);
  			}
  
  			throw error;
  		}
  	};
  
  	let importFromProject = async ref => {
  		// Do not use the cacheDir since it's not guaranteed to be inside node_modules.
  		const avaCacheDir = path.join(projectDir, 'node_modules', '.cache', 'ava');
  		await mkdir(avaCacheDir, {recursive: true});
  		const stubPath = path.join(avaCacheDir, 'import-from-project.mjs');
  		await writeFileAtomic(stubPath, 'export const importFromProject = ref => import(ref);\n');
  		({importFromProject} = await import(pathToFileURL(stubPath)));
  		return importFromProject(ref);
  	};
  
  	try {
  		for await (const [ref, ...args] of (options.require ?? [])) {
  			const loadedModule = await loadRequiredModule(ref);
  
  			if (typeof loadedModule === 'function') { // CJS module
  				await loadedModule(...args);
  			} else if (typeof loadedModule.default === 'function') { // ES module, or exports.default from CJS
  				const {default: fn} = loadedModule;
  				await fn(...args);
  			}
  		}
  
  		if (options.debug?.port !== undefined && options.debug?.host !== undefined) {
  			// If an inspector was active when the main process started, and is
  			// already active for the worker process, do not open a new one.
  			const {default: inspector} = await import('node:inspector');
  			if (!options.debug.active || inspector.url() === undefined) {
  				inspector.open(options.debug.port, options.debug.host, true);
  			}
  
  			if (options.debug.break) {
  				debugger; // eslint-disable-line no-debugger
  			}
  		}
  
  		await load(testPath);
  
  		if (flags.loadedMain) {
  			// Unreference the channel if the test file required AVA. This stops it
  			// from keeping the event loop busy, which means the `beforeExit` event can be
  			// used to detect when tests stall.
  			channel.unref();
  		} else {
  			channel.send({type: 'missing-ava-import'});
  			forceExit();
  		}
  	} catch (error) {
  		channel.send({type: 'uncaught-exception', err: serializeError(error, {testFile: options.file})});
  		forceExit();
  	}
  };
  
  const onError = error => {
  	// There shouldn't be any errors, but if there are we may not have managed
  	// to bootstrap enough code to serialize them. Re-throw and let the process
  	// crash.
  	setImmediate(() => {
  		throw error;
  	});
  };
  
  let options;
  if (isRunningInThread) {
  	channel.send({type: 'starting'}); // AVA won't terminate the worker thread until it's seen this message.
  	({options} = workerData);
  	delete workerData.options; // Don't allow user code access.
  } else if (isRunningInChildProcess) {
  	channel.send({type: 'ready-for-options'});
  	options = await channel.options;
  }
  
  try {
  	await run(options);
  } catch (error) {
  	onError(error);
  }
  