Foundry Forge 零 Gas 快照误报修复:forge snapshot --check --tolerance的边界处理与源码解析
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
本篇指南聚焦 Foundry 中
forge snapshot --check --tolerance在 CI 场景下的一个边界问题修复:当被测用例的 gas 消耗为 0(零 gas 测试)时,旧的容差计算会产生错误的 gas 快照差异(false diff)。本文以该修复为主线,结合 crates/forge/src/cmd/snapshot.rs 的实现与测试,讲解快照容差机制的底层原理、零值边界处理逻辑以及如何在项目中正确使用--tolerance,读完即可理解并规避此类误报。
一、背景:gas 快照与容差机制
Foundry 的forge snapshot命令用于生成测试用例的 gas 消耗基线文件(默认.gas-snapshot),而--check模式则将其用于 CI:对比当前测试的 gas 消耗与预存的快照文件,不一致时以退出码 1 失败。为了防止平台差异、编译器版本或微小的执行环境波动导致频繁告警,Foundry 提供了--tolerance参数:
Tolerates gas deviations up to the specified percentage.(容忍不超过指定百分比的 gas 偏差)
该参数定义于 crates/forge/src/cmd/snapshot.rs:
/// Tolerates gas deviations up to the specified percentage. #[arg( long, value_parser = RangedU64ValueParser::<u32>::new().range(0..100), value_name = "SNAPSHOT_THRESHOLD" )] tolerance: Option<u32>,从源码可见其关键约束:
- 取值范围被限制在
0..100(百分比),0表示零容忍,100表示允许任意幅度的相对偏差; - 类型为
Option<u32>,即不传该参数时走严格相等比较路径(见下文within_tolerance的None分支); - 它只作用于
--check模式:在 crates/forge/src/cmd/snapshot.rs 中,--check分支会将self.tolerance传给check(),并依据比较结果决定进程退出码:
} else if let Some(path) = self.check { let snap = path.as_ref().unwrap_or(&self.snap); let snaps = read_gas_snapshot(snap)?; let code = if check(tests, snaps, self.tolerance) { 0 } else { 1 }; std::process::exit(code) }即:全部测试都在容差范围内时check()返回true,进程以0退出(CI 通过);否则以1退出(CI 失败)。
二、问题复现:零 gas 测试的虚假差异
--tolerance的容差比较按相对百分比进行。在修复之前,比较逻辑存在一个零值边界漏洞:当一个测试用例的 gas 消耗为0(例如函数体为空、所有操作在编译期被折叠、或使用gasleft前即返回的极简用例),而快照中的基准值同样为0时,旧逻辑会因“除以零”或0/0的未定义语义,产生一个不存在的差异(false diff),导致forge snapshot --check --tolerance N在明明没有任何 gas 变化的情况下以退出码 1 失败。
这类零 gas 测试并不罕见:空实现、常量返回、被优化器完全折叠的调用、以及部分纯视图/无状态用例都可能落入此场景。对于使用--tolerance的 CI 流水线来说,这种误报会直接阻塞合并,且难以排查——因为 diff 输出显示“有变化”,实际却一无所有。
三、修复核心:within_tolerance的零值短路
本次修复(见 changelog 条目 .changelog/zero-gas-snapshot-tolerance.md,类型标记为forge: patch)在容差判断函数的零值路径上做了短路处理。核心实现在 crates/forge/src/cmd/snapshot.rs:
/// Returns true of the difference between the gas values exceeds the tolerance /// /// If `tolerance` is `None`, then this returns `true` if both gas values are equal fn within_tolerance(source_gas: u64, target_gas: u64, tolerance_pct: Option<u32>) -> bool { if let Some(tolerance) = tolerance_pct { let (hi, lo) = if source_gas > target_gas { (source_gas, target_gas) } else { (target_gas, source_gas) }; if hi == 0 { // No percentage difference when both values are zero. return true; } let diff = (1. - (lo as f64 / hi as f64)) * 100.; diff < tolerance as f64 } else { source_gas == target_gas } }函数逻辑拆解:
- 严格模式(
tolerance为None):直接比较source_gas == target_gas,0 == 0天然成立,不存在此问题——因此误报只在显式传入--tolerance时触发; - 容差模式:先取出较大值
hi与较小值lo,若hi == 0,说明两个值都为 0(因为hi是两者中的最大值),此时不存在百分比差异,直接返回true,即视为“在容差内”; - 常规路径:按
(1 - lo/hi) * 100计算相对偏差百分比,与容差阈值比较。
这里的修复关键在于先判断hi == 0再进入除法,从而避免了对零的浮点除法。修复前的行为可以推断为:当hi == 0时,表达式lo / hi触发除零(在浮点语义下产生NaN或无穷大),diff < tolerance的比较结果不确定,从而把完全一致的(0, 0)对错误地判为超差。
四、配套修复:diff 输出中的零值百分比
同一提交还对--diff模式的百分比计算做了零值一致性处理,位于 crates/forge/src/cmd/snapshot.rs:
/// Determines the percentage change fn gas_diff(&self) -> f64 { let target_gas = self.target_gas_used.gas(); if target_gas > 0 { self.gas_change() as f64 / target_gas as f64 } else if self.source_gas_used.gas() == 0 { // No percentage change when both values are zero. 0.0 } else { // Preserve an unbounded increase from zero. f64::INFINITY } }三种情况各有明确语义:
| 场景 | 返回值 | 含义 |
|---|---|---|
target_gas > 0 | 正常百分比change / target | 常规比较 |
target_gas == 0且source_gas == 0 | 0.0 | 双方都是零,无百分比变化 |
target_gas == 0且source_gas > 0 | f64::INFINITY | 从零增长到正数,视为无界的百分比增长 |
可以看到,修复不仅消除了零值误报,还保留了“从零增长”这一真实 gas 回归的检测能力:当基准快照为0而新执行消耗了 gas 时,百分比被报告为无穷大,确保这种回归不会被误判为“无变化”。这与within_tolerance中“两个零相等则通过、零对正数仍按实值比较”的策略相互呼应,构成完整的零值语义闭环。
五、测试验证:单元测试与 CLI 集成测试
5.1 单元测试:零值容差断言
修复的正确性由 crates/forge/src/cmd/snapshot.rs 的单元测试固化:
#[test] fn test_tolerance() { assert!(within_tolerance(100, 105, Some(5))); assert!(within_tolerance(105, 100, Some(5))); assert!(!within_tolerance(100, 106, Some(5))); assert!(!within_tolerance(106, 100, Some(5))); assert!(within_tolerance(100, 100, None)); assert!(within_tolerance(0, 0, Some(5))); }其中最后一行assert!(within_tolerance(0, 0, Some(5)))正是本次修复的回归断言:零 gas 与零 gas 在 5% 容差下必须视为一致。其余断言验证了:
- 容差是对称的(
(100, 105)与(105, 100)结果一致); - 严格边界语义:
5%容差下100 → 105通过,而100 → 106(超出 5%)不通过; None容差即严格相等。
5.2 CLI 集成测试
快照整体流程的端到端行为由 crates/forge/tests/cli/cmd.rs 中的forgetest!系列用例覆盖,包括:
can_check_snapshot:验证forge snapshot基本写入与校验流程;snapshot_check_writes_diff_to_stderr:预置空快照后执行snapshot --check,断言其以失败退出,且“No matching snapshot entry found for ...”(未找到对应快照条目)的报告写入stderr而非 stdout,便于 CI 日志抓取;snapshot_reports_when_snap_file_is_not_written:测试失败时快照文件不被写入并给出明确错误提示;snapshot_expands_invariant_campaign_predicates、snapshot_diff_summary_on_stderr:分别覆盖不变量谓词展开与 diff 摘要输出通道。
六、快照文件格式:三类条目
理解容差比较的前提是读懂.gas-snapshot文件。其条目解析由正则 RE_BASIC_SNAPSHOT_ENTRY 与 GasSnapshotEntry 完成,共三种形态:
// 普通单元测试:固定 gas 消耗 Test:deposit() (gas: 7222) // 模糊测试:runs(轮数)、μ(均值)、~(中位数) Test:deposit() (runs: 256, μ: 100, ~: 200) // 不变量测试:runs(轮数)、calls(调用数)、reverts(回滚数) ERC20Invariants:invariantBalanceSum() (runs: 256, calls: 3840, reverts: 2388)解析测试(crates/forge/src/cmd/snapshot.rs#L624-L695)分别验证了三种条目的解析结果。注意容差比较针对的是TestKindReport汇总出的 gas 值(模糊/不变量测试使用其均值或中位数等报告值),因此零 gas 修复对这三类条目同样生效。
七、使用建议与影响面
7.1 实战用法
在 CI 或本地执行快照校验时,为需要弹性阈值的项目加入容差:
# 生成基线(首次或基线更新时) forge snapshot # 以 5% 容差校验,未超出范围则以退出码 0 结束 forge snapshot --check --tolerance 5 # 指定快照文件(默认 .gas-snapshot) forge snapshot --check --snap .gas-snapshot --tolerance 5相关配置项也暴露在foundry.toml中,见 crates/config/src/lib.rs:
# 快照存放目录 snapshots = "snapshots" # 是否在测试后检查与既有快照的差异 gas_snapshot_check = false # 是否将 gas 快照写入磁盘 gas_snapshot_emit = true默认值(crates/config/src/lib.rs)为gas_snapshot_check = false、gas_snapshot_emit = true,即默认只生成不校验;启用校验可配合 CI 门禁使用。此外,crates/forge/src/cmd/snapshot.rs#L102-L107 显示forge snapshot默认使用静态 fuzz 种子(STATIC_FUZZ_SEED)以保证快照可复现,可通过--fuzz-seed覆盖。
7.2 影响范围
本次变更标记为forge: patch,属于补丁级修复,影响面限定在forge snapshot --check --tolerance路径。修复后的行为总结:
(0, 0)对:不再产生误报,CI 正常通过;(0, N>0)对:按实际 gas 差值比较(diff 模式报告无穷大百分比),真实回归仍然可见;(N>0, M>0)对:原有相对百分比容差逻辑保持不变。
若你的项目存在零 gas 测试且正在使用--tolerance做 CI 门禁,升级到包含该修复的版本即可消除此类噪声;若你希望严格约束所有 gas 变化,则无需传入--tolerance,此时走source_gas == target_gas的严格相等路径。
八、小结
forge snapshot --check --tolerance的容差机制本质是相对百分比比较,而零值正是百分比计算的边界陷阱。本次修复通过within_tolerance中的hi == 0短路,将“双方皆零”明确界定为“无差异”,同时在gas_diff中保留了“从零增长”的无穷大语义,兼顾了误报消除与真实回归检测。配套的单元测试与 CLI 集成测试为这一行为提供了长期保障,使其成为 CI gas 门禁中稳定、可预期的一环。
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考