Skip to content

ci: shard command_test so compat-offline-core can finish - #449

Merged
genedna merged 1 commit into
mainfrom
ci-shard-command-test
Aug 10, 2026
Merged

ci: shard command_test so compat-offline-core can finish#449
genedna merged 1 commit into
mainfrom
ci-shard-command-test

Conversation

@genedna

@genedna genedna commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

背景

compat-offline-core 已经数周没有完整跑完过cargo test --all 是 fail-fast 的,而 lib 套件自 2026-07-28 起一直有失败,cargo 每次都在 lib 阶段中止,一个集成测试二进制都没跑到过。表面上看是「53 分钟后失败」,实际上它背后的整个集成套件从未被测量。

把 lib 的失败在一个分支上修好之后,这个 job 直接顶破了 GitHub 的 360 分钟平台上限。随后暴露出三个彼此独立的问题——每一个都是「跑得比以往任何一次都远」才看得见的:

三个问题与修法

1. command_test 是锁瓶颈,不是算力瓶颈

tests/command_test.rs 把 150 个模块(约 2950 条测试)编进一个二进制——这是有意为之,为了省下「每个命令一个二进制」的编译开销。但其中约三分之一会去抢 ChangeDirGuard 持有的那把进程级 cwd 锁,于是无论机器多少核,它实际都只跑在一个核上:日志里 479 条测试单条超过 60 秒,而同期宿主机 load 只有 0.3。

加核没用,只有加进程有用。所以拆成 compat-offline-command 四路矩阵。实测:每片 739 条,约 2h50m,安全落在上限内。

2. switch_test 栈溢出导致整个二进制 abort

command::switch_test::test_detach_head_basic 触发 fatal runtime error: stack overflow,整个进程 abort,连 test result: 都没有。

不是递归:debug 构建下 async 状态机会把每一层 await 的 future 内联进同一个栈帧,switch::execute 的调用链装不进 libtest 给测试线程的 2 MiB 栈。隔离单跑可稳定复现:2 MiB 必溢出,4 MiB 稳定通过。这里设 RUST_MIN_STACK 为 16 MiB 留足余量。

发布的 CLI 跑在 8 MiB 主线程上,这是测试框架的限制,不是用户能碰到的产品缺陷

3. 目标枚举必须过滤 required-features

拆分要求 compat-offline-core 点名目标而非用 --all。但 required-features 未启用的目标必须丢弃而不能点名:cargo test --all 会静默跳过它们,用 --test <name> 点名则是硬错误(target agent_live_gate_test requires the features: test-live-agent)。

已验证:共 208 个 test target,过滤后 200 个;被丢掉的 8 个正是本 job 后续步骤已经带着各自 feature 显式运行的那些。

覆盖面不变

lib、bins、doctests 和其余 200 个集成目标留在 compat-offline-corecommand_test 在分片里跑。

分片按枚举出的测试名做取模划分,穷尽且互斥是构造保证的——重命名或新增测试不可能从所有分片里漏掉,而手工维护的过滤名单迟早会漏。本地验证:2956 条,四片各 739,并集去重 2956,交集为空。

两个 job 都会在枚举数异常偏小时拒绝运行而不是带着残缺覆盖面通过,并都设了 timeout-minutes: 350,让超时以完整日志失败,而不是被平台在 360 分钟处直接砍掉。

验证

#447 上实测:分片 0 完成 737/739,分片 1 完成 738/739 —— 而在此之前每一次尝试都是超时或 abort,拿不到任何结果。剩下的少量失败是既有问题,与本改动无关。

🤖 Generated with Claude Code


Note

Low Risk
Workflow-only changes; risk is CI coverage drift if enumeration logic is wrong, mitigated by minimum-count checks and unchanged feature-gated steps.

Overview
Splits the monolithic command_test run into a new compat-offline-command job with a 4-shard matrix (fail-fast: false). Each shard builds command_test once, lists every test name, assigns tests by index modulo SHARD_COUNT, and runs them with --exact so parallel processes avoid the cwd-lock bottleneck that kept a single job on one core past GitHub’s time limit.

compat-offline-core no longer runs cargo test --all. It runs --lib, --bins, doctests, and every integration test target except command_test, discovered via cargo metadata while dropping targets whose required-features are not enabled (matching --all’s silent skip). Feature-gated suites stay on the existing explicit steps. Both jobs set timeout-minutes: 350 and RUST_MIN_STACK=16MiB so libtest threads don’t abort on large unoptimized async stacks (e.g. switch_test).

Guardrails: each job fails if enumeration looks truncated (<100 integration targets or <2000 command tests).

Reviewed by Cursor Bugbot for commit 18db9e7. Bugbot is set up for automated code reviews on this repo. Configure here.

…ishing

