Sway E2E VM 测试编写与运行指南:从 test.toml 配置到快照测试的完整实践
2026/9/12 9:26:33 网站建设 项目流程

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.rstestcrate 的 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.tomltest.<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(即编译脚本所需的registersflagsprimitives等核心模块)。而library类型的项目没有这一强制依赖,可以把依赖面压到更低。因此指南建议:能用library表达的测试就尽量不要写成script

2.3 不要为"顺手取一个类型"而引入 std

如果测试只是需要一个任意的类型或 trait,完全没必要导入OptionHash这类 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编译脚本所需的最小核心模块(registersflagsprimitivessliceopsraw_ptrcodecstrmarkerdebug等,见 lib.sw)
sway-lib-std-assertcore全部内容 + 断言(asserting)、日志(logging)、回滚(reverting)
sway-lib-std-option-resultassert全部内容 +OptionResult
sway-lib-std-vecoption-result全部内容 +VecIteratortrait、From/Intotraits
sway-lib-std-conversionsvec全部内容 + intrinsics、Bytes、字节转换、数组转换、原始类型转换

这些精简库的编译时间与整个std相比几乎可以忽略不计。若精简库中没有所需模块,才允许引入完整的sway-lib-std

从源码看,这些精简库并非手写副本,而是由 test/src/reduced_std_libs.rs 在每次运行测试前自动生成的:它读取每个精简库目录下的reduced_lib.config(其中每行列出一个模块名,例如sway-lib-std-assert的配置列出了assert.swlogging.swrevert.swerror_signals.swdebug.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_atest_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是一个表格,包含actionvalue两个字段:

  • action取值:
    • "return":VM 成功返回的整数值;
    • "return_data":VM 返回的字节数组(value为十六进制字符串,注意RETD操作码返回的是内存区间);
    • "result":Fuel Core 节点返回的整数字(用于run_on_node);
    • "revert":VM 失败回滚返回的整数值。
  • value:对于returnresultrevert是整数;对于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_encodingcategory_new_encoding——当启用 new encoding 实验特性时,测试可分别为新旧编码声明不同的类别与预期结果(见 mod.rs 中对category_new_encodingscript_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_datarun/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中(以#开头的行),常用的有checknextln等:

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类测试需要它;纯runcompilefail类测试则不需要)。启动节点后,另开一个终端进入sway仓库根目录执行测试命令。

5.2 运行全部测试

cargo run --bin=test

testtest/crate 的可执行文件名(binary 名为test)。测试套件跑完后,会看到类似输出:

Tests passed. _n_ tests run (0 skipped)

默认cargo使用 debug 构建模式,执行较慢。要显著提速(官方称可达一个数量级),改用 release 模式:

cargo run --release --bin=test

5.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.

小技巧:如果先cdsway/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-onlyabi只运行校验 ABI JSON 输出的测试
--storage-onlystorage只运行校验存储槽 JSON 输出的测试
--no-std-onlyno_std只运行无std依赖的测试
--contract-onlycontract只运行需要部署合约(run_on_node)的测试
--forc-test-onlyforc-test只运行执行forc testunit_tests_pass)的测试
--first-onlyfirst只运行第一个测试
--perf-only只运行会产生性能数据(gas 用量与字节码大小)的测试
-v, --verbose打印告警、错误与 print 选项输出(并行时忽略)
-r, --release以 release 模式编译 Sway 代码
--lockedCI 用,确保测试锁文件是最新的
--build-target <TARGET>target指定构建目标,默认 Fuel
--update-output-files更新所有 oracle 输出文件
--print-ir <opts>verbose 时打印指定 IR
--print-asm <opts>verbose 时打印指定 ASM
--print-bytecodeverbose 时打印最终字节码
-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)。并行执行通过rayonpar_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}替换为测试名。支持的子命令包括echoforcforc docforc migratereplace-filepatch-binfuel-vm,以及用于输出过滤的subregexfilter-fn(例如filter-fn可只摘取指定 IR/ASM 函数的输出到快照);cmds项还支持以{ repeat = "for-each-block", cmds = [...] }表的形式,对源码中每个/* START block_name */ ... /* END block_name */代码块分别执行命令并生成快照。输出会经过clean_output归一化(去除 ANSI 转义、绝对路径与编译耗时),保证快照在不同机器上可复现。

八、性能数据收集

E2E 套件还承担着编译性能回归检测的职责。加--perf运行后,runcompileunit_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询