跳到主要内容

G5 测试约定

无 oracle 仿真如何用守恒、must_use、mutation 三招保正确性

核心要点

  • 新增守恒探测器必带 mutation 验证
  • must_use 候选有明确筛选规则
  • mutation 验证步骤标准化,防主观判断
  • 公约写一次,每个 PR 执行

名词定义

跨文档共享名词见 4.1 总览 名词定义。以下为本文专属名词:

名词定义
must_useRust 属性,强制调用方处理返回值,防止静默丢弃事件(如 ISSUE-034/036 路径)
守恒漂移守恒等式未随代码演化同步更新导致的假阳——系统引入新事件路径但等式 term 没变
事件出口完整性守恒等式必须枚举所有合法事件出口(含 cancel/reschedule),不假设"当前无此路径"
production-path 测试走真实模块入口 API(不绕过主流程直接改字段)触发 stat 累加的测试
反例测试手构造违反守恒的 stats,验证 assert_conservation 在违规输入下确实 panic
Golden cell锁定为回归基线的代表性配置(跨拓扑 × 路由 × 规模 × 消息量),存于 baseline.json
proptestRust 属性测试框架,用显式策略对象描述输入空间,缩小比基于类型的 QuickCheck 更精确

适用范围

核心问题:哪些操作必须遵守本公约?

本公约适用于 G5 仿真器(perfmodel/evaluation/g5/)的以下三类操作:

  • 新增 G5 测试:新写测试函数或测试模块时,守恒探测器公约和 must_use 候选公约适用
  • 改动 G5 测试:修改已有测试逻辑时,mutation 验证方法学适用
  • review G5 PR:reviewer 按本公约检查 PR description 中的 mutation 验证表

历史教训说明了为什么需要这三条公约

  • ISSUE-034 / ISSUE-036 路径:返回值被调用方忽略(未 schedule 入 kernel),事件静默丢失,仿真提前终止但测试照样绿。直接动因:must_use 候选筛选不系统。
  • Phase 1 credit 守恒 by-construction 教训:Phase 1 初期 credit 守恒通过代码结构"默认不会违反"而非探测器显式检查来保证,结果无法在测试中验证守恒确实被感知。直接动因:守恒探测器公约不存在。
  • Phase 2A Task 1a 手动 mutation 经验:Phase 2A 手动注释插桩点并验证测试会 fail,有效但靠个人执行,缺乏标准化步骤和记录格式。直接动因:mutation 验证方法学未制度化。

守恒探测器公约

核心问题:新增 conservation stat 怎么保证它能真探测 bug?

守恒探测器公约的前提是:把值写进 CONSERVATION_KEYS 不等于探测器有效。 探测器是否能在真实 bug 发生时触发断言,取决于以下 4 条公约全部满足。

公约 1:守恒等式数学推导

新增守恒 stat 时,必须在代码注释或 PR description 中写出完整的守恒等式,说明每个 term 的来源:

  • init:初始状态下存在的量(如初始 credit 数)
  • consumed:被消耗的量(如 TX 侧发出的 credit)
  • returned:被归还的量(如 RX 侧 CreditReturn 事件确认的 credit)
  • held:当前仍在途未结算的量

等式形式:init + returned - consumed - held = 0(结算完毕时 held = 0)。

等式中每个 term 必须映射到代码中的具体变量或累加点。不能只写"符合守恒"而不给出等式。

示例(cbfc_residue 守恒,Phase 2A 实做)

// cbfc credit 守恒等式:
// consumed(TX 发出)- returned(RX CreditReturn 确认)= residue
// 仿真终止时 residue 应为 0:所有发出的 credit 都被 CreditReturn 确认
// per-pair 聚合:residue(src, dst, vc) = Σconsumed - Σreturned

子公约 1.a:事件出口完整性(ISSUE-038 教训)

