Skip to content

Commit ab94b1f

Browse files
committed
test: CI 工作流(Node 18/20/22 矩阵)+ 测试运行器修复
- 工作流:push master/release + PR + 手动触发;Node 18/20/22 矩阵; concurrency 取消同分支旧运行;timeout 10min - 测试助手修复(两处,都会污染环境或造成偶发失败): 1. allocPort 原用 pid 派生端口区间,node --test 各文件并行时 pid 取模会撞桶(如 pid 100 与 600 同桶)-> 偶发 EADDRINUSE。 改为让内核分配(listen 0)后立刻释放。 2. startProxy 用 mkdtempSync 建临时工作目录却从不删除 -> 每次运行泄漏 N 个目录。改为在 kill() 的两条退出路径上递归删除。
1 parent cb59851 commit ab94b1f

2 files changed

Lines changed: 48 additions & 23 deletions

File tree

‎.github/workflows/test.yml‎

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,29 @@ name: Test
22

33
on:
44
push:
5-
branches: ['**']
5+
# 只在这些长期分支上跑 push;其余分支靠 pull_request 触发,避免同一提交跑两遍
6+
branches: [master, release]
67
pull_request:
78
workflow_dispatch:
89

10+
# 同一分支的新推送取消旧运行,避免排队浪费
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
permissions:
16+
contents: read
17+
918
jobs:
1019
test:
1120
runs-on: ubuntu-latest
21+
timeout-minutes: 10
1222
strategy:
1323
fail-fast: false
1424
matrix:
15-
# 覆盖 engines 下限与 Dockerfile 使用的版本
25+
# engines 下限是 18;Dockerfile 用 22;中间放 20 覆盖 LTS 跨度
1626
node: ['18', '20', '22']
27+
name: node ${{ matrix.node }}
1728
steps:
1829
- name: Checkout
1930
uses: actions/checkout@v4
@@ -23,20 +34,11 @@ jobs:
2334
with:
2435
node-version: ${{ matrix.node }}
2536

26-
- name: Show version
37+
- name: Show Node version
2738
run: node --version
2839

40+
- name: Syntax check
41+
run: node --check proxy.mjs
42+
2943
- name: Run tests
3044
run: npm test
31-
32-
# 单独一条:确认 proxy.mjs 语法可被最低支持版本解析。
33-
# setup-node 的解析失败会表现为进程启动即崩,与测试失败混淆,故单列出来。
34-
syntax:
35-
runs-on: ubuntu-latest
36-
steps:
37-
- uses: actions/checkout@v4
38-
- uses: actions/setup-node@v4
39-
with:
40-
node-version: '18'
41-
- name: node --check
42-
run: node --check proxy.mjs

‎test/helpers.mjs‎

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,32 @@
33
import http from 'node:http';
44
import { spawn } from 'node:child_process';
55
import { setTimeout as sleep } from 'node:timers/promises';
6-
import { mkdtempSync, copyFileSync, existsSync } from 'node:fs';
6+
import { mkdtempSync, copyFileSync, existsSync, rmSync } from 'node:fs';
77
import { tmpdir } from 'node:os';
88
import { join, dirname } from 'node:path';
99
import { fileURLToPath } from 'node:url';
1010

1111
export const REPO = dirname(fileURLToPath(import.meta.url)).replace(/[/\\]test$/, '');
1212

13-
// 每个用例拿到独立端口。node --test 每个文件一个进程,用 pid 派生基数,
14-
// 并用随机起点降低跨进程撞端口的概率。
15-
let nextPort = 40000 + (process.pid % 500) * 40 + Math.floor(Math.random() * 20);
16-
export const allocPort = () => nextPort++;
13+
// 取一个当前空闲的端口:让内核分配(listen 0)后立刻释放。
14+
// 不能用 pid 派生区间 —— node --test 各文件并行,pid 取模会在不同 pid 间
15+
// 映射到同一区间(如 pid 100 与 pid 600 同桶),进而偶发 EADDRINUSE。
16+
// 内核分配把冲突面缩到「释放到重新占用」之间的极小窗口,调用方另有重试兜底。
17+
import net from 'node:net';
18+
export async function allocPort() {
19+
return await new Promise((resolve, reject) => {
20+
const srv = net.createServer();
21+
srv.once('error', reject);
22+
srv.listen(0, '127.0.0.1', () => {
23+
const { port } = srv.address();
24+
srv.close(() => resolve(port));
25+
});
26+
});
27+
}
1728

1829
/** 启动一个 mock 上游。ndjson 为要回给代理的 CC NDJSON 行数组。 */
1930
export async function startMockUpstream(opts = {}) {
20-
const port = allocPort();
31+
const port = await allocPort();
2132
const seen = [];
2233
const server = http.createServer((req, res) => {
2334
const chunks = [];
@@ -56,8 +67,10 @@ export async function startMockUpstream(opts = {}) {
5667

5768
/** 在临时 cwd 中启动代理(复刻真实部署:proxy.mjs 与 config.json 同目录)。 */
5869
export async function startProxy({ upstreamPort, env = {}, cwd } = {}) {
59-
const port = allocPort();
70+
const port = await allocPort();
6071
const logs = [];
72+
// 自建的临时工作目录用完必须删;调用方传了 cwd 则由调用方负责。
73+
const ownWorkdir = cwd === undefined;
6174
const workdir = cwd ?? mkdtempSync(join(tmpdir(), 'ccp-test-'));
6275
copyFileSync(join(REPO, 'proxy.mjs'), join(workdir, 'proxy.mjs'));
6376
if (!existsSync(join(workdir, 'config.json'))) {
@@ -90,7 +103,17 @@ export async function startProxy({ upstreamPort, env = {}, cwd } = {}) {
90103
method: 'POST', headers: { 'Content-Type': 'application/json', ...headers },
91104
body: typeof body === 'string' ? body : JSON.stringify(body),
92105
}),
93-
kill: () => new Promise(r => { child.once('exit', r); child.kill(); setTimeout(r, 2000); }),
106+
kill: () => new Promise(r => {
107+
child.once('exit', () => {
108+
if (ownWorkdir) { try { rmSync(workdir, { recursive: true, force: true }); } catch {} }
109+
r();
110+
});
111+
child.kill();
112+
setTimeout(() => {
113+
if (ownWorkdir) { try { rmSync(workdir, { recursive: true, force: true }); } catch {} }
114+
r();
115+
}, 2000);
116+
}),
94117
};
95118
}
96119

0 commit comments

Comments
 (0)