Spaces:
Running
Running
| import { spawn, spawnSync } from 'node:child_process'; | |
| import { readdirSync } from 'node:fs'; | |
| import path from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| export const DEFAULT_TEST_TIMEOUT_MS = 60_000; | |
| export const DEFAULT_TEST_CONCURRENCY = 2; | |
| export function buildTestEnvironment(baseEnvironment = process.env) { | |
| return { | |
| ...baseEnvironment, | |
| NODE_ENV: 'test', | |
| APP_LOG_TEST_CONSOLE_MIRROR: 'false' | |
| }; | |
| } | |
| function collectTestFiles(root, directory, suffixes, files) { | |
| const currentDirectory = path.join(root, directory); | |
| for (const entry of readdirSync(currentDirectory, { withFileTypes: true })) { | |
| const entryPath = path.join(currentDirectory, entry.name); | |
| if (entry.isDirectory()) { | |
| collectTestFiles(root, path.join(directory, entry.name), suffixes, files); | |
| continue; | |
| } | |
| if (entry.isFile() && suffixes.some((suffix) => entry.name.endsWith(suffix))) { | |
| files.push(path.relative(root, entryPath)); | |
| } | |
| } | |
| } | |
| function validateTestScope(scope) { | |
| if (scope !== 'all' && scope !== 'scripts') { | |
| throw new Error(`未知测试范围:${scope}`); | |
| } | |
| } | |
| export function findTestFiles(root = process.cwd(), scope = 'all') { | |
| validateTestScope(scope); | |
| const files = []; | |
| if (scope === 'all') { | |
| collectTestFiles(root, 'src', ['.test.ts', '.test.tsx'], files); | |
| } | |
| collectTestFiles(root, 'scripts', ['.test.mjs'], files); | |
| return files.sort(); | |
| } | |
| function hasExplicitTestFile(nodeArguments) { | |
| return nodeArguments.some( | |
| (argument) => !argument.startsWith('-') && /\.test\.(?:[cm]?[jt]s|[jt]sx)$/.test(argument) | |
| ); | |
| } | |
| function hasTestConcurrencyArgument(nodeArguments) { | |
| return nodeArguments.some( | |
| (argument) => argument === '--test-concurrency' || argument.startsWith('--test-concurrency=') | |
| ); | |
| } | |
| function hasTestTimeoutArgument(nodeArguments) { | |
| return nodeArguments.some((argument) => argument === '--test-timeout' || argument.startsWith('--test-timeout=')); | |
| } | |
| export function parseTestRunnerArguments(arguments_) { | |
| const nodeArguments = [...arguments_]; | |
| const scope = nodeArguments[0] === 'scripts' ? nodeArguments.shift() : 'all'; | |
| return { scope, nodeArguments }; | |
| } | |
| export function selectTestFiles(root, scope, nodeArguments) { | |
| return hasExplicitTestFile(nodeArguments) ? [] : findTestFiles(root, scope); | |
| } | |
| export function buildTestArguments( | |
| supportsTestConcurrency, | |
| testFiles = findTestFiles(), | |
| nodeArguments = [], | |
| supportsTestTimeout = false | |
| ) { | |
| const defaultConcurrency = | |
| supportsTestConcurrency && !hasTestConcurrencyArgument(nodeArguments) | |
| ? [`--test-concurrency=${DEFAULT_TEST_CONCURRENCY}`] | |
| : []; | |
| const defaultTimeout = | |
| supportsTestTimeout && !hasTestTimeoutArgument(nodeArguments) ? [`--test-timeout=${DEFAULT_TEST_TIMEOUT_MS}`] : []; | |
| return ['--test', ...defaultConcurrency, ...defaultTimeout, '--import', 'tsx', ...nodeArguments, ...testFiles]; | |
| } | |
| function detectNodeOptionSupport(option) { | |
| const result = spawnSync(process.execPath, [option, '--help'], { stdio: 'ignore' }); | |
| if (result.error) throw result.error; | |
| if (result.status === 0) return true; | |
| if (result.status === 9) return false; | |
| throw new Error(`Node 参数能力探测失败(${option}),退出码 ${result.status ?? 'unknown'}`); | |
| } | |
| export function detectTestConcurrencySupport() { | |
| return detectNodeOptionSupport('--test-concurrency=1'); | |
| } | |
| export function detectTestTimeoutSupport() { | |
| return detectNodeOptionSupport('--test-timeout=1'); | |
| } | |
| export function runTests(scope = 'all', nodeArguments = []) { | |
| const supportsTestConcurrency = detectTestConcurrencySupport(); | |
| const supportsTestTimeout = detectTestTimeoutSupport(); | |
| const testFiles = selectTestFiles(process.cwd(), scope, nodeArguments); | |
| if (testFiles.length === 0 && !hasExplicitTestFile(nodeArguments)) throw new Error('未找到测试文件。'); | |
| if (!supportsTestConcurrency && !hasTestConcurrencyArgument(nodeArguments)) { | |
| console.error(`[test] Node ${process.version} 不支持 --test-concurrency,使用运行时默认并发。`); | |
| } | |
| return spawn(process.execPath, buildTestArguments(supportsTestConcurrency, testFiles, nodeArguments, supportsTestTimeout), { | |
| stdio: 'inherit', | |
| env: buildTestEnvironment() | |
| }); | |
| } | |
| function main() { | |
| let child; | |
| try { | |
| const { scope, nodeArguments } = parseTestRunnerArguments(process.argv.slice(2)); | |
| child = runTests(scope, nodeArguments); | |
| } catch (error) { | |
| console.error(`[test] 无法启动测试:${error instanceof Error ? error.message : String(error)}`); | |
| process.exitCode = 1; | |
| return; | |
| } | |
| child.once('error', (error) => { | |
| console.error(`[test] 无法启动测试:${error.message}`); | |
| process.exitCode = 1; | |
| }); | |
| child.once('exit', (code, signal) => { | |
| if (signal) { | |
| process.kill(process.pid, signal); | |
| return; | |
| } | |
| process.exitCode = code ?? 1; | |
| }); | |
| } | |
| if (path.resolve(process.argv[1] ?? '') === fileURLToPath(import.meta.url)) { | |
| main(); | |
| } | |