/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/internal/test_runner/runner.js
1 134 строки
35 KB
Jihwan
test_runner: fix env option validation
04 авг 2026, 21:32
Не верифицирован
04 авг 2026, 21:32
8465148
Код
Авторство
О чём код?
'use strict'; const { ArrayIsArray, ArrayPrototypeEvery, ArrayPrototypeFilter, ArrayPrototypeFind, ArrayPrototypeForEach, ArrayPrototypeIncludes, ArrayPrototypeJoin, ArrayPrototypeMap, ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeShift, ArrayPrototypeSome, ArrayPrototypeSort, MathMax, ObjectAssign, PromisePrototypeThen, PromiseWithResolvers, SafeMap, SafePromiseAll, SafePromiseAllReturnVoid, SafePromiseAllSettledReturnVoid, SafeSet, String, StringFromCharCode, StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeStartsWith, Symbol, TypedArrayPrototypeGetLength, TypedArrayPrototypeSubarray, } = primordials; const { spawn } = require('child_process'); const { finished } = require('internal/streams/end-of-stream'); const { availableParallelism } = require('os'); const { resolve, sep, isAbsolute } = require('path'); const { DefaultDeserializer, DefaultSerializer } = require('v8'); const { getOptionValue, getOptionsAsFlagsFromBinding } = require('internal/options'); const { Interface } = require('internal/readline/interface'); const { deserializeError } = require('internal/error_serdes'); const { Buffer } = require('buffer'); const { FilesWatcher } = require('internal/watch_mode/files_watcher'); const console = require('internal/console/global'); const { codes: { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_TEST_FAILURE, }, } = require('internal/errors'); const esmLoader = require('internal/modules/esm/loader'); const { validateArray, validateBoolean, validateFunction, validateObject, validateOneOf, validateInteger, validateUint32, validateString, validateStringArray, } = require('internal/validators'); const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector'); const { isRegExp } = require('internal/util/types'); const { pathToFileURL } = require('internal/url'); const { emitExperimentalWarning, kEmptyObject, } = require('internal/util'); const { kEmitMessage } = require('internal/test_runner/tests_stream'); const { createTestTree, startSubtestAfterBootstrap, } = require('internal/test_runner/harness'); const { kAborted, kCancelledByParent, kSubtestsFailed, kTestCodeFailure, kTestTimeoutFailure, Test, } = require('internal/test_runner/test'); const { FastBuffer } = require('internal/buffer'); const { createRandomSeed, convertStringToRegExp, countCompletedTest, kDefaultPattern, parseCommandLine, } = require('internal/test_runner/utils'); const { validateAndCanonicalizeTagFilter, } = require('internal/test_runner/tag_filter'); const { Glob } = require('internal/fs/glob'); const { once } = require('events'); const { validatePath } = require('internal/fs/utils'); const { loadPreloadModules } = require('internal/process/pre_execution'); const { triggerUncaughtException, exitCodes: { kGenericUserError }, } = internalBinding('errors'); let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => { debug = fn; }); const kIsolatedProcessName = Symbol('kIsolatedProcessName'); const kFilterArgs = [ '--test', '--experimental-test-coverage', '--test-randomize', '--watch', '--experimental-default-config-file', ]; const kFilterArgValues = [ '--experimental-test-tag-filter', '--test-reporter', '--test-reporter-destination', '--test-random-seed', '--experimental-config-file', ]; const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms']; const kCanceledTests = new SafeSet() .add(kCancelledByParent).add(kAborted).add(kTestTimeoutFailure); // Execution-ordered events are forwarded immediately, bypassing the // per-file declaration-order buffer. const kExecutionOrderedEvents = new SafeSet() .add('test:enqueue').add('test:dequeue').add('test:complete').add('test:log'); let kResistStopPropagation; // Worker ID pool management for concurrent test execution class WorkerIdPool { #nextId = 0; #maxConcurrency; constructor(maxConcurrency) { this.#maxConcurrency = maxConcurrency; } acquire() { const id = (this.#nextId++ % this.#maxConcurrency) + 1; return id; } } function createTestFileList(patterns, cwd) { const hasUserSuppliedPattern = patterns != null; if (!patterns || patterns.length === 0) { patterns = [kDefaultPattern]; } const glob = new Glob(patterns, { __proto__: null, cwd, exclude: (name) => name === 'node_modules', }); const results = glob.globSync(); if (hasUserSuppliedPattern && results.length === 0 && ArrayPrototypeEvery(glob.matchers, (m) => !m.hasMagic())) { console.error(`Could not find '${ArrayPrototypeJoin(patterns, ', ')}'`); process.exit(kGenericUserError); } return ArrayPrototypeSort(results); } function filterExecArgv(arg, i, arr) { return !ArrayPrototypeIncludes(kFilterArgs, arg) && !ArrayPrototypeSome(kFilterArgValues, (p) => { return arg === p || StringPrototypeStartsWith(arg, `${p}=`) || (p !== '--experimental-config-file' && i > 0 && arr[i - 1] === p); }); } function getRunArgs(path, { forceExit, inspectPort, testNamePatterns, testSkipPatterns, testTagFilterExpressions, only, hasFiles, testFiles, globPatterns, argv: suppliedArgs, execArgv, rerunFailuresFilePath, randomize, randomSeed, root: { timeout }, cwd }) { const processNodeOptions = getOptionsAsFlagsFromBinding(); const runArgs = ArrayPrototypeFilter(processNodeOptions, filterExecArgv); /** * Node supports V8 options passed via cli. * These options are being consumed by V8 and are not stored in nodeOptions. * * We need to propagate these options to the child processes manually. * * An example of such option are --allow-natives-syntax and --expose-gc */ const nodeOptionsSet = new SafeSet(processNodeOptions); const unknownProcessExecArgv = ArrayPrototypeFilter( process.execArgv, (arg, i, arr) => !nodeOptionsSet.has(arg) && filterExecArgv(arg, i, arr), ); ArrayPrototypePushApply(runArgs, unknownProcessExecArgv); if (forceExit === true) { ArrayPrototypePush(runArgs, '--test-force-exit'); } if (isUsingInspector()) { ArrayPrototypePush(runArgs, `--inspect-port=${getInspectPort(inspectPort)}`); } if (testNamePatterns != null) { ArrayPrototypeForEach(testNamePatterns, (pattern) => ArrayPrototypePush(runArgs, `--test-name-pattern=${pattern}`)); } if (testSkipPatterns != null) { ArrayPrototypeForEach(testSkipPatterns, (pattern) => ArrayPrototypePush(runArgs, `--test-skip-pattern=${pattern}`)); } if (testTagFilterExpressions != null) { ArrayPrototypeForEach(testTagFilterExpressions, (value) => ArrayPrototypePush(runArgs, `--experimental-test-tag-filter=${value}`)); } if (only === true) { ArrayPrototypePush(runArgs, '--test-only'); } if (timeout != null) { ArrayPrototypePush(runArgs, `--test-timeout=${timeout}`); } if (rerunFailuresFilePath) { ArrayPrototypePush(runArgs, `--test-rerun-failures=${rerunFailuresFilePath}`); } if (randomize) { ArrayPrototypePush(runArgs, '--test-randomize'); } if (randomSeed != null) { ArrayPrototypePush(runArgs, `--test-random-seed=${randomSeed}`); } ArrayPrototypePushApply(runArgs, execArgv); if (path === kIsolatedProcessName) { ArrayPrototypePush(runArgs, '--test'); ArrayPrototypePush(runArgs, '--test-isolation=none'); ArrayPrototypePushApply(runArgs, hasFiles ? testFiles : globPatterns ?? []); } else { ArrayPrototypePush(runArgs, path); } ArrayPrototypePushApply(runArgs, suppliedArgs); return runArgs; } const serializer = new DefaultSerializer(); serializer.writeHeader(); const v8Header = serializer.releaseBuffer(); const kV8HeaderLength = TypedArrayPrototypeGetLength(v8Header); const kSerializedSizeHeader = 4 + kV8HeaderLength; class FileTest extends Test { // This class maintains two buffers: #reportBuffer = []; // Parsed items waiting for this.isClearToSend() #rawBuffer = []; // Raw data waiting to be parsed #rawBufferSize = 0; #reportedChildren = 0; #pendingPartialV8Header = false; failedSubtests = false; constructor(options) { super(options); this.loc ??= { __proto__: null, line: 1, column: 1, file: resolve(this.name), }; this.timeout = null; } #skipReporting() { return this.#reportedChildren > 0 && (!this.error || this.error.failureType === kSubtestsFailed); } #checkNestedComment(comment) { const firstSpaceIndex = StringPrototypeIndexOf(comment, ' '); if (firstSpaceIndex === -1) return false; const secondSpaceIndex = StringPrototypeIndexOf(comment, ' ', firstSpaceIndex + 1); return secondSpaceIndex === -1 && ArrayPrototypeIncludes(kDiagnosticsFilterArgs, StringPrototypeSlice(comment, 0, firstSpaceIndex)); } #handleReportItem(item) { // The name is empty when a single child process runs all test files. if (this.name !== '') { item.data.entryFile = this.loc.file; } const isTopLevel = item.data.nesting === 0; if (isTopLevel) { if (item.type === 'test:plan' && this.#skipReporting()) { return; } if (item.type === 'test:diagnostic' && this.#checkNestedComment(item.data.message)) { return; } } if (item.data.details?.error) { item.data.details.error = deserializeError(item.data.details.error); } if (item.type === 'test:pass' || item.type === 'test:fail') { item.data.testNumber = isTopLevel ? (this.root.harness.counters.topLevel + 1) : item.data.testNumber; countCompletedTest({ __proto__: null, name: item.data.name, finished: true, skipped: item.data.skip !== undefined, isTodo: item.data.todo !== undefined, passed: item.type === 'test:pass', cancelled: kCanceledTests.has(item.data.details?.error?.failureType), nesting: item.data.nesting, reportedType: item.data.details?.type, }, this.root.harness); } this.reporter[kEmitMessage](item.type, item.data); } #accumulateReportItem(item) { if (item.type !== 'test:pass' && item.type !== 'test:fail') { return; } this.#reportedChildren++; if (item.data.nesting === 0 && item.type === 'test:fail') { this.failedSubtests = true; } } #drainReportBuffer() { if (this.#reportBuffer.length > 0) { ArrayPrototypeForEach(this.#reportBuffer, (ast) => this.#handleReportItem(ast)); this.#reportBuffer = []; } } addToReport(item) { if (kExecutionOrderedEvents.has(item.type)) { this.#handleReportItem(item); return; } this.#accumulateReportItem(item); if (!this.isClearToSend()) { ArrayPrototypePush(this.#reportBuffer, item); return; } this.#drainReportBuffer(); this.#handleReportItem(item); } reportStarted() {} drain() { this.#drainRawBuffer(); this.#drainReportBuffer(); } report() { this.drain(); const skipReporting = this.#skipReporting(); if (!skipReporting) { super.reportStarted(); super.report(); } } parseMessage(readData) { let dataLength = TypedArrayPrototypeGetLength(readData); if (this.#pendingPartialV8Header) { readData = Buffer.concat([TypedArrayPrototypeSubarray(v8Header, 0, 1), readData]); dataLength = TypedArrayPrototypeGetLength(readData); this.#pendingPartialV8Header = false; } if (dataLength === 0) return; const partialV8Header = readData[dataLength - 1] === v8Header[0]; if (partialV8Header) { // This will break if v8Header length (2 bytes) is changed. // However it is covered by tests. readData = TypedArrayPrototypeSubarray(readData, 0, dataLength - 1); dataLength--; } if (dataLength > 0) { if (this.#rawBuffer[0] && TypedArrayPrototypeGetLength(this.#rawBuffer[0]) < kSerializedSizeHeader) { this.#rawBuffer[0] = Buffer.concat([this.#rawBuffer[0], readData]); } else { ArrayPrototypePush(this.#rawBuffer, readData); } this.#rawBufferSize += dataLength; this.#processRawBuffer(); } if (partialV8Header) { this.#pendingPartialV8Header = true; } } #drainRawBuffer() { if (this.#pendingPartialV8Header) { ArrayPrototypePush(this.#rawBuffer, TypedArrayPrototypeSubarray(v8Header, 0, 1)); this.#rawBufferSize++; this.#pendingPartialV8Header = false; } while (this.#rawBuffer.length > 0) { const prevBufferLength = this.#rawBuffer.length; const prevBufferSize = this.#rawBufferSize; this.#processRawBuffer(); if (this.#rawBuffer.length === prevBufferLength && this.#rawBufferSize === prevBufferSize) { const bufferHead = this.#rawBuffer[0]; this.addToReport({ __proto__: null, type: 'test:stdout', data: { __proto__: null, file: this.name, message: StringFromCharCode(bufferHead[0]), }, }); if (TypedArrayPrototypeGetLength(bufferHead) === 1) { ArrayPrototypeShift(this.#rawBuffer); } else { this.#rawBuffer[0] = TypedArrayPrototypeSubarray(bufferHead, 1); } this.#rawBufferSize--; } } } #processRawBuffer() { // This method is called when it is known that there is at least one message let bufferHead = this.#rawBuffer[0]; let headerIndex = bufferHead.indexOf(v8Header); let nonSerialized = new FastBuffer(); while (bufferHead && headerIndex !== 0) { const nonSerializedData = headerIndex === -1 ? bufferHead : bufferHead.slice(0, headerIndex); nonSerialized = Buffer.concat([nonSerialized, nonSerializedData]); this.#rawBufferSize -= TypedArrayPrototypeGetLength(nonSerializedData); if (headerIndex === -1) { ArrayPrototypeShift(this.#rawBuffer); } else { this.#rawBuffer[0] = TypedArrayPrototypeSubarray(bufferHead, headerIndex); } bufferHead = this.#rawBuffer[0]; headerIndex = bufferHead?.indexOf(v8Header); } if (TypedArrayPrototypeGetLength(nonSerialized) > 0) { this.addToReport({ __proto__: null, type: 'test:stdout', data: { __proto__: null, file: this.name, message: nonSerialized.toString('utf-8') }, }); } while (bufferHead?.length >= kSerializedSizeHeader) { // We call `readUInt32BE` manually here, because this is faster than first converting // it to a buffer and using `readUInt32BE` on that. const fullMessageSize = (( bufferHead[kV8HeaderLength] << 24 | bufferHead[kV8HeaderLength + 1] << 16 | bufferHead[kV8HeaderLength + 2] << 8 | bufferHead[kV8HeaderLength + 3] ) >>> 0) + kSerializedSizeHeader; if (this.#rawBufferSize < fullMessageSize) break; const concatenatedBuffer = this.#rawBuffer.length === 1 ? this.#rawBuffer[0] : Buffer.concat(this.#rawBuffer, this.#rawBufferSize); const deserializer = new DefaultDeserializer( TypedArrayPrototypeSubarray(concatenatedBuffer, kSerializedSizeHeader, fullMessageSize), ); bufferHead = TypedArrayPrototypeSubarray(concatenatedBuffer, fullMessageSize); this.#rawBufferSize = TypedArrayPrototypeGetLength(bufferHead); this.#rawBuffer = this.#rawBufferSize !== 0 ? [bufferHead] : []; deserializer.readHeader(); const item = deserializer.readValue(); this.addToReport(item); } } } function runTestFile(path, filesWatcher, opts) { const watchMode = filesWatcher != null; const testPath = path === kIsolatedProcessName ? '' : path; const testOpts = { __proto__: null, signal: opts.signal }; const subtest = opts.root.createSubtest(FileTest, testPath, testOpts, async (t) => { const args = getRunArgs(path, opts); const stdio = ['pipe', 'pipe', 'pipe']; const env = { __proto__: null, NODE_TEST_CONTEXT: 'child-v8', ...(opts.env || process.env) }; // Acquire a worker ID from the pool for process isolation mode let workerId; if (opts.workerIdPool) { workerId = opts.workerIdPool.acquire(); env.NODE_TEST_WORKER_ID = String(workerId); debug('Assigned worker ID %d to test file: %s', workerId, path); } if (watchMode) { stdio.push('ipc'); env.WATCH_REPORT_DEPENDENCIES = '1'; } if (opts.root.harness.shouldColorizeTestFiles) { env.FORCE_COLOR = '1'; } const child = spawn( process.execPath, args, { __proto__: null, signal: t.signal, encoding: 'utf8', env, stdio, cwd: opts.cwd, }, ); if (watchMode) { filesWatcher.runningProcesses.set(path, child); filesWatcher.watcher.watchChildProcessModules(child, path); } let err; child.on('error', (error) => { err = error; }); child.stdout.on('data', (data) => { subtest.parseMessage(data); }); const rl = new Interface({ __proto__: null, input: child.stderr }); rl.on('line', (line) => { if (isInspectorMessage(line)) { process.stderr.write(line + '\n'); return; } // stderr cannot be treated as TAP, per the spec. However, we want to // surface stderr lines to improve the DX. Inject each line into the // test output as an unknown token as if it came from the TAP parser. subtest.addToReport({ __proto__: null, type: 'test:stderr', data: { __proto__: null, file: path, message: line + '\n' }, }); }); const { 0: { 0: code, 1: signal } } = await SafePromiseAll([ once(child, 'exit', { __proto__: null, signal: t.signal }), finished(child.stdout, { __proto__: null, signal: t.signal }), ]); // Close readline interface to prevent memory leak rl.close(); if (watchMode) { filesWatcher.runningProcesses.delete(path); filesWatcher.runningSubtests.delete(path); (async () => { try { await subTestEnded; } finally { if (filesWatcher.runningSubtests.size === 0) { opts.root.reporter[kEmitMessage]('test:watch:drained'); opts.root.postRun(); } } })(); } if (code !== 0 || signal !== null) { if (!err) { const failureType = subtest.failedSubtests ? kSubtestsFailed : kTestCodeFailure; err = ObjectAssign(new ERR_TEST_FAILURE('test failed', failureType), { __proto__: null, exitCode: code, signal: signal, // The stack will not be useful since the failures came from tests // in a child process. stack: undefined, }); } throw err; } }); const subTestEnded = subtest.start(); return subTestEnded; } function watchFiles(testFiles, opts) { const runningProcesses = new SafeMap(); const runningSubtests = new SafeMap(); const watcherMode = opts.hasFiles ? 'filter' : 'all'; const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: watcherMode, signal: opts.signal }); if (!opts.hasFiles) { watcher.watchPath(opts.cwd); } const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests }; opts.root.harness.watching = true; async function restartTestFile(file) { const runningProcess = runningProcesses.get(file); if (runningProcess) { runningProcess.kill(); await once(runningProcess, 'exit'); } if (!runningSubtests.size) { // Reset the topLevel counter opts.root.harness.counters.topLevel = 0; } await runningSubtests.get(file); runningSubtests.set(file, runTestFile(file, filesWatcher, opts)); } // Watch for changes in current filtered files const onChanged = ({ owners, eventType }) => { if (!opts.hasFiles && (eventType === 'rename' || eventType === 'change')) { const updatedTestFiles = createTestFileList(opts.globPatterns, opts.cwd); const newFileName = ArrayPrototypeFind(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x)); const previousFileName = ArrayPrototypeFind(testFiles, (x) => !ArrayPrototypeIncludes(updatedTestFiles, x)); testFiles = updatedTestFiles; // When file renamed (created / deleted) we need to update the watcher if (newFileName) { owners = new SafeSet().add(newFileName); const resolveFileName = isAbsolute(newFileName) ? newFileName : resolve(opts.cwd, newFileName); watcher.filterFile(resolveFileName, owners); } if (!newFileName && previousFileName) { return; // Avoid rerunning files when file deleted } } // Reset the root start time to recalculate the duration // of the run opts.root.clearExecutionTime(); opts.root.reporter[kEmitMessage]('test:watch:restarted'); // Restart test files if (opts.isolation === 'none') { PromisePrototypeThen(restartTestFile(kIsolatedProcessName), undefined, (error) => { triggerUncaughtException(error, true /* fromPromise */); }); } else { watcher.unfilterFilesOwnedBy(owners); PromisePrototypeThen(SafePromiseAllReturnVoid(testFiles, async (file) => { if (!owners.has(file)) { return; } await restartTestFile(file); }, undefined, (error) => { triggerUncaughtException(error, true /* fromPromise */); })); } }; watcher.on('changed', onChanged); // Cleanup function to remove event listener and prevent memory leak const cleanup = () => { watcher.removeListener('changed', onChanged); opts.root.harness.watching = false; opts.root.postRun(); }; if (opts.signal) { kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; opts.signal.addEventListener( 'abort', cleanup, { __proto__: null, once: true, [kResistStopPropagation]: true }, ); } // Expose cleanup method for proper resource management filesWatcher.cleanup = cleanup; return filesWatcher; } function run(options = kEmptyObject) { validateObject(options, 'options'); let { testNamePatterns, testSkipPatterns, testTagFilters, shard, coverageExcludeGlobs, coverageIncludeGlobs, } = options; const { concurrency, timeout, signal, files, forceExit, inspectPort, isolation = 'process', watch, setup, globalSetupPath, only, globPatterns, coverage = false, coverageIncludeAll = false, lineCoverage = 0, branchCoverage = 0, functionCoverage = 0, randomize: suppliedRandomize, randomSeed: suppliedRandomSeed, execArgv = [], argv = [], cwd = process.cwd(), rerunFailuresFilePath, env, } = options; if (files != null) { validateArray(files, 'options.files'); } if (watch != null) { validateBoolean(watch, 'options.watch'); } if (forceExit != null) { validateBoolean(forceExit, 'options.forceExit'); if (forceExit && watch) { throw new ERR_INVALID_ARG_VALUE( 'options.forceExit', watch, 'is not supported with watch mode', ); } } if (only != null) { validateBoolean(only, 'options.only'); } if (globPatterns != null) { validateArray(globPatterns, 'options.globPatterns'); } if (suppliedRandomize != null) { validateBoolean(suppliedRandomize, 'options.randomize'); } if (suppliedRandomSeed != null) { validateUint32(suppliedRandomSeed, 'options.randomSeed'); } let randomize = suppliedRandomize; let randomSeed = suppliedRandomSeed; if (randomSeed != null) { randomize = true; } if (watch) { if (randomSeed != null) { throw new ERR_INVALID_ARG_VALUE( 'options.randomSeed', randomSeed, 'is not supported with watch mode', ); } if (randomize) { throw new ERR_INVALID_ARG_VALUE( 'options.randomize', randomize, 'is not supported with watch mode', ); } } if (rerunFailuresFilePath) { validatePath(rerunFailuresFilePath, 'options.rerunFailuresFilePath'); // TODO(pmarchini): Support rerun-failures with randomization by // persisting the randomization seed in the rerun state file. if (randomSeed != null) { throw new ERR_INVALID_ARG_VALUE( 'options.randomSeed', randomSeed, 'is not supported with rerun failures mode', ); } if (randomize) { throw new ERR_INVALID_ARG_VALUE( 'options.randomize', randomize, 'is not supported with rerun failures mode', ); } } if (randomize) { randomSeed ??= createRandomSeed(); } validateString(cwd, 'options.cwd'); if (globPatterns?.length > 0 && files?.length > 0) { throw new ERR_INVALID_ARG_VALUE( 'options.globPatterns', globPatterns, 'is not supported when specifying \'options.files\'', ); } if (shard != null) { validateObject(shard, 'options.shard'); // Avoid re-evaluating the shard object in case it's a getter shard = { __proto__: null, index: shard.index, total: shard.total }; validateInteger(shard.total, 'options.shard.total', 1); validateInteger(shard.index, 'options.shard.index', 1, shard.total); if (watch) { throw new ERR_INVALID_ARG_VALUE('options.shard', watch, 'shards not supported with watch mode'); } } if (setup != null) { validateFunction(setup, 'options.setup'); } if (testNamePatterns != null) { if (!ArrayIsArray(testNamePatterns)) { testNamePatterns = [testNamePatterns]; } testNamePatterns = ArrayPrototypeMap(testNamePatterns, (value, i) => { if (isRegExp(value)) { return value; } const name = `options.testNamePatterns[${i}]`; if (typeof value === 'string') { return convertStringToRegExp(value, name); } throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp'], value); }); } if (testSkipPatterns != null) { if (!ArrayIsArray(testSkipPatterns)) { testSkipPatterns = [testSkipPatterns]; } testSkipPatterns = ArrayPrototypeMap(testSkipPatterns, (value, i) => { if (isRegExp(value)) { return value; } const name = `options.testSkipPatterns[${i}]`; if (typeof value === 'string') { return convertStringToRegExp(value, name); } throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp'], value); }); } let testTagFilterExpressions = null; if (testTagFilters != null) { if (!ArrayIsArray(testTagFilters)) { testTagFilters = [testTagFilters]; } if (testTagFilters.length === 0) { testTagFilters = null; } else { emitExperimentalWarning('Test tags'); testTagFilters = ArrayPrototypeMap(testTagFilters, (value, i) => ( validateAndCanonicalizeTagFilter(value, `options.testTagFilters[${i}]`) )); testTagFilterExpressions = testTagFilters; } } testTagFilterExpressions ??= options.testTagFilterExpressions; validateOneOf(isolation, 'options.isolation', ['process', 'none']); validateBoolean(coverage, 'options.coverage'); validateBoolean(coverageIncludeAll, 'options.coverageIncludeAll'); if (coverageExcludeGlobs != null) { if (!ArrayIsArray(coverageExcludeGlobs)) { coverageExcludeGlobs = [coverageExcludeGlobs]; } validateStringArray(coverageExcludeGlobs, 'options.coverageExcludeGlobs'); } else if (coverage) { coverageExcludeGlobs = [kDefaultPattern]; } if (coverageIncludeGlobs != null) { if (!ArrayIsArray(coverageIncludeGlobs)) { coverageIncludeGlobs = [coverageIncludeGlobs]; } validateStringArray(coverageIncludeGlobs, 'options.coverageIncludeGlobs'); } validateInteger(lineCoverage, 'options.lineCoverage', 0, 100); validateInteger(branchCoverage, 'options.branchCoverage', 0, 100); validateInteger(functionCoverage, 'options.functionCoverage', 0, 100); validateStringArray(argv, 'options.argv'); validateStringArray(execArgv, 'options.execArgv'); if (globalSetupPath != null) { validatePath(globalSetupPath, 'options.globalSetupPath'); } if (env != null) { validateObject(env, 'options.env'); if (isolation === 'none') { throw new ERR_INVALID_ARG_VALUE('options.env', env, 'is not supported with isolation=\'none\''); } } const rootTestOptions = { __proto__: null, concurrency, timeout, signal }; const globalOptions = { __proto__: null, // parseCommandLine() should not be used here. However, The existing run() // behavior has relied on it, so removing it must be done in a semver major. ...parseCommandLine(), setup, // This line can be removed when parseCommandLine() is removed here. coverage, coverageIncludeAll, coverageExcludeGlobs, coverageIncludeGlobs, rerunFailuresFilePath, lineCoverage: lineCoverage, branchCoverage: branchCoverage, functionCoverage: functionCoverage, cwd, globalSetupPath, randomize, randomSeed, testTagFilters, }; const root = createTestTree(rootTestOptions, globalOptions); let testFiles = files ?? createTestFileList(globPatterns, cwd); const { isTestRunner } = globalOptions; if (randomize) { root.diagnostic(`Randomized test order seed: ${randomSeed}`); } if (shard) { testFiles = ArrayPrototypeFilter(testFiles, (_, index) => index % shard.total === shard.index - 1); } let teardown; let postRun; let filesWatcher; let runFiles; // Create worker ID pool for concurrent test execution. // Use concurrency from globalOptions which has been processed by parseCommandLine(). const effectiveConcurrency = globalOptions.concurrency ?? concurrency; let maxConcurrency = 1; if (effectiveConcurrency === true) { maxConcurrency = MathMax(availableParallelism() - 1, 1); } else if (typeof effectiveConcurrency === 'number') { maxConcurrency = effectiveConcurrency; } const workerIdPool = new WorkerIdPool(maxConcurrency); debug( 'Created worker ID pool with max concurrency: %d, ' + 'effectiveConcurrency: %s, testFiles: %d', maxConcurrency, effectiveConcurrency, testFiles.length, ); const opts = { __proto__: null, root, signal, inspectPort, testNamePatterns, testSkipPatterns, testTagFilters, testTagFilterExpressions, hasFiles: files != null, globPatterns, only, forceExit, cwd, isolation, argv, execArgv, rerunFailuresFilePath, env, workerIdPool: isolation === 'process' ? workerIdPool : null, randomize, randomSeed, testFiles, }; if (isolation === 'process') { if (process.env.NODE_TEST_CONTEXT !== undefined) { process.emitWarning('node:test run() is being called recursively within a test file. skipping running files.'); root.postRun(); return root.reporter; } if (watch) { filesWatcher = watchFiles(testFiles, opts); } else { postRun = () => root.postRun(); teardown = () => root.harness.teardown(); } runFiles = () => { root.harness.bootstrapPromise = null; root.harness.buildPromise = null; return SafePromiseAllSettledReturnVoid(testFiles, (path) => { const subtest = runTestFile(path, filesWatcher, opts); filesWatcher?.runningSubtests.set(path, subtest); return subtest; }); }; } else if (isolation === 'none') { // For isolation=none, set worker ID to 1 in the current process process.env.NODE_TEST_WORKER_ID = '1'; debug('Set NODE_TEST_WORKER_ID=1 for isolation=none'); if (watch) { const absoluteTestFiles = ArrayPrototypeMap(testFiles, (file) => (isAbsolute(file) ? file : resolve(cwd, file))); filesWatcher = watchFiles(absoluteTestFiles, opts); runFiles = async () => { root.harness.bootstrapPromise = null; root.harness.buildPromise = null; const subtest = runTestFile(kIsolatedProcessName, filesWatcher, opts); filesWatcher?.runningSubtests.set(kIsolatedProcessName, subtest); return subtest; }; } else { runFiles = async () => { const { promise, resolve: finishBootstrap } = PromiseWithResolvers(); await root.runInAsyncScope(async () => { const parentURL = pathToFileURL(cwd + sep).href; const cascadedLoader = esmLoader.getOrInitializeCascadedLoader(); let topLevelTestCount = 0; root.harness.bootstrapPromise = root.harness.bootstrapPromise ? SafePromiseAllReturnVoid([root.harness.bootstrapPromise, promise]) : promise; // We need to setup the user modules in the test runner if we are running with // --test-isolation=none and --test in order to avoid loading the user modules // BEFORE the creation of the root test (that would cause them to get lost). if (isTestRunner) { // If we are not coming from the test runner entry point, the user-required and imported // modules have already been loaded. // Since it's possible to delete modules from require.cache, a CommonJS module // could otherwise be executed twice. loadPreloadModules(); } const userImports = getOptionValue('--import'); for (let i = 0; i < userImports.length; i++) { await cascadedLoader.import(userImports[i], parentURL, kEmptyObject); } for (let i = 0; i < testFiles.length; ++i) { const testFile = testFiles[i]; const fileURL = pathToFileURL(resolve(cwd, testFile)); const parent = i === 0 ? undefined : parentURL; let threw = false; let importError; root.entryFile = resolve(testFile); debug('loading test file:', fileURL.href); try { await cascadedLoader.import(fileURL, parent, { __proto__: null }); } catch (err) { threw = true; importError = err; } debug( 'loaded "%s": top level test count before = %d and after = %d', testFile, topLevelTestCount, root.subtests.length, ); if (topLevelTestCount === root.subtests.length) { // This file had no tests in it. Add the placeholder test. const subtest = root.createSubtest(Test, testFile, kEmptyObject, undefined, { __proto__: null, loc: [1, 1, resolve(testFile)], }); if (threw) { subtest.fail(importError); } startSubtestAfterBootstrap(subtest); } topLevelTestCount = root.subtests.length; } }); debug('beginning test execution'); root.entryFile = null; finishBootstrap(); return root.processPendingSubtests(); }; } } const runChain = async () => { if (root.harness?.bootstrapPromise) { await root.harness.bootstrapPromise; } if (typeof setup === 'function') { await setup(root.reporter); } await runFiles(); postRun?.(); teardown?.(); }; runChain(); return root.reporter; } module.exports = { FileTest, // Exported for tests only run, };