等式中必须枚举所有合法事件出口,不能假设"当前实现无 X 路径"就省略 term。系统演化会引入新出口(如 ISSUE-036 修复时引入 cancel_timer),等式没相应更新就会守恒漂移——误报真 bug 不存在的违反,反过来让真 bug 隐藏在噪声里。

反例(event_balance 漂移,Phase 1 Task 5 → ISSUE-038)

Phase 1 Task 5 引入 kernel 事件守恒时假设"G5 无合法 cancel",等式定义:

event_balance = enqueued - dequeued  // 假设无 cancel

ISSUE-036 修复后 PAXI 大量使用 cancel_timer(retry disarm/rearm + dispatch reschedule),合法 cancel 数量从 0 上升到 3000-163000+,但等式没更新 → 守恒 stat 在所有 alltoall 场景虚高(被 Phase 2C MR + Golden 联合发现)。

正确等式

// kernel 事件守恒:完整枚举三类出口
event_balance = enqueued - dequeued - cancel_skipped
// dequeued:被处理的事件
// cancel_skipped:被合法 cancel_timer 跳过的事件(PAXI retry / SegmentDispatch reschedule)

操作要求

  • 写守恒等式时,列出该 stat 涉及的所有路径(含异常路径、cancel 路径、超时路径),逐一确认是否进出等式
  • 系统引入新事件路径时(PR 含 kernel.schedule_* / kernel.cancel_* / 新 Event::* variant)必须 review 现有守恒等式是否需要新增 term
  • review checklist:grep events_enqueued / events_dequeued / cancel_skipped 等计数点位,确认 PR 是否新增了未计入等式的路径

公约 2:production-path 测试

守恒 stat 必须有 ≥ 1 个测试走真实 API 路径(不能只测 helper 内部行为),验证 stat 的累加点确实被触发。

具体要求

  • 测试必须通过正式的模块入口(如 Paxi::process_eventRcLinkTx::try_send)触发 stat 累加,不能绕过主流程直接修改字段
  • 测试结束时断言 CONSERVATION_KEYS 中的 stat 值符合预期(非 0 且有意义)

示例(cbfc production-path 测试模式)

// 走真实 API 路径触发 credit 消耗与归还
let events = tx.try_send(frame, now);
for event in events { kernel.schedule(event); }
// ... 推进仿真至 CreditReturn ...
assert_conservation(&stats); // 守恒等式验证

公约 3:Mutation 验证文档

PR description 中必须有一张 mutation × test 对应表,格式如下:

插桩点位(注释掉哪行)预期 fail 的测试
rc_link_tx.rs:NNN credits_consumed += 1(accept 分支)test_credit_conservation_basic
rc_link_rx.rs:NNN credits_returned += 1(CreditReturn 处理)test_credit_conservation_basic
......

表中每个插桩点必须有且只有一个对应测试(多余的覆盖是好事,但至少要列最直接的那个)。

验证方式:提 PR 前,PR 作者本地临时注释掉各插桩点,确认对应测试确实 fail,再恢复。见本文「Mutation 验证方法学」节。

公约 4:反例测试

守恒 helper(如 assert_conservation)本身必须有一个反例测试:构造违反守恒的 stat(非 0 残差),验证 helper 在该输入下 panic。

具体要求

  • 反例测试不依赖真实仿真运行,直接手构造 stats HashMap
  • 测试用 #[should_panic(expected = "conservation violated")] 注解
  • panic message 应包含违反守恒的 stat 名和具体值,便于 debug 定位

示例(Phase 2A Task 1 cbfc_residue 反例测试模式)

#[test]
#[should_panic(expected = "conservation violated")]
fn test_assert_conservation_panics_on_nonzero_cbfc_residue() {
let mut stats = /* 构造 good stats */;
stats.set("multi_chip.cbfc_residue_max", 6.0); // 注入违规
assert_conservation(&stats); // 必须 panic
}

must_use 候选公约

核心问题:哪些方法应该加 #[must_use]

#[must_use] 的目的不是装饰,而是防止调用方忽略返回值导致静默错误。 以下 3 条筛选规则确定哪些函数属于候选。

