Sway E2E VM 测试编写与运行指南:从 test.toml 配置到快照测试的完整实践
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
本文以 Sway 编译器仓库(sway)的端到端虚拟机(E2E VM)测试体系为核心,系统讲解如何编写低依赖、高编译效率的 E2E 测试,如何通过test.toml描述测试行为,以及如何运行、过滤、并行调度和做快照断言。读完本文,你将掌握 Sway 官方测试套件test/src/e2e_vm_tests/的完整使用方式,能够为语言特性、编译器缺陷或新功能贡献高质量的回归测试。
一、E2E VM 测试体系概览
E2E VM 测试是 Sway 编译器最核心的回归测试手段之一:每个测试是一个独立的 Forc 项目,通过一个test.toml描述文件声明测试的类别(编译通过/编译失败/在 VM 中运行/在链上运行等)与预期结果,由test这个 Rust crate 统一发现、编译、执行并断言。其入口位于 test/src/main.rs,核心调度与解析逻辑位于 test/src/e2e_vm_tests/mod.rs。
测试程序的目录结构如下(根目录test/):
test/src/main.rs:testcrate 的 CLI 入口,负责解析命令行参数并调度三类测试(E2E、IR、Snapshot);test/src/e2e_vm_tests/:E2E 测试的调度、解析与执行逻辑;test/src/e2e_vm_tests/test_programs/:所有测试项目本体,按should_pass/、should_fail/、test_asserts/组织;test/src/e2e_vm_tests/reduced_std_libs/:为测试准备的精简版标准库;test/src/snapshot/mod.rs:快照(Snapshot)测试的实现。
测试的发现机制非常直接:discover_test_tomls使用 glob 模式test/src/e2e_vm_tests/test_programs/**/test*.toml递归扫描所有测试目录下的test.toml或test.<feature>.toml文件(见 mod.rs),每个 TOML 文件对应一条测试描述。因此,新增一个 E2E 测试,本质上就是在test_programs下新建一个 Forc 项目并放置test.toml。
二、编写测试的第一原则:最小化依赖、压缩编译时间
E2E 测试套件包含成百上千个测试项目,每个测试都要经历完整的forc build编译流程。在没有增量编译、无法跨测试复用已编译std的情况下,std作为测试依赖被反复编译会带来显著的编译时间开销。因此,编写 E2E 测试的首要准则是尽量削减测试的依赖面。官方指南(即 test/src/e2e_vm_tests/README.md)给出了如下优先级递减的实践路径。
2.1 使用implicit-std = false关闭隐式 std
Forc 项目默认会隐式引入std。如果某个测试(尤其是should_pass/language下的语言特性测试)并不需要std,应在Forc.toml中显式关闭隐式标准库:
[project] name = "my_test" entry = "main.sw" license = "Apache-2.0" implicit-std = false仓库中大量语言特性测试正是这样配置的。例如test_asserts库自身就是implicit-std = false的纯库(见 test_asserts/Forc.toml),这也印证了"能不用 std 就不用"是套件的一贯约定。
2.2 优先使用library而非script项目类型
默认的项目类型script由于编码原因,必然依赖至少一个精简版 std 库sway-lib-std-core(即编译脚本所需的registers、flags、primitives等核心模块)。而library类型的项目没有这一强制依赖,可以把依赖面压到更低。因此指南建议:能用library表达的测试就尽量不要写成script。
2.3 不要为"顺手取一个类型"而引入 std
如果测试只是需要一个任意的类型或 trait,完全没必要导入Option或Hash这类 std 类型。用自定义的占位类型即可,例如:
struct Dummy {} trait Trait {}这样既避免了std的编译开销,也让测试意图更纯粹——它只关注被测的语言特性本身,而不是标准库的某个实现。
2.4 只引入满足需求的最小 std 切片:精简版 std 库
当测试确实需要std的某些功能时,优先使用仓库预置的精简版 std 库,而不是整个sway-lib-std。这些库位于test/src/e2e_vm_tests/reduced_std_libs/,每个库在上一级基础上增加少量模块,按功能从小到大依次为:
| 精简库 | 包含的功能 |
|---|---|
sway-lib-std-core | 编译脚本所需的最小核心模块(registers、flags、primitives、slice、ops、raw_ptr、codec、str、marker、debug等,见 lib.sw) |
sway-lib-std-assert | core全部内容 + 断言(asserting)、日志(logging)、回滚(reverting) |
sway-lib-std-option-result | assert全部内容 +Option、Result |
sway-lib-std-vec | option-result全部内容 +Vec、Iteratortrait、From/Intotraits |
sway-lib-std-conversions | vec全部内容 + intrinsics、Bytes、字节转换、数组转换、原始类型转换 |
这些精简库的编译时间与整个std相比几乎可以忽略不计。若精简库中没有所需模块,才允许引入完整的sway-lib-std。
从源码看,这些精简库并非手写副本,而是由 test/src/reduced_std_libs.rs 在每次运行测试前自动生成的:它读取每个精简库目录下的reduced_lib.config(其中每行列出一个模块名,例如sway-lib-std-assert的配置列出了assert.sw、logging.sw、revert.sw、error_signals.sw、debug.sw等模块),再从sway-lib-std/src把对应模块复制到精简库的src/下。这意味着精简库的内容与主std保持同步,测试者无需手工维护副本。
2.5 只需assert时使用test_asserts库
如果测试仅仅因为需要assert系列函数才想依赖std,指南特别推荐改用测试专用的test_asserts库(位于 test/src/e2e_vm_tests/test_programs/test_asserts/)。它不依赖std,提供两个极简 API(见 lib.sw):
library; pub fn assert_true(revert_code: u64, condition: bool) { if condition { } else { __revert(revert_code); } } pub fn assert_false(revert_code: u64, condition: bool) { if condition { __revert(revert_code); } }使用方式是把test_asserts加入测试项目的[dependencies],例如:
assert_true(11, __eq(a, b)); // 断言 a 等于 b,失败时以 11 作为回滚码 assert_false(22, __eq(a, b)); // 断言 a 不等于 b,失败时以 22 作为回滚码2.6 高内聚分组:一个测试项目承载一个特性的多种情况
除了削减依赖,指南还建议将强相关的用例聚合到同一个测试项目中,而不是拆成多个独立测试。例如,与其建test_some_feature_for_option_a和test_some_feature_for_option_b两个测试,不如只建一个test_some_feature,在内部用多个模块分别覆盖两种情形。这样每个特性只需编译一次项目,显著降低总编译时间。当然,分组必须"有意义",避免人为把无关特性硬凑在一起。
三、test.toml:配置驱动的测试描述
每个 E2E 测试的行为完全由放置在 Forc 项目根目录(与Forc.toml同级)的test.toml描述,字段的完整规范见 test_programs/README.md,解析实现在 mod.rs。
3.1 category:必填,声明测试类别
category是必填字段,取值为以下字符串之一(对应TestCategory枚举):
| 取值 | 含义 | 对应行为 |
|---|---|---|
"run" | 编译并在 VM 中运行 | 编译成功后由runs_in_vm在 Fuel VM 中执行 |
"run_on_node" | 编译并在本地 Fuel Core 节点上运行 | 先部署contracts指定的合约,再执行并检查收据 |
"compile" | 只要求编译成功,不运行 | 校验编译产物与 ABI/存储槽 JSON |
"unit_tests_pass" | 编译且所有单元测试通过 | 通过forc test语义运行 |
"fail" | 预期编译失败 | 编译必须失败,且必须附带 FileCheck 校验 |
"disabled" | 该测试被禁用 | 被跳过不执行 |
3.2 expected_result:run / run_on_node 类测试必填
expected_result是一个表格,包含action与value两个字段:
action取值:"return":VM 成功返回的整数值;"return_data":VM 返回的字节数组(value为十六进制字符串,注意RETD操作码返回的是内存区间);"result":Fuel Core 节点返回的整数字(用于run_on_node);"revert":VM 失败回滚返回的整数值。
value:对于return、result、revert是整数;对于return_data是 0~255 的字节数组(十六进制字符串)。
仓库真实示例(array_basics/test.toml):
category = "run" expected_result = { action = "return", value = 1 } expected_result_new_encoding = { action = "return_data", value = "01" } validate_abi = true expected_warnings = 1注意这里还出现了expected_result_new_encoding与category_new_encoding——当启用 new encoding 实验特性时,测试可分别为新旧编码声明不同的类别与预期结果(见 mod.rs 中对category_new_encoding、script_data_new_encoding的兼容处理)。这类字段在解析时也支持test.<feature>.toml后缀变体:如果存在后缀变体文件,会以test.toml为基底、用变体文件覆盖/补充字段(见 mod.rs)。
3.3 contracts:run_on_node 测试部署的合约
run_on_node测试通常需要先把一个或多个合约部署到节点,再部署并运行被测代码。contracts为字符串数组,元素是相对于test/src/e2e_vm_tests/test_programs目录的 Forc 项目路径:
category = "run_on_node" expected_result = { action = "result", value = 11 } contracts = ["should_pass/test_contracts/test_contract_a", "should_pass/test_contracts/test_contract_b"]从源码看,合约部署在TestContext::deploy_contract中实现:每个(合约路径,编码模式)组合只部署一次并缓存进ContractId(见 mod.rs),避免同一合约被重复部署。而run_on_node测试使用的签名密钥来自fuel-core仓库chain-config中预置的测试网钱包私钥(见 mod.rs)。
3.4 validate_abi 与 validate_storage_slots
validate_abi = true:要求校验测试生成的 ABI JSON 与仓库中的 oracle 文件(json_abi_oracle.debug.json/json_abi_oracle.release.json等)一致;validate_storage_slots = true:要求校验存储槽 JSON 与 oracle 一致。
这两类 oracle 文件的更新可通过命令行--update-output-files一键重新生成。
3.5 supported_targets 与 unsupported_profiles
supported_targets:数组,声明该测试支持的构建目标。默认为空数组时按 Fuel VM 目标处理(见 mod.rs 的默认回退逻辑);unsupported_profiles:数组,声明不兼容的构建 profile(release/debug)。默认情况下测试会跑全部 profile,存在不兼容时用此字段排除。
3.6 expected_warnings
若测试允许一定数量的合法告警,可设expected_warnings = <整数>。实际编译产生的告警数超过该值,测试即失败(断言逻辑见 mod.rs)。
3.7 其他字段
script_data:run/run_on_node测试的脚本输入数据,十六进制字符串,会被解码为字节(空格会被忽略);witness_data:可选的见证数据数组,每项为十六进制字符串;expected_decoded_test_logs:仅unit_tests_pass类别可用,声明期望解码出的测试日志值列表(用于校验forc test的日志输出);experimental:表类型,逐项开关实验特性,此时将忽略 CLI 传入的实验特性标志(见 mod.rs);logs:字符串,可向编译命令注入日志选项。
3.8 完整示例
一个典型的should_pass/language测试,要求"编译成功、在 VM 中运行、返回 42、并校验 ABI":
category = "run" expected_result = { action = "return", value = 42 } validate_abi = true一个需要返回数据的示例(RETD返回内存区间,value 是十六进制字节序列):
category = "run" expected_result = { action = "return_data", value = "0000000003ffffc400000000000000040000000000000003" } validate_abi = true四、fail 测试与 FileCheck 模式校验
fail类别(即should_fail/目录)的测试必须附带 FileCheck 校验指令,否则会在解析阶段直接报错(见 mod.rs:'fail' tests must contain some FileCheck verification directives.)。FileCheck 指令以注释形式写在test.toml中(以#开头的行),常用的有check、nextln等:
category = "fail" # check: // this asm block should return unit, i.e. nothing # nextln: asm(r1: 5) { # check: $()Mismatched types. # nextln: $()expected: () # nextln: $()found: u64. # nextln: $()help: Implicit return must match up with block's type.一个必须注意的坑:编译器输出带 ANSI 颜色转义序列,会干扰 FileCheck 按"词边界"匹配,尤其错误信息第一个词常被转义序列前缀污染。解决办法是不要匹配错误消息的第一个词,或用空字符串模式$()显式声明模式起点,例如:
# check: $()The imported symbol "S" shadows another symbol with the same name.FileCheck 的解析与执行在 mod.rs(FileCheck::build解析指令并拒绝未知指令)与 mod.rs(check_file_checker对编译输出执行匹配)中实现。
五、运行 E2E VM 测试
5.1 前置条件
本指南假设本地已有一个fuel-core节点运行在默认端口上(run_on_node类测试需要它;纯run、compile、fail类测试则不需要)。启动节点后,另开一个终端进入sway仓库根目录执行测试命令。
5.2 运行全部测试
cargo run --bin=testtest是test/crate 的可执行文件名(binary 名为test)。测试套件跑完后,会看到类似输出:
Tests passed. _n_ tests run (0 skipped)默认cargo使用 debug 构建模式,执行较慢。要显著提速(官方称可达一个数量级),改用 release 模式:
cargo run --release --bin=test5.3 运行特定测试
testcrate 支持用正则表达式过滤测试名。在sway根目录执行:
cargo run --bin=test -- specific_tests_pattern例如只运行名字中包含abi_impl的测试:
cargo run --bin=test -- abi_impl输出形如:
Finished dev [unoptimized + debuginfo] target(s) in 0.66s Running `target/debug/test abi_impl` Compiling should_fail/abi_impl_purity_mismatch Compiling should_fail/abi_impl_purity_mismatch Compiling should_fail/too_many_abi_impl_methods Compiling should_fail/too_many_abi_impl_methods Compiling should_fail/abi_impl_pub_fn Compiling should_fail/abi_impl_pub_fn Compiling should_fail/abi_impl_arity_mismatch Compiling should_fail/abi_impl_arity_mismatch _________________________________ Tests passed. Ran 4 out of 322 E2E tests (0 disabled). No IR generation tests were run. Regex filter "abi_impl" filtered out all 48 tests.小技巧:如果先cd到sway/test目录,可以简写为cargo run [pattern](省略--bin=test --)。
5.4 查看详细输出
默认只打印测试名与通过/失败状态。要打印编译告警、错误及 print 类选项的输出,从sway/test目录执行:
SWAY_TEST_VERBOSE=true cargo run [pattern]verbose选项对应 CLI 参数--verbose,并绑定环境变量SWAY_TEST_VERBOSE(见 main.rs)。注意 verbose 类选项在并行运行模式下会被忽略。
5.5 完整 CLI 参数
testcrate 的 CLI 由 clap 定义(见 main.rs),除正则位置参数外还支持:
| 参数 | 别名 | 作用 |
|---|---|---|
-e, --exclude <REGEX> | — | 排除匹配该正则的测试 |
--skip-until <REGEX> | — | 跳过所有测试,直到命中该正则的测试为止 |
--abi-only | abi | 只运行校验 ABI JSON 输出的测试 |
--storage-only | storage | 只运行校验存储槽 JSON 输出的测试 |
--no-std-only | no_std | 只运行无std依赖的测试 |
--contract-only | contract | 只运行需要部署合约(run_on_node)的测试 |
--forc-test-only | forc-test | 只运行执行forc test(unit_tests_pass)的测试 |
--first-only | first | 只运行第一个测试 |
--perf-only | — | 只运行会产生性能数据(gas 用量与字节码大小)的测试 |
-v, --verbose | — | 打印告警、错误与 print 选项输出(并行时忽略) |
-r, --release | — | 以 release 模式编译 Sway 代码 |
--locked | — | CI 用,确保测试锁文件是最新的 |
--build-target <TARGET> | target | 指定构建目标,默认 Fuel |
--update-output-files | — | 更新所有 oracle 输出文件 |
--print-ir <opts> | — | verbose 时打印指定 IR |
--print-asm <opts> | — | verbose 时打印指定 ASM |
--print-bytecode | — | verbose 时打印最终字节码 |
-k, --kind <e2e\|ir\|snapshot\|all> | — | 只运行指定类型的测试(可多选,默认 all) |
-s, --sequential | — | 串行运行(而非并行) |
--write-output | — | 把编译产物写入各测试目录的out/,便于排查失败 |
--perf | — | 收集 gas 用量与字节码大小并写入test/perf_out |
--gas-costs <SRC> | — | gas 成本值来源:built-in/mainnet/testnet/ 本地 JSON 文件路径 |
--exact <PATH> | — | 只运行指定绝对路径的单个test.toml(内部并行调度用,隐藏参数) |
六、并行与串行:测试调度机制
默认情况下 E2E 测试是并行执行的(main使用current_thread版 Tokio 运行时,见 main.rs)。并行执行通过rayon的par_iter对测试列表做并行遍历,每个测试以子进程方式重新调用当前可执行文件并传入--exact <test.toml 路径>(见 mod.rs),从而把"当前测试"与"进程内共享状态"隔离。
run_on_node类测试在并行下有特殊处理(见 mod.rs):
- 不共享合约的
run_on_node测试按钱包(签名密钥)分组并行——每个测试使用不同钱包,避免交易冲突; - 共享同一合约路径的
run_on_node测试只能串行执行,因为并行子进程之间无法共享TestContext的合约部署缓存,会造成同一合约被重复部署。
若想关闭并行、逐条稳定复现顺序,可加-s/--sequential;此时TestContext会在进程内缓存已部署合约(每个合约路径+编码只部署一次)。
七、Snapshot 快照测试
当一个 E2E 测试目录中存在snapshot.toml文件时,该测试还会作为cargo insta快照测试运行(实现见 test/src/snapshot/mod.rs)。快照测试会把一组命令的完整输出保存为快照文件(stdout.snap),之后运行时与快照比对,防止输出意外漂移。
运行方式有三种:
cargo r -p test # 常规运行(包含快照) cargo t -p test # 通过 cargo test 运行 cargo insta test # 通过 insta 运行快照的审阅采用标准的cargo insta工作流:运行cargo insta review(或cargo insta accept/cargo insta reject)交互式地接受或拒绝变更。
文档最初记载snapshot.toml尚无可配置项、只是空文件;而从当前源码看(见 snapshot/mod.rs),空文件会回退为默认命令cmds = [ "forc build --path {root}" ],且已支持自定义cmds数组。仓库真实示例(array_repeat/snapshot.toml):
cmds = [ "forc build --path {root} --ir final --asm final --bytecode --release", "forc test --path {root} --verbose --release", ]其中{root}会被替换为测试项目相对仓库根的路径,{name}替换为测试名。支持的子命令包括echo、forc、forc doc、forc migrate、replace-file、patch-bin、fuel-vm,以及用于输出过滤的sub、regex、filter-fn(例如filter-fn可只摘取指定 IR/ASM 函数的输出到快照);cmds项还支持以{ repeat = "for-each-block", cmds = [...] }表的形式,对源码中每个/* START block_name */ ... /* END block_name */代码块分别执行命令并生成快照。输出会经过clean_output归一化(去除 ANSI 转义、绝对路径与编译耗时),保证快照在不同机器上可复现。
八、性能数据收集
E2E 套件还承担着编译性能回归检测的职责。加--perf运行后,run、compile、unit_tests_pass三类测试会记录各自编译产物的字节码大小与(脚本或单元测试的)gas 用量,最终以 CSV 形式写入test/perf_out/目录,文件名形如<时间戳>-e2e-<类型>-<profile>-<分支名>.csv(见 mod.rs)。--gas-costs参数则控制 gas 成本值来源:默认built-in(即随 forc 版本发布的 Fuel 主网 gas 成本值),也可实时从mainnet/testnet拉取,或指定本地 JSON 文件。
结语
Sway 的 E2E VM 测试体系是一套"配置驱动、依赖精简、并行高效"的回归测试框架:test.toml用声明式的方式描述测试意图,implicit-std = false与精简 std 库把编译成本压到最低,FileCheck 与快照机制分别守护编译错误消息与命令输出的稳定性,而正则过滤与并行调度让上千条测试可以快速、精准地运行。无论是为语言新特性补充should_pass用例,还是为某个编译器缺陷添加should_fail回归,遵循本文的依赖纪律与配置规范,都能写出既快又稳的 E2E 测试。
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考