Skip to content

Commit 5c33642

Browse files
authored
feat: mcpp test capability batch — isolation, parallel, filter, list, timeout, JSON (0.0.104) (#274)
* docs: implementation plan for test isolation/filter/json + musl deliverable * feat(test): name tests by tests/-relative path, enabling subdirectories * feat(build): BuildOptions::ninjaTargets — build an explicit subset of the plan * feat(test): per-test compile isolation — a broken test no longer kills the run * feat(test): positional pattern filters tests by path-based name * feat(test): --message-format json — NDJSON records for tooling (d2x provider et al.) * fix(process): strip private-glibc entries from inherited loader paths — nested mcpp no longer segfaults its tools * docs: changelog + doc sync for mcpp test isolation/filter/json + loader-path fix * feat(test): [build].flags globs cover test TUs; dead-glob warning now checks the disk, not just scanned sources * docs: changelog for test glob-flags coverage + downstream d2mcpp adoption note * fix(test): interleave per-test Compiling/result/diagnostics; drop misleading mid-run Finished banner * docs: mcpp test architecture review — layering vs gtest, batch evaluation, roadmap P1-P6 * docs: batch-2 implementation plan (P1-P4,P6 + release 0.0.104) * perf(test): parallel keep-going pre-build for Phase B — full-suite wall time 29s -> 3.7s on 49 tests * feat(test): shell-convention signal exit codes (128+sig) + per-test duration_ms in JSON * feat(test): --list — enumerate (filtered) tests without building; JSON records for tooling * feat(test): --timeout <secs> per-test run deadline (POSIX; Windows best-effort untimed) * docs(test): framework-agnostic wording (drop 'gtest style'), new flags, naming exemption note (zh+en) * release: 0.0.104 — mcpp test capability batch (isolation/parallel, filter, list, timeout, json, signal fix) * release: sync MCPP_VERSION constant to 0.0.104 * test(e2e): renumber new scripts 118-126 -> 152-160 (numeric collision with pre-existing scripts) * test(e2e): gate signal/timeout scripts on unix-shell — POSIX-only semantics, documented Windows limitation
1 parent 4bf9dc6 commit 5c33642

28 files changed

Lines changed: 2111 additions & 57 deletions

.agents/docs/2026-07-23-test-isolation-json-plan.md

Lines changed: 939 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# mcpp test 架构评估与设计方案
2+
3+
- 日期:2026-07-24
4+
- 分支:`feat/test-isolation-json`(评估对象 = 本分支 8 个提交 + mcpp test 既有形态)
5+
- 关联:d2mcpp「练习即测试」重设计(d2mcpp 仓 `.agents/docs/2026-07-23-exercises-as-tests-design.md`
6+
- 本文定位:**设计评审记录 + 后续演进方案**。先从 mcpp 自身架构评估本批次改动与
7+
`mcpp test`/gtest 的定位关系,再评估对 d2mcpp 的适配性,最后给出路线图。
8+
9+
---
10+
11+
## 1. 结论摘要
12+
13+
1. `mcpp test` 的正确定位是**二进制运行器(runner of binaries)**:发现/构建/运行
14+
`tests/**/*.cpp`(每文件一个独立二进制)并聚合退出码。它对二进制内部使用什么
15+
断言框架**零感知**——gtest 是依赖层(dev-dependencies)的方案,与运行器正交。
16+
本批次改动全部落在运行器层,未破坏该分层。
17+
2. 六项改动中五项与 mcpp 架构自洽且为通用能力;**逐测试隔离的当前实现存在一个
18+
实质代价:Phase B 跨测试构建串行化**(全绿路径最明显),有明确的改进方案(P1,
19+
keep-going 预构建 + 存在性归因),语义不变、并行度恢复。
20+
3. gtest 与 mcpp test 不是竞争关系而是两层:文件级过滤(`mcpp test <pattern>`)与
21+
用例级过滤(`-- --gtest_filter=...` 透传)今天已可组合。不建议把 gtest 感知
22+
内建进运行器;若要用例级一等公民,正确路径是**可选的测试协议**(P5,v2 提案)。
23+
4. 对 d2mcpp:练习=文件=二进制的模型是**必然选择**(用例级框架无法表达"尚未编译
24+
通过"这一教学核心状态);判定三来源分层映射良好;遗留一处发现逻辑重复
25+
(Provider 目录扫描 vs 运行器 glob),可由 P2(`mcpp test --list`)收敛。
26+
27+
---
28+
29+
## 2. 分层模型:运行器与框架的正交性
30+
31+
```
32+
┌────────────────────────────────────────────────────────┐
33+
│ runner 层(mcpp test) │
34+
│ 发现 tests/**/*.cpp → 每文件合成 TestBinary target │
35+
│ 构建(dev-deps 生效) → 运行 → 退出码聚合 → 人读/JSON │
36+
├────────────────────────────────────────────────────────┤
37+
│ in-binary 层(框架自选,mcpp 零感知) │
38+
│ 裸 main / gtest(+gtest_main) / catch2 / d2x 练习库 … │
39+
└────────────────────────────────────────────────────────┘
40+
```
41+
42+
既有事实支撑这一分层已是 mcpp 的实际设计:
43+
44+
- gtest 以 `[dev-dependencies] gtest = "1.15.2"` 进入,`plan.cppm`
45+
dependency-provided-entry 启发(gtest_main 提供 `main`)解决链接问题——框架
46+
支持发生在**依赖解析层**,不在运行器;
47+
- `mcpp test -- args` 把参数透传给**每个**测试二进制,`-- --gtest_filter=X`
48+
即用例级过滤,无需运行器懂 gtest;
49+
- 判定协议只有一条:**进程退出码**。gtest 聚合其内部用例到退出码,裸 main 直接
50+
返回,d2x 练习库经 atexit 收口——三者对运行器等价。
51+
52+
### 与同类工具对照
53+
54+
| 维度 | mcpp test | cargo test | ctest | gtest(单跑) |
55+
|---|---|---|---|---|
56+
| 测试单元 | 文件=二进制 | 集成测试同(tests/*.rs=crate);单测经 libtest 协议到用例级 | 注册的命令 | 用例(in-binary) |
57+
| 编译失败语义 | **该测试 fail,其余照跑**(本批次) | 整体构建失败(不隔离) | 构建在外部,坏目标不阻塞其他命令 | n/a |
58+
| 过滤 | 文件名子串(本批次) | 用例名子串 | -R 正则 | --gtest_filter |
59+
| 机器可读 | NDJSON(本批次) | --message-format json | --output-json | --gtest_output=json |
60+
| 判定 | 退出码 | libtest 协议/退出码 | 退出码/正则 | 内部统计 |
61+
62+
结论:mcpp test 的文件级模型与 ctest 同族、比 cargo 的集成测试**多出编译隔离**
63+
(cargo 的 tests/*.rs 一个编译失败同样拖死整轮)。用例级是 cargo 单测/gtest 的
64+
领地,mcpp 通过透传组合而非内建。
65+
66+
---
67+
68+
## 3. 本批次改动逐项评估
69+
70+
### 3.1 tests 相对路径命名(4b09984)✓
71+
72+
- 动机:子目录布局下 stem 撞名硬错误;名字应与文件系统布局同构。
73+
- 一致性:合成 target 专属规则,平铺布局名字不变,无兼容破坏。
74+
- **已知豁免需要记录**:合成测试名可含 `/``00-a/0`),而 `[targets.*]` 用户
75+
target 名文法不允许——两套命名空间事实上分离(合成 target 不进 manifest、不参与
76+
publish/xpkg)。属可接受的不一致,但应在 docs 里写明,防止未来有人"统一"它。
77+
78+
### 3.2 `BuildOptions::ninjaTargets`(bce3e7d)✓
79+
80+
- 后端接口的最小扩展(显式 goal 集合,空=全量),`producedArtifacts` 同步收窄。
81+
- 通用性无争议;风险点是调用方误传非 output 路径 → ninja "unknown target" 报错,
82+
错误信息可读,可接受。
83+
84+
### 3.3 逐测试编译隔离(ed557df + Phase A 目标集修正)——语义 ✓,实现有代价
85+
86+
语义评估:
87+
- 包级失败(lib/deps/共享模块)≠ 测试失败的区分是**正确且必要**的:52 个红测试
88+
误导用户"你全错了",而真相是基础设施坏了。Phase A 以"全部非测试入口的编译单元 +
89+
非测试 link 产物"为 goal 集合,覆盖了 test 模式下 lib target 被跳过、共享破坏
90+
藏在模块对象里的边角(e2e 119 第二场景验证)。
91+
- 归因精确:每测试独立 ninja goal,诊断天然按测试分组,扫描期错误(dyndep)也
92+
落在对应测试上。
93+
94+
实现代价(本文最重要的自我批评):
95+
- **Phase B 对每个测试串行调用 `backend->build()`**。旧实现一次 ninja 调用并行
96+
编译全部测试 TU;现在 N 个测试 = N 次 ninja 进程 + N 次 build.ninja/
97+
compile_commands 重写 + N 次(缓存命中的)hermetic 检查,且**测试之间的编译
98+
不再并行**。全绿路径(CI、正常项目)代价最大;d2mcpp 的 49 题答案全量验证
99+
实测 ~29s(含运行),可用但不优。
100+
- 运行阶段本就串行(与旧行为一致),不算回归,但同样是并行机会。
101+
102+
**改进方案(P1,语义不变)**:三段式
103+
1. Phase A 不变;
104+
2. Phase B-1:**单次** ninja 调用携带全部(过滤后)测试 goal + `-k 0`
105+
(keep going)——恢复全并行;
106+
3. Phase B-2:逐测试按**产物存在性**判定编译成败;对失败者**逐个**重跑其 goal
107+
(必然快速失败)以取得干净的每测试诊断,随后照常运行成功者。
108+
失败是少数路径,逐个重试的代价与收益成正比。
109+
110+
### 3.4 过滤器(16da216)✓
111+
112+
- 过滤发生在**构建/运行阶段而非计划阶段**是关键决策:计划恒含全部测试,
113+
`compile_commands.json` 保持完整(clangd 依赖)。正确。
114+
- 子串语义与 cargo 对齐;`…/1` 匹配 `…/10` 的邻居效应已在下游(d2mcpp Provider
115+
逐行精确匹配)消化。可选增强:`--exact`
116+
117+
### 3.5 `--message-format json`(f2c7830)✓,schema 需小幅演进
118+
119+
- 流式逐测试记录 + 包级错误记录 + summary,stdout 纯协议流:与 `-q`/`ui::set_quiet`
120+
的既有静音机制正交组合,实现干净。
121+
- 缺口:per-test `duration_ms`(进度类前端需要);无 `--list`(见 P2);
122+
`signal` 从退出码反推(128+n 启发)而非 wait status 直读,Windows 语义空缺。
123+
124+
### 3.6 输出交错与横幅(fix 提交)✓
125+
126+
`Compiling → 结果 → 该测试诊断`的连续块 + 删除 Phase A 后误导性的
127+
`Finished test` 横幅——cargo 形态,修复了"成功横幅紧邻全红"的自相矛盾。
128+
129+
### 3.7 平台/清单层修复 ✓
130+
131+
- `merged_environ` 剥离 `xim-x-glibc` loader 条目:只针对私有 payload、用户条目
132+
保留、显式 override 优先——外科式,配合 musl 静态发行从两端封死嵌套段错误。
133+
- `[build].flags` glob 覆盖测试 TU + 死 glob 警告改查磁盘:glob 指名文件,
134+
是否被扫描为 source 与之正交;警告语义从"没命中扫描源"修正为"磁盘上无此文件",
135+
是普适改进而非特例。
136+
137+
---
138+
139+
## 4. mcpp test 与 gtest:定位与演进
140+
141+
### 4.1 现状判断
142+
143+
- 运行器不感知框架是**优点**:gtest/catch2/doctest/裸 main/d2x 库在退出码协议下
144+
等价,mcpp 无需 per-framework 适配矩阵。
145+
- `docs/00-getting-started.md` 的 "(gtest style)" 措辞**具有误导性**——它描述的是
146+
"常配合 gtest 使用",读起来却像"运行器实现了 gtest 语义"。应改为
147+
"one binary per file; bring your own framework (gtest via dev-dependencies)"。
148+
149+
### 4.2 不建议:运行器内建 gtest 感知
150+
151+
内建意味着:解析 gtest 输出/JSON、维护版本兼容、并对 catch2/doctest 重复同样工作。
152+
分层被打穿,收益仅是省去一次 `--` 透传。否决。
153+
154+
### 4.3 建议(P5,v2 提案):可选的用例级测试协议
155+
156+
参照 cargo/libtest 的"运行器↔测试二进制"契约,定义 mcpp 自己的**可选**协议:
157+
158+
```
159+
运行器设 MCPP_TEST_PROTOCOL=1 后:
160+
<bin> --mcpp-list → NDJSON: {"case":"Math.Add"} …
161+
<bin> --mcpp-run <case> → 运行单用例,退出码判定
162+
(或单进程模式:<bin> --mcpp-json → 逐用例 NDJSON 结果流)
163+
不支持协议的二进制(裸 main):探测失败即回退到整二进制模式——零破坏。
164+
```
165+
166+
- gtest 侧一个 ~50 行 adapter(translate 到 `--gtest_list_tests`/`--gtest_filter`/
167+
`--gtest_output`)即可接入;d2x 练习库天然可实现。
168+
- 运行器获得:用例级过滤/并行/JSON,而仍不认识任何具体框架。
169+
- 明确 **YAGNI 边界**:在出现第二个真实需求方之前不实现,本文仅锁定方向,防止
170+
未来用"内建 gtest"这类打穿分层的方案填这个空。
171+
172+
---
173+
174+
## 5. 对 d2mcpp 的适配性评估
175+
176+
1. **文件=二进制模型是教学的必然选择**:练习的核心状态是"尚未编译通过",用例级
177+
框架(gtest 等)以"可编译"为前提,无法表达这一状态;编译错误本身是教学主通道
178+
`D2X_YOUR_ANSWER` 指着要填的位置)。文件级隔离恰好把"一个未完成练习"约束为
179+
"一个红测试"。
180+
2. **判定三来源分层映射良好**:mcpp JSON(编译/退出码事实)× d2x 库侧信道
181+
(断言语义/路障)× Provider(合并成协议 verdict)——运行器不需要为 d2mcpp
182+
增加任何专有语义,验证了 §2 的分层。
183+
3. **发现逻辑存在重复**:Provider 自行扫描 `src/*/tests/` 推导 id/order,运行器
184+
另有一份 `tests/**` glob。两者约定同构但真相源有二。P2 的
185+
`mcpp test --list --message-format json`(输出测试名+主文件路径)可让 Provider
186+
的枚举改为消费运行器输出,回到单一真相源。
187+
4. **性能画像**:checker 单题路径(filter + 缓存)最优(~0.2s 级);e2e 全量
188+
路径承受 §3.3 的串行代价(49 题 ~29s),P1 落地后预期显著下降。
189+
5. **已消化的风险**:子串过滤邻居效应(Provider 精确匹配);`signal` 启发式
190+
(练习判定不依赖 signal 字段)。
191+
192+
---
193+
194+
## 6. 路线图(按优先级)
195+
196+
| # | 事项 || 验收标准 |
197+
|---|---|---|---|
198+
| P1 | Phase B 改 keep-going 预构建 + 存在性归因 + 失败者逐个取诊断 | runner | e2e 152–157 不变全绿;多测试全绿工程 `mcpp test` 构建墙钟时间恢复到与旧单次构建同量级 |
199+
| P2 | `mcpp test --list [--message-format json]` | runner | 输出测试名+主文件;d2mcpp Provider 枚举可切换为消费该输出 |
200+
| P3 | JSON 记录增加 `duration_ms``signal` 改从 wait status 直读(POSIX) | runner | schema 向后兼容(只增字段) |
201+
| P4 | 每测试超时 `--timeout <s>`(挂死测试 → run_fail + 标注) | runner | 睡死测试不拖垮整轮 |
202+
| P5 | 用例级测试协议(§4.3) | 协议 | 出现第二个需求方后再立项 |
203+
| P6 | docs 措辞修正:"(gtest style)" → framework-agnostic 表述;记录 §3.1 命名豁免 | docs ||
204+
205+
## 7. 风险与未决
206+
207+
- **Windows 全链路未实测**`--quiet`(sandbox ninja 补丁旗标)、路径分隔、
208+
`_pclose` 退出码语义——发版前需要一轮 Windows CI。
209+
- P1 的存在性归因前提是"goal 失败 ⇒ 产物不存在":ninja 对失败目标不落盘产物,
210+
**残留的旧产物**会造成误判——实现时须以"本轮 mtime 是否推进/restat 状态"或
211+
预删除产物为判据,不能只看 exists()。
212+
- 协议字段一旦被 d2mcpp 之外的消费者依赖,schema 演进需按"只增不改"纪律执行。
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# mcpp test 演进批次二 — Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** 落实 `2026-07-24-mcpp-test-design-review.md` 路线图 P1/P2/P3/P4/P6(P5 按该文档决策显式排除:出现第二个需求方前不立项),版本 0.0.103 → 0.0.104,单 PR 合入 main。
6+
7+
**Architecture:** 全部改动收敛在 `src/build/execute.cppm`(run_tests)、`src/build/backend.cppm`/`ninja_backend.cppm`(keepGoing)、`src/platform/process.cppm`(信号退出码规范化 + 带超时运行)、`src/cli*`(--list/--timeout 参数)与 docs。每项 TDD:先写 e2e(124–127)确认 RED 再实现。
8+
9+
**Tech Stack:** 既有栈;e2e bash 脚本。
10+
11+
## Global Constraints
12+
13+
- 语义回归零容忍:e2e 118–123 全程保持绿。
14+
- JSON schema 只增不改(新增 `duration_ms``timed_out` 字段;`signal` 语义修正属 bug fix)。
15+
- 每任务一个提交;最后版本提交(mcpp.toml 0.0.104 + CHANGELOG 定版 2026-07-24)。
16+
- PR:单个,源分支 `feat/test-isolation-json` → main;CI 全绿后 `gh pr merge --squash --admin`(用户已明确授权 bypass squash)。
17+
- 生态验证:合入后以 main 构建 musl 二进制,复跑 mcpp e2e 全量 + d2mcpp e2e all + d2x 构建/单测/checker 冒烟。
18+
19+
## Tasks
20+
21+
- [ ] **T1 (P1) Phase B 并行化**:`BuildOptions.keepGoing`(→ ninja `-k 0`);run_tests 在逐测试循环前插入一次携带全部过滤后 goal 的 bulk build(结果忽略,只为并行填充缓存);既有逐测试 build 保留(成功者缓存命中≈无操作,失败者快速重试取干净诊断)。验收:118–123 绿;d2mcpp cpp11 全量答案态构建墙钟显著下降(手测记录)。
22+
- [ ] **T2 (P3a) 信号退出码规范化**:`normalize_exit_code` 增加 `WIFSIGNALED → 128+WTERMSIG`(shell 惯例;当前返回原始 status,JSON signal 推导永不触发的 bug)。e2e 124:段错误测试 → `"exit_code":139,"signal":11`
23+
- [ ] **T3 (P3b) per-test `duration_ms`**:JSON 记录增加字段(编译+运行墙钟);summary 已有 elapsed 不动。并入 e2e 124 断言字段存在。
24+
- [ ] **T4 (P2) `mcpp test --list`**:列出(过滤后)测试名;`--message-format json` 时逐行 `{"test":…,"main":…}` + `{"summary":{"total":N}}`;不触发构建。e2e 125。
25+
- [ ] **T5 (P4) `--timeout <secs>`**:平台层新增带截止的运行(POSIX spawn+WNOHANG 轮询,超时 SIGKILL;Windows 暂不支持并文档注明);超时 → `FAIL (timeout)` / JSON `"timed_out":true` + run_fail。e2e 126。
26+
- [ ] **T6 (P6) docs**:"(gtest style)" → framework-agnostic 措辞;记录合成测试名含 `/` 的命名豁免;README/README.zh 同步;CHANGELOG 批次二条目。
27+
- [ ] **T7 版本与回归**:mcpp.toml → 0.0.104;CHANGELOG 未发布 → [0.0.104] — 2026-07-24;全量 e2e(run_all)glibc + 新脚本 musl 双跑。
28+
- [ ] **T8 PR + CI + 合入**:push 分支,gh pr create(标题含 0.0.104),等 CI 全绿,`gh pr merge --squash --admin`
29+
- [ ] **T9 生态验证**:main 重建 musl;mcpp e2e 全量;d2mcpp `e2e.sh all` + d2x mcpp build/test + checker 冒烟,全部以 main 产物执行;结果记录回本文件。

0 commit comments

Comments
 (0)