筛选规则 1:返回类型不自带 must_use

Rust 标准库中 Result<T, E> 已自带 #[must_use],不需要重复添加。需要手动标注的类型包括:

  • bool:布尔返回值容易被忽略(如"是否成功")
  • Vec<Event>:事件列表必须被 schedule,忽略等于丢弃
  • Option<(f64, Event)>:含时间戳的事件,忽略导致仿真推进但事件不触发
  • impl Iterator:延迟求值的迭代器,不消费等于不执行

筛选规则 2:语义要求调用方必须处理

函数返回值属于以下语义之一时,必须加 #[must_use]

  • 调用方有义务 schedule 入 kernel:返回的事件不 schedule 则该时刻的仿真行为不发生(类 ISSUE-034 / ISSUE-036 路径)
  • 调用方有义务留存 queue / buffer:返回值是状态更新的一部分,丢弃导致状态不一致
  • 返回值是决策依据:调用方必须根据返回的 bool 决定后续路径,忽略等于跳过分支

筛选规则 3:must_use message 写不变约束,不写 API 名

#[must_use = "..."] 的 message 必须描述不变约束(违反会导致什么后果),不写具体的 API 调用名。

原因:API 名会随重构改变,但不变约束不会。写 API 名的注释在 rename 后立刻过时且可能误导(Phase 2A code-review F005 教训)。

// [FAIL] 写了具体 API 名,rename 后过时:
#[must_use = "返回的事件必须 schedule_f64_events 入 kernel,否则 RcFrameDone 不触发"]

// [PASS] 写不变约束,永远成立:
#[must_use = "返回的事件必须入 kernel 调度,否则 RcFrameDone 不触发 = 同 ISSUE-036 路径"]

message 中可以引用 ISSUE 编号,帮助读者追溯背景,但不引用具体函数名。

Mutation 验证方法学

核心问题:怎么系统验证守恒探测器的插桩点都被测试覆盖?

手动 mutation 验证是守恒探测器公约第 3 条的执行步骤。 步骤标准化后,验证结果可重现,不依赖个人经验判断。

步骤 1:列出插桩点位

执行公约 3 前,先枚举守恒 stat 的所有累加点。以 cbfc credit 守恒为例,插桩点包括:

  • TX 侧:accept 分支的 credits_consumed += 1
  • TX 侧:nak 分支的 credits_consumed += 1(如有独立分支)
  • RX 侧:CreditReturn 处理的 credits_returned += 1

每个分支对应一个插桩点,不能合并(合并会导致漏测某个分支)。

步骤 2:逐点临时 mutation + 跑测试

对每个插桩点执行以下操作:

  1. 用注释临时屏蔽该 += 1 调用(不删,用注释保留原行便于恢复)
  2. 运行指定测试:cargo test <test_name> --release
  3. 确认测试确实 fail(如果没有 fail,说明该插桩点没有被任何测试覆盖,需要补测试)
  4. 恢复注释,确认测试重新 pass

步骤 3:记录 mutation × test 对应表

每个插桩点验证完成后,填写公约 3 要求的对应表。格式见「守恒探测器公约 § 公约 3」。

步骤 4:可选自动化

cargo-mutants 等工具可以自动执行步骤 2 的逐点 mutation,输出未被覆盖的插桩点列表。本期不强制使用,但对 CONSERVATION_KEYS 较多的场景推荐评估。

自动化工具作为加速手段,不替代步骤 3 的人工记录——对应表仍需由 PR 作者填写,reviewer 审核。

前置阅读

  • 守恒与活性测试范式 → 4.2 测试与验证「守恒与活性:静默丢失的克星」
  • 蜕变测试(Phase 2C 蜕变 MR 上线后将补充对应公约) → 4.2 测试与验证「蜕变测试:用关系替代标准答案」

公约 5:蜕变关系设计要求

核心问题:新增蜕变 MR 时怎样保证它探测能力真实、不假阳?