`compat-offline-core` has not run to completion in weeks. `cargo test
--all` is fail-fast, and the lib suite has been failing since
2026-07-28, so cargo aborted there every time and never reached a
single integration binary. Runs looked like 53-minute failures; the
suite behind them was never measured.

Once the lib failures were fixed on a branch, the job ran past the
360-minute platform cap. Three separate problems, each only visible
because the run got further than any before it:

1. `tests/command_test.rs` compiles 150 modules (~2950 tests) into one
   binary — deliberately, to avoid one binary per command. Roughly a
   third of them take the process-global cwd lock every
   `ChangeDirGuard` holds, so it runs at about one core no matter the
   machine: 479 of its tests logged "running for over 60 seconds" while
   the host sat near 0.3 load. It is lock-bound, not CPU-bound, and the
   only thing that shortens it is more than one PROCESS.

   Split into `compat-offline-command`, a four-way matrix. Measured:
   739 tests per shard, ~2h50m each, comfortably inside the cap.

2. `command::switch_test::test_detach_head_basic` ABORTED the whole
   binary with a stack overflow. Not recursion — an unoptimized async
   state machine inlines every awaited future into one frame, and
   libtest's 2 MiB test threads cannot hold `switch::execute`'s chain.
   Reproduced in isolation at 2 MiB, passes at 4 MiB. `RUST_MIN_STACK`
   is 16 MiB here for headroom. The shipped CLI runs those futures on
   the 8 MiB main thread, so this is a harness limit, not a product
   one.

3. compat-offline-core now names its targets instead of using `--all`,
   so the two halves can be split. Targets whose `required-features`
   are off must be dropped rather than named: `cargo test --all` skips
   them silently, but `--test <name>` on one is a hard error. Verified:
   208 test targets, 200 after the filter, and the eight dropped are
   exactly the ones this job's later steps already run explicitly with
   the feature each needs.

Coverage is unchanged. Shards partition by enumerated test NAME, so the
split is exhaustive and disjoint by construction — a rename cannot drop
a test the way a hand-maintained filter list would. Both jobs refuse to
run a suspiciously small set rather than pass on partial coverage, and
both carry `timeout-minutes: 350` so an overrun fails with its log
intact instead of being reaped at the cap.

Validated on #447: shard 0 finished 737/739 and shard
1 finished 738/739, where every previous attempt had timed out or
aborted with no result at all. The handful of remaining failures are
pre-existing and unrelated to this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@genedna
genedna merged commit e74a0ee into main Aug 10, 2026
5 checks passed
@genedna

genedna commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

说明:本 PR 无法自我验证

base.yml 的触发条件是

on:
  pull_request:
    paths-ignore:
      - '.github/**'
      - 'docs/**'

所以只改 workflow 的 PR 永远不会触发 Check, Build and Test——这也是本 PR 上看不到任何 compat-* job 的原因。这个仓库里 workflow 改动本身从来不经 CI 检验,是个独立于本 PR 的缺口,值得单独处理。

因此本 PR 的验证证据来自 #447,那里同样的 base.yml 与代码改动一起跑过:

job 结果
compat-offline-command (0) 3h05m,完整跑完 737 passed / 2 failed
compat-offline-command (1) 2h55m,完整跑完 738 passed / 1 failed
compat-offline-core 日志打出 running lib + bins + doctests + 200 integration targets,与预期一致

对照:在此之前每一次 compat-offline-core 都是超时(6 小时被平台砍掉)或 abort,拿不到任何测试结果

剩余的少量失败是既有问题,与本 PR 无关,正由 #450 单独处理。

@genedna

genedna commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

完整验证结果(四分片全部跑完)

#447 上这一轮已全部结束,四个分片无一超时、无一 abort

分片 耗时 结果
compat-offline-command (0) 3h05m 737 passed / 2 failed
compat-offline-command (1) 2h55m 738 passed / 1 failed
compat-offline-command (2) 2h25m 737 passed / 2 failed
compat-offline-command (3) 2h50m 739 passed / 0 failed
compat-offline-core 54m running lib + bins + doctests + 200 integration targets,2 条既有失败

每片 739 条,合计 2956 条,与本地验证的划分完全一致;最长 3h05m,距 350 分钟上限有充分余量。

对照:在此改动之前,compat-offline-core 的每一次运行不是被平台在 360 分钟处砍掉,就是在 switch_test 栈溢出时 abort —— 从来拿不到任何测试结果

合计 7 条失败,全部是既有问题、全部确定性(隔离单跑同样失败),与本 PR 无关。其中测试侧的部分由 #450 处理;另有几条属于产品与测试的契约不一致(错误码、HTTP 状态码、警告文案),需要维护者定夺,已在 #450 中列出。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant