From 93aa102c0a5a929d2ac3772c328bbcc30b95b52a Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 11 Jul 2026 20:49:32 +0800 Subject: [PATCH 1/2] test: add shared CLI test helpers --- packages/rstack/test/helpers/cli.ts | 2 +- packages/rstack/test/helpers/cliTest.ts | 52 +++++++++++++++++++++- packages/rstack/test/helpers/utils.ts | 58 +++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 2 deletions(-) diff --git a/packages/rstack/test/helpers/cli.ts b/packages/rstack/test/helpers/cli.ts index ac13e6b..9693df5 100644 --- a/packages/rstack/test/helpers/cli.ts +++ b/packages/rstack/test/helpers/cli.ts @@ -2,7 +2,7 @@ import { type ExecSyncOptions, execSync } from 'node:child_process'; import path from 'node:path'; import type { LogHelper } from './logs.ts'; -const RSTACK_BIN_PATH = path.join(import.meta.dirname, '../../bin/rs.js'); +export const RSTACK_BIN_PATH: string = path.join(import.meta.dirname, '../../bin/rs.js'); export type ExecCliOptions = ExecSyncOptions & { logHelper?: LogHelper; diff --git a/packages/rstack/test/helpers/cliTest.ts b/packages/rstack/test/helpers/cliTest.ts index 25e8876..87d780f 100644 --- a/packages/rstack/test/helpers/cliTest.ts +++ b/packages/rstack/test/helpers/cliTest.ts @@ -1,11 +1,21 @@ +import { type ChildProcess, type ExecOptions, exec as nodeExec } from 'node:child_process'; import path from 'node:path'; import { test as baseTest } from 'rstack/test'; -import { execCli as baseExecCli, type ExecCli } from './cli.ts'; +import { execCli as baseExecCli, type ExecCli, RSTACK_BIN_PATH } from './cli.ts'; import { type ExtendedLogHelper, proxyConsole } from './logs.ts'; +type Exec = ( + command: string, + options?: ExecOptions, +) => { + childProcess: ChildProcess; +}; + export type CliTestFixtures = { cwd: string; + exec: Exec; execCli: ExecCli; + execCliAsync: Exec; logHelper: ExtendedLogHelper; }; @@ -20,6 +30,15 @@ function makeBox(title: string) { }; } +const setupExecOptions = (options: T, cwd: string): T => { + // inherit process.env from current process + const { NODE_ENV: _, ...restEnv } = process.env; + options.env ||= {}; + options.env = { ...restEnv, ...options.env }; + options.cwd ||= cwd; + return options; +}; + export const test: CliTest = baseTest.extend({ cwd: async ({ expect }, use) => { const { testPath } = expect.getState(); @@ -55,4 +74,35 @@ export const test: CliTest = baseTest.extend({ await use(execCli); }, + exec: async ({ cwd, logHelper }, use) => { + let close: (() => void) | undefined; + + const exec: Exec = (command, options = {}) => { + const childProcess = nodeExec(command, setupExecOptions(options, cwd)); + + const onData = (data: Buffer) => { + logHelper.addLog(data.toString()); + }; + + childProcess.stdout?.on('data', onData); + childProcess.stderr?.on('data', onData); + + close = () => { + childProcess.stdout?.off('data', onData); + childProcess.stderr?.off('data', onData); + childProcess.kill(); + }; + + return { childProcess }; + }; + + await use(exec); + close?.(); + }, + execCliAsync: async ({ exec }, use) => { + const execCliAsync: Exec = (command, options = {}) => { + return exec(`node ${RSTACK_BIN_PATH} ${command}`, options); + }; + await use(execCliAsync); + }, }); diff --git a/packages/rstack/test/helpers/utils.ts b/packages/rstack/test/helpers/utils.ts index fecbeab..933d2a9 100644 --- a/packages/rstack/test/helpers/utils.ts +++ b/packages/rstack/test/helpers/utils.ts @@ -1 +1,59 @@ +import fs from 'node:fs'; +import net from 'node:net'; +import { expect } from 'rstack/test'; + export const toPosixPath: (filePath: string) => string = (filePath) => filePath.replace(/\\/g, '/'); + +function isPortAvailable(port: number) { + try { + const server = net.createServer().listen(port); + return new Promise((resolve) => { + server.on('listening', () => { + server.close(); + resolve(true); + }); + server.on('error', () => { + resolve(false); + }); + }); + } catch { + return false; + } +} + +const portMap = new Map(); + +/** + * Get a random port + * Available port ranges: 1024 ~ 65535 + * `10080` is not available on macOS CI, `> 50000` get 'permission denied' on Windows. + * so we use `15000` ~ `45000`. + */ +export async function getRandomPort( + defaultPort: number = Math.ceil(Math.random() * 30000) + 15000, +): Promise { + let port = defaultPort; + while (true) { + if (!portMap.get(port) && (await isPortAvailable(port))) { + portMap.set(port, 1); + return port; + } + port++; + } +} + +/** + * Expect a file to exist + */ +export const expectFile = (dir: string): Promise => + expectPoll(() => fs.existsSync(dir)).toBeTruthy(); + +/** + * A faster `expect.poll` + */ +export const expectPoll: typeof expect.poll = (fn) => { + return expect.poll(fn, { + interval: 50, + timeout: 5_000, + }); +}; From 9777e6f37aa6be6482ac51ed3d801b6d76d43c8b Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 11 Jul 2026 21:50:30 +0800 Subject: [PATCH 2/2] test: sync Rsbuild helper fixes --- packages/rstack/test/helpers/cliTest.ts | 25 +++++++++++++++---------- packages/rstack/test/helpers/utils.ts | 10 ++++++---- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/rstack/test/helpers/cliTest.ts b/packages/rstack/test/helpers/cliTest.ts index 87d780f..df0ba85 100644 --- a/packages/rstack/test/helpers/cliTest.ts +++ b/packages/rstack/test/helpers/cliTest.ts @@ -1,4 +1,4 @@ -import { type ChildProcess, type ExecOptions, exec as nodeExec } from 'node:child_process'; +import { type ChildProcess, type SpawnOptions, spawn as nodeSpawn } from 'node:child_process'; import path from 'node:path'; import { test as baseTest } from 'rstack/test'; import { execCli as baseExecCli, type ExecCli, RSTACK_BIN_PATH } from './cli.ts'; @@ -6,7 +6,7 @@ import { type ExtendedLogHelper, proxyConsole } from './logs.ts'; type Exec = ( command: string, - options?: ExecOptions, + options?: SpawnOptions, ) => { childProcess: ChildProcess; }; @@ -30,7 +30,7 @@ function makeBox(title: string) { }; } -const setupExecOptions = (options: T, cwd: string): T => { +const setupExecOptions = (options: T, cwd: string): T => { // inherit process.env from current process const { NODE_ENV: _, ...restEnv } = process.env; options.env ||= {}; @@ -75,10 +75,10 @@ export const test: CliTest = baseTest.extend({ await use(execCli); }, exec: async ({ cwd, logHelper }, use) => { - let close: (() => void) | undefined; + const closes: Array<() => void> = []; const exec: Exec = (command, options = {}) => { - const childProcess = nodeExec(command, setupExecOptions(options, cwd)); + const childProcess = nodeSpawn(command, setupExecOptions({ shell: true, ...options }, cwd)); const onData = (data: Buffer) => { logHelper.addLog(data.toString()); @@ -87,21 +87,26 @@ export const test: CliTest = baseTest.extend({ childProcess.stdout?.on('data', onData); childProcess.stderr?.on('data', onData); - close = () => { + closes.push(() => { childProcess.stdout?.off('data', onData); childProcess.stderr?.off('data', onData); childProcess.kill(); - }; + }); return { childProcess }; }; - await use(exec); - close?.(); + try { + await use(exec); + } finally { + for (const close of closes) { + close(); + } + } }, execCliAsync: async ({ exec }, use) => { const execCliAsync: Exec = (command, options = {}) => { - return exec(`node ${RSTACK_BIN_PATH} ${command}`, options); + return exec(`node "${RSTACK_BIN_PATH}" ${command}`, options); }; await use(execCliAsync); }, diff --git a/packages/rstack/test/helpers/utils.ts b/packages/rstack/test/helpers/utils.ts index 933d2a9..e50b1fa 100644 --- a/packages/rstack/test/helpers/utils.ts +++ b/packages/rstack/test/helpers/utils.ts @@ -9,15 +9,16 @@ function isPortAvailable(port: number) { const server = net.createServer().listen(port); return new Promise((resolve) => { server.on('listening', () => { - server.close(); - resolve(true); + server.close(() => { + resolve(true); + }); }); server.on('error', () => { resolve(false); }); }); } catch { - return false; + return Promise.resolve(false); } } @@ -33,13 +34,14 @@ export async function getRandomPort( defaultPort: number = Math.ceil(Math.random() * 30000) + 15000, ): Promise { let port = defaultPort; - while (true) { + while (port <= 65535) { if (!portMap.get(port) && (await isPortAvailable(port))) { portMap.set(port, 1); return port; } port++; } + throw new Error('No available ports found in the valid range.'); } /**