蜕变测试 (metamorphic) 用"输入变换 → 输出关系"替代标准答案。新增一条 MR 必须满足以下三条,否则探测能力无法验证或假阳频发。

  • 给出关系的数学形式:写明变换 T(input) 与期望关系 R(output, T(output))。例:单调性 MR——消息量翻倍则总延迟不降,形式化为 latency(2×msg) >= latency(msg)
  • >= / <= 而非严格 > / <:事件驱动仿真在 ECMP 哈希噪声 / 整除共振下会有同值或微小逆序,严格不等号必假阳。对称性 MR 用"CV ≤ 理论上限 × 1.5"而非写死 0.05
  • proptest 随机生成 + 守恒断言双层探测:MR 断言 (关系层) 之外,每个随机 cell 仍跑守恒断言 (KPI 层)。两层任一红都暴露 bug——关系对但守恒错,或守恒对但关系违反。

落地:top/multi_chip/mr_tests.rs (4 条 MR:单调 / 对称 / 标度 / 守恒比例),边界条件依据见 4.2 测试与验证

公约 6:Golden cell 选取标准

核心问题:Golden baseline 选哪些 cell、何时更新?

Golden 回归锁定代表性配置的数值,算法改动导致漂移超容差即报红。cell 选取与基线更新遵守:

  • 代表性覆盖:跨拓扑族 (ring / torus / clos / fat-tree) × 路由 (dor / ecmp / dmodk) × 规模 (8 / 16 / 32 chip) × 消息量 (64KB / 256KB) 选 15–20 cell,含已知压力点 (大规模 + 大消息)。
  • baseline 绑定代码版本:baseline.json 存 g5_git_sha,防数值与代码版本错配。
  • per-cell 容差:默认 default_tolerance_pct,数值更稳的 cell 可 per-cell 收紧;容差吸收 run-to-run ULP / eventQueue 抖动,不吸收算法漂移。
  • 基线更新触发条件:仅当偏移是预期改动 (优化 / 公式修正) 时,跑 tools/update_golden.py 重新生成 + commit 附说明;非预期偏移是回归,走 iforge-debug 找根因,不更新 baseline

落地:tests/golden_compare.rs (比对) + src/bin/generate_golden.rs (生成) + src/golden.rs (共享 schema),schema 写读两侧共用防漂移。

后续维护

核心问题:本公约何时会扩展?

本公约随 Phase 推进增补新条款,不修改已有条款(已有条款修改需单独讨论):

  • 已落地 (Phase 2C):公约 5 (蜕变关系设计要求) + 公约 6 (Golden cell 选取标准),对应蜕变 MR 与 Golden baseline 上线。
  • Phase 2C proptest 上线后:must_use 候选公约可参考 proptest 生成器对返回值的利用率数据,反向识别遗漏的 must_use 候选。

Takeaway

知识点核心结论
守恒探测器公约4 条:等式推导 + production-path 测试 + mutation 验证表 + 反例测试,缺一不可
蜕变 MR 设计 (公约 5)给出关系数学形式 + 用 >=/<= 防哈希噪声假阳 + proptest 随机叠守恒断言双层探测
Golden cell 选取 (公约 6)跨拓扑×路由×规模×消息量选 15–20 cell + baseline 绑 git_sha + 非预期偏移走 debug 不更新 baseline
must_use 候选筛选3 规则:返回类型(Vec/bool/Option 等)+ 语义(必须 schedule)+ message 写约束不写 API 名
mutation 验证方法学4 步:列插桩点 → 逐点注释+跑测试 → 填对应表 → 可选自动化
历史教训ISSUE-034/036 静默丢失路径 + Phase 1 by-construction 守恒的盲区,公约的直接来源
守恒漂移ISSUE-038: Phase 1 假设无 cancel → ISSUE-036 引入 cancel_timer → 等式没更新 → 1280-163K event_balance 误报;公约 1.a 事件出口完整性直接来源