光谱带
以 Redis 为工作队列分配测试跑取器 。 一个过程将测试文件路径推向Redis列表;多个 CI 跑者在原子上会偷取批次,并通过可配置的命令执行.
CI Job 1 (push): RPUSH key f1 f2 f3 ... fN --> [Redis List]
CI Job 2 (worker): LPOP key 5 <-- [Redis List] --> npx jest
CI Job 3 (worker): LPOP key 5 <-- [Redis List] --> npx jest
CI Job N (worker): LPOP key 5 <-- [Redis List] --> npx jest
LPOP 与一个计数参数(Redis 6.2+)是原子——多个工人同时调用它永远不会收到同一个文件.
这是 TypeScript 端口 光谱带 (鲁比/RSpec). 工人不是在进程中运行RSpec,而是将您提供的任何命令(jest, vitest, node等)以被窃取的文件路径作为参数生成.
安装
npm install @factorialco/specbandit
或者直接运行 :
npx specbandit --help
所需资源: Node.js 18, Redis 6.2 (英语)
使用量
1. 将测试文件推给 Redis
单一的 CI 任务在工人开始前排入所有测试文件路径 .
# Via glob pattern
specbandit push --key pr-123-run-456 --pattern 'test/**/*.test.ts'
# Via stdin pipe (for large file lists or custom filtering)
find test -name '*.test.ts' | specbandit push --key pr-123-run-456
# Via direct arguments (for small lists)
specbandit push --key pr-123-run-456 test/models/user.test.ts test/models/order.test.ts
文件输入优先级 : stdin > -- pattern > 直接参数。 。 。 .
添加 --reset 当制片人可以为同一密钥运行不止一次时:
specbandit push --key pr-123-run-456 --reset --pattern 'test/**/*.test.ts'
见 屡次出击: --reset。 。 。 .
2. 盗窃和从多个工人那里逃跑
每个CI跑者偷取批发并运行. 随你喜欢多少跑步者,他们就会自动分出工作.
specbandit work --key pr-123-run-456 --command "npx jest" --batch-size 10
每个工人循环:
LPOP来自 Redis (原子) 的 N 文件路径- 将命令与文件路径合并为参数
- 重复到队列为空
- 如果所有批次都通过,则退出 0, 如果有任何失败,则退出 1
默认情况下,失败的批次会 没有 拦住工人。 它继续偷剩下的工作 所以其他的跑者不会被阻止 等待永远不会被消耗的文件。 为了在第一次失败时停止整个运行,使用 --fail-fast。 。 。 .
CLI 参考
specbandit push [options] [files...]
--key KEY Redis queue key (required)
--pattern PATTERN Glob pattern for file discovery
--redis-url URL Redis URL (default: redis://localhost:6379)
--key-ttl SECONDS TTL for all Redis keys (default: 604800 / 1 week)
--reset Empty the key before pushing (see below)
specbandit reset [options]
--key KEY Redis queue key (required)
--redis-url URL Redis URL (default: redis://localhost:6379)
specbandit work [options]
--key KEY Redis queue key (required)
--command CMD Command to run with file paths (required, e.g. "npx jest")
--command-opts OPTS Extra options forwarded to the command (space-separated)
--batch-size N Files per batch (default: 5)
--redis-url URL Redis URL (default: redis://localhost:6379)
--key-rerun KEY Per-runner rerun key for re-run support (see below)
--key-ttl SECONDS TTL for all Redis keys (default: 604800 / 1 week)
--verbose Show per-batch file list and full command output
--json-out PATH Write merged JSON results to file
--fail-fast Stop at the first failed batch and empty the shared queue (see below)
环境变量
所有 CLI 选项可以通过环境变量设置:
| 变量 | 说明 | 默认 |
|---|---|---|
SPECBANDIT_KEY | 重写队列键 | (必须) |
SPECBANDIT_REDIS_URL | 重做连接 URL | redis://localhost:6379 |
SPECBANDIT_COMMAND | 要运行的命令 | (工作所需) |
SPECBANDIT_COMMAND_OPTS | 空格分隔命令选项 | (无) |
SPECBANDIT_BATCH_SIZE | 每起窃取的文件 | 5 |
SPECBANDIT_KEY_RERUN | 每个运行者重运行密钥 | (无) |
SPECBANDIT_KEY_FAILED | 失败测试文件的 Redis 密钥 | (无) |
SPECBANDIT_KEY_TTL | 以秒计的所有 Redis 密钥过期 | 604800 (1个星期) |
SPECBANDIT_VERBOSE | 启用动词输出( 1/ true/ 是) | false |
SPECBANDIT_FAIL_FAST | 停止于第一批失败( 1/ 是真的/ 是) | false |
SPECBANDIT_JEST_BATCH_TIMEOUT | Jest适配器闲置(无进展)超时数秒;这段长时间没有Jest输出的批次被视为挂起并失败. 0 关闭它. | 600 |
CLI旗优先于环境变量.
节点.js API
import { Configuration, Publisher, Worker, RedisQueue } from '@factorialco/specbandit'
// Push
const queue = new RedisQueue('redis://my-redis:6379')
const publisher = new Publisher({
key: 'pr-123-run-456',
keyTtl: 7200,
queue,
})
await publisher.publish({ pattern: 'test/**/*.test.ts' })
// Work
const worker = new Worker({
key: 'pr-123-run-456',
command: 'npx jest',
commandOpts: ['--coverage'],
batchSize: 10,
queue,
})
const exitCode = await worker.run()
await queue.close()
process.exit(exitCode)
示例: GitHub 动作( 基本)
jobs:
push-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: |
npx specbandit push \
--key "pr-${{ github.event.number }}-${{ github.run_id }}" \
--redis-url "${{ secrets.REDIS_URL }}" \
--pattern 'test/**/*.test.ts'
run-tests:
runs-on: ubuntu-latest
needs: push-tests
strategy:
matrix:
runner: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: |
npx specbandit work \
--key "pr-${{ github.event.number }}-${{ github.run_id }}" \
--redis-url "${{ secrets.REDIS_URL }}" \
--command "npx jest" \
--batch-size 10
重运行失败的 CI 任务
问题
当您使用 specbandit 在多个 CI 跑者之间分配测试时( 例如一个 GitHub 动作矩阵, 其中包含 4 个跑者), 每个跑者 偷窃 共享的 Redis 队列中随机的测试文件子集。 分布为非定相.
这造成了CI重跑的问题:
- 首发: Runner #3 窃取并执行文件 X, Y, Z. File Y 失败. 共享队列现在是空的 .
- 3号跑道重跑: GitHub 动作只重跑失败的跑者. 开始了
specbandit work同样的--key,但队列已经空出。 跑步者三号无所事事 0出口 -- -- 测试失败无声通过。 。 。 .
解决办法是: --key-rerun
这个 --key-rerun 旗帜给每个矩阵跑取者在Redis中自己的"记忆". 它可以使光谱带 记录 每一个被执行的跑者的文件,以及 重播 确切地说,那些文件在重新运行.
specbandit work \
--key "pr-42-run-100" \
--key-rerun "pr-42-run-100-runner-3" \
--command "npx jest" \
--batch-size 10
已公布的标记可以防止无声的假通行证
Redis 一旦最后一个元素被弹出就会删除列表, 所以 a 排水量 队列和 a
队列 从未出版 看起来一模一样 告诉他们, push 设置a
相伴标记键( e)<key>:published)与队列相同的TTL. 标记
超过排出的名单,所以 work 相信这部作品真的已经出版.
如果标记过期( TTL) 或 Redis 在第一次运行和再次运行之间被冲出,
work 没有标记和 立即失败 而不是进行零测试和
正在离开 0。 换成旧的 --rerun 旗 -- -- 现在保护是自动的
不需要任意配置.
如何运作:操作模式
work 读取已公布的标记加上共享队列和重运行密钥的状态,然后
沿着这个真相表:
| 发表 | 共享队列 | 重运行密钥 | 结果 |
|---|---|---|---|
| 没有 | 任意 | 任意 | 崩溃(出局 1) - 没有发表(从未被推动,或TTL过期). |
| 对 | 空 | 空 | 还好 工人来晚了 已排出队列,无所事事(出局0). |
| 对 | 人口数 | 空 | 摆 经典运行 -- 从共享队列中取出, 记录每批到重运行键 --key-rerun 已经设定. |
| 对 | 空 | 人口数 | 摆 经典重运行 -- 完全重放记录的文件, 忽略共享的队列 . |
| 对 | 人口数 | 人口数 | 摆 全部重运行 - 队列被重排, 而这个跑者仍然持有前一次运行的重运行内存 。 stale再运行的密钥被删除,跑者从共享的队列中窃取,就像经典跑道一样,随行再录. |
使用重运行支持完成 GitHub 动作示例
jobs:
push-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: |
npx specbandit push \
--key "pr-${{ github.event.number }}-${{ github.run_id }}" \
--redis-url "${{ secrets.REDIS_URL }}" \
--pattern 'test/**/*.test.ts'
run-tests:
runs-on: ubuntu-latest
needs: push-tests
strategy:
matrix:
runner: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: |
npx specbandit work \
--key "pr-${{ github.event.number }}-${{ github.run_id }}" \
--key-rerun "pr-${{ github.event.number }}-${{ github.run_id }}-runner-${{ matrix.runner }}" \
--redis-url "${{ secrets.REDIS_URL }}" \
--command "npx jest" \
--batch-size 10
屡次出击: --reset
队列密钥由CI运行范围,而非CI尝试范围. 必须是: 重跑一个失败的跑者不会重跑所推动的工作,因此跑者仍然必须找到队列和已公布的标记,这是创建的第一个尝试.
成本是生产者不是一能的。 一个推动然后失败的制片人,或者作为整个工作流程的一部分重新运行的制片人,将工作列表的第二个副本附加到同一个密钥上. 每一个文件然后被排出两次,所以套房运行两次,任何两份到达同一个工人的拷贝都会在一个过程中被装入两次. 对于在模块范围定义常数的测试文件,第二个负载是致命的.
--reset 使推能:
specbandit push --key "pr-42-run-100" --reset --pattern 'test/**/*.test.ts'
队列及其 <key>:published 标记被删除,然后推出工作列表,所以无论制片人运行了多少次,密钥只持有一份副本。 剩下的数字被记录, 一个非零的告诉你, 早先的尝试推了一个名单 没有人消耗:
[specbandit] Reset key 'pr-42-run-100': discarded 4213 queued files from a previous push.
对于单独清理和推进的呼叫者,也有一个独立的命令:
specbandit reset --key "pr-42-run-100"
两者都留下了每个跑者重跑的密钥和失败的密钥,因此一个单跑者重跑仍然可以重放自己的文件. 在共享队列及其重运行密钥中查找数据的跑者是 全部重运行 上表中的大小写,并重排自己的内存.
重置为无重置不是错误 。 无论是哪一个命令都无法在无事可做的情况下清除钥匙:自行丢下标记会让钥匙崩溃的每个工人都成为"从未公布".
失败快取 : --fail-fast
使用 --fail-fast 当一次失败使得其余的跑道无用时,你要CI尽快报告.
specbandit work --key pr-123-run-456 --command "npx jest" --fail-fast
当一个批次失败,工人:
- 将批次记录在摘要和
--report档案,和往常一样. - 删除共享队列列表( E)
DEL <key>),所以没有工人可以偷取待处理的文件. - 站出站口1个.
这个 <key>:published 标记留下。 检查排队的工人在prune后看到"被发表但被排出"并出道0,而不是用"从未发表"来崩溃. 一个有批次飞行的工人完成批次,然后发现队列空出并停止. Specbandit在飞行中不会中断一个批次.
斯佩克班迪特每批都看到结果,所以工人在分批失败后停止,而不是在分批失败测试后停止. 也停止在一个批次内, 转发您的测试跑者选项, 例如 -- --bail 为杰斯特.
在回放模式下,工人在第一批失败后停止并跳过所剩的回放文件. 重放从不触碰共享的队列,所以没有什么可推的.
--fail-fast 无法使用 --key-failed。失败的密钥会反馈一个后期的重试通过,并在大多数文件运行之前的故障快速停止,这样该列表就不完整了 。 Specbandit在启动和出站时拒绝组合 1. 一个空的 --key-failed "" 计数为未设置.
如何运作
- 推开 用途
RPUSH,然后设定EXPIRE键上(默认:1周)。 也设置了<key>:published标记相同的TTL,这样工人就可以从从未公布的排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排的排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排的排出排出排出排出排出排出排出排出排出. - 偷窃 用途
LPOP key count(Redis 6.2+),在原子上会弹出到N元素. 没有卢亚脚本,没有锁,没有种族条件. - 记录 (当
--key-rerun设定 : 每次被窃之后, 批次也是RPUSHed以相同的TTL重跑键重跑. - 重播 (当
--key-rerun有数据, 共享队列已排出 : 读取重运行密钥的所有文件LRANGE(无损)分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出,分出分出分出分出分出分出分出分出分出分出,分出分出分出分出分出分出分出分出分出,分出分出分出分出分出分出分出分出出出出出出出出出出分出分出分出分出出出分出分出分出分出分出出分出出出分出分出出分出分出出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出. - 全部重运行 (当共享队列和重跑密钥都有数据时): stale 重跑密钥被删除
DEL,然后跑者从共享队列中偷取并重新录入经典跑道. - 重设 (单位:千美元)
push --reset或者说specbandit reset)删除队列及其<key>:published在单个标记中DEL,因此,运行两次的制片人不能为工作名单排出两次. 重运行和失败的密钥未受影响 . - 运行 将配置的命令通过
child_process.spawnSync()以文件路径作为参数。 上面没有弹壳扩张. - 失败快 (单位:千美元)
--fail-fast: 第一组失败后, 共享队列列表被删除DEL(单位:千美元)<key>:published标记留下),工人停止. - 退出代码 如果每批通过(或队列已经空出),则为0;如果任何批次出现故障,则为1.
发展
npm install
npm test # unit tests (no Redis needed)
npm run build # compile TypeScript
npm run typecheck # type-check without emitting
许可证
麻省理工学院