Foundry 修复解析:通过 Source Remapping 导入合约的动态测试链接与增量重建问题
【免费下载链接】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 test的补丁修复:通过 source remappings 导入的合约(包括被继承的测试合约)在动态测试链接(dynamic test linking)及后续增量重建中的缺陷,以及升级后必须执行的一次性forge test --force重建操作。读完本文,你将理解动态测试链接的底层机制、它与 remapping 交互时的边界条件,以及如何在升级后正确清理旧缓存、避免误判增量编译。
修复概述:一条补丁解决两类症状
该 changelog 条目(.changelog/fix-remapped-test-linking.md)声明了以下修复范围:
forge与foundry-common两个 crate 各发布一个 patch 版本;- 修复动态测试链接(dynamic test linking):当测试合约所引用的目标合约是通过 source remapping 导入时,链接结果不再出错;
- 修复后续增量重建(incremental rebuilds):在第一次构建之后,后续增量编译不再产生错误或遗漏;
- 覆盖“被继承的测试合约”(inherited test contracts):即测试基类同样通过 remapping 导入的场景;
- 升级指引:受影响的版本会在
out/目录中缓存错误的工件,因此升级后需对每个项目执行一次forge test --force强制全量重建。
要理解这条修复,必须先理解动态测试链接本身。
背景:什么是动态测试链接
配置开关与默认值
dynamic_test_linking是 Foundry 编译配置中的一项布尔开关,定义于 crates/config/src/lib.rs,默认值为true(见同文件第 2927 行的默认实现)。它也可以通过 CLI 的--no-dynamic-test-linking显式关闭,该选项定义于 crates/cli/src/opts/build/core.rs,会在生成配置时把dynamic_test_linking写为false。
# foundry.toml [dynamic_test_linking] # 实际为顶层布尔项,示例写法如下正确的配置写法是顶层布尔项:
dynamic_test_linking = true机制:把“字节码依赖”改写为 cheatcode 调用
动态测试链接的核心思想是:测试代码中凡是依赖目标合约字节码的地方(new Contract、type(Contract).creationCode),都在编译前被预处理改写为对 VM cheatcode(deployCode/getCode)的调用,从而让测试在执行期动态部署/获取目标合约字节码,而不是在编译期静态内联。
这条预处理管线在 crates/common/src/compile.rs 中接线:当dynamic_test_linking开启时,ProjectCompiler::compile会给编译器挂载DynamicTestLinkingPreprocessor预处理插件,随后才执行compiler.compile()。同样使用这一开关的命令还包括forge build(crates/forge/src/cmd/build.rs)、forge test(crates/forge/src/cmd/test/mod.rs)、forge coverage(crates/forge/src/cmd/coverage.rs)、forge selectors(crates/forge/src/cmd/selectors.rs)以及 mutation 测试(crates/forge/src/mutation/orchestrator.rs)。
改写过程实现在 crates/common/src/preprocessor/deps.rs:
PreprocessorDependencies::new通过 Solar 编译器对测试/脚本合约做语义分析,收集“字节码依赖”(BytecodeDependency),其类型包括CreationCode(type(X).creationCode)和New(new X(...),含构造参数、value、salt、try/catch场景);remove_bytecode_dependencies把这些依赖逐一替换为对VmContractHelper接口中deployCode(...)/getCode(...)的调用;- 对带构造参数的目标合约,还会生成
DeployHelper{id}.sol辅助合约与encodeArgs辅助函数,并在源文件末尾追加import {DeployHelper..., encodeArgs...} from "foundry-pp/DeployHelper{id}.sol"。
这就是“动态”二字的由来:测试运行时才决定部署哪个字节码工件,运行时工件查找依赖“运行中测试的上下文”。
缺陷所在:remapping 场景下的链接与增量重建
问题的两个层面
从源码注释可以还原问题的成因。在 crates/common/src/preprocessor/deps.rs 的can_rewrite函数中,有一段关键判断:
// Runtime artifact lookup uses the running test's context, which can differ from the source // containing an inherited helper. Any remapping matching the generated path is therefore // unsafe unless every possible runtime context is known. if remappings.iter().any(|remapping| remapping_matches_path(remapping, generated_path)) { return false; }结合remapping_applies与remapping_matches_path(同文件第 342-359 行)的实现,可以归纳出两个层面的缺陷:
动态链接指向错误工件:当测试通过 remapping(例如
@test/=test/、@src/=src/)导入目标合约时,预处理生成的辅助导入路径(foundry-pp/DeployHelper{id}.sol)与目标路径都可能被 remapping 重定向。由于运行时工件查找使用的是“正在运行的测试的上下文”,而继承的辅助代码来自另一个源文件,两者的 remapping 解析上下文不一致,导致deployCode拿到错误的工件名,测试部署到错误的字节码。增量重建不触发:被 remapping 导入的文件在首次编译后,其“源单元身份”与缓存记录不一致(源码中通过
strip_prefix(root_dir)归一化路径再与source_units比较)。当这些文件发生变化时,Foundry 的增量缓存误判为“未变化”,从而跳过重新编译,导致forge test后续运行时继续使用陈旧的工件——这正是 changelog 中“subsequent incremental rebuilds”所描述的症状。
继承的测试合约为何是重点
PreprocessorDependencies::new中对“候选合约”的判定(第 53-68 行)仅保留位于测试/脚本路径集合中的源文件,而“被继承的测试合约”指测试基类通过 remapping 导入的场景。此时:
- 依赖收集阶段,
is_path_in_dir需要同时兼容相对路径、绝对路径与符号链接路径(第 332-339 行),而 remapped 导入常常解析为绝对或符号链接路径,与编译器输入用的相对路径不一致; - 预处理阶段,被继承测试文件同样被当作“mock 或可重写目标”处理,若其自身还依赖源目录下的目标合约,改写生成的辅助文件与
can_rewrite的判定共同决定了它能否被安全重写; - 修复前,这类继承关系中的任意一环出错都会让整个测试基类的动态链接失效,且失败会在缓存建立后“潜伏”,直到增量场景才暴露。
修复策略与验证方式
修复后的正确行为
修复后,can_rewrite的 remapping 检查作为安全兜底:只要生成的辅助路径或依赖路径命中任何 remapping,且无法证明所有运行时上下文一致,就放弃改写、保留原生字节码依赖(keep_native分支,见 deps.rs 第 114-144 行),从而避免生成错误的辅助代码。换言之,修复不是简单“允许 remapping 场景”,而是让每个 remapping 场景都能被正确分类到“可安全改写”或“保留原生”两条路径之一,杜绝了此前“看似改写了、实则链接错误”的中间态。
升级操作:一次forge test --force
changelog 明确给出升级步骤:
After upgrading, run
forge test --forceonce in each project with artifacts cached by an affected version.
原因在于:受影响版本已经向out/(或自定义cache_path)写入了基于错误上下文生成的工件与缓存条目。普通增量编译会命中这些损坏缓存,继续产出错误结果。--force强制清空并全量重建(对应配置项force,见 crates/config/src/lib.rs),让所有工件在修复后的预处理逻辑下重新生成。此后增量编译即可正常工作。
对使用脚本的项目,crates/forge/tests/cli/script.rs 中的测试注释提示:dynamic_test_linking配置项目前尚未被forge script完全采纳,升级排查时如需最小化变量,可在 script 流程中确认该开关的实际生效范围。
配置与命令速查
| 配置项 / 命令 | 位置 | 说明 |
|---|---|---|
dynamic_test_linking = true | crates/config/src/lib.rs,默认true | 开启测试/脚本的动态字节码链接预处理 |
--no-dynamic-test-linking | crates/cli/src/opts/build/core.rs | 覆盖配置,显式关闭动态测试链接 |
forge test --force | 升级后一次性执行 | 清空受影响版本的损坏工件缓存并全量重建 |
force = true | crates/config/src/lib.rs | 每次运行前强制清理并重建所有工件 |
总结
本条补丁修复的是动态测试链接在与 source remapping 交互时的两个连环缺陷:运行时工件查找上下文不一致导致的链接错误,以及缓存误判导致的增量重建失效。修复的核心手段是在 crates/common/src/preprocessor/deps.rs 中强化can_rewrite的 remapping 安全判定,将“不安全即保留原生依赖”的兜底逻辑落实到位。对于升级用户,最关键的实践动作是在每个缓存过受影响版本工件的项目中执行一次forge test --force,确保损坏缓存被一次性清空,后续增量构建恢复正确。
【免费下载链接】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),仅供参考