Aptos MonoMove 差分测试套件(mono-move-testsuite)完全指南:V1 与 V2 VM 的双轨端到端验证
2026/9/18 10:11:15 网站建设 项目流程

Aptos MonoMove 差分测试套件(mono-move-testsuite)完全指南:V1 与 V2 VM 的双轨端到端验证

【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core

本文是 aptos-core 仓库中third_party/move/mono-move/testsuite(包名mono-move-testsuite)的实战技术指南。该测试套件是 MonoMove(v2)VM 项目质量保障的核心:每个 Move 源码或汇编输入都会同时在传统 MoveVM(V1)与 MonoMove(v2)上运行并对比行为,同时支持将 specializer 的 golden 输出(bytecode、stackless IR、micro-ops)固化为基线。读完本文,你将掌握测试目录组织方式、// RUN:/// CHECK:指令系统的完整语义、.exp基线的更新机制(UPBL=1),以及如何基于 tests/differential.rs 与 src/runner.rs 理解测试的执行管线,从而为 MonoMove 新增功能或修复缺陷时写出合格的差分用例。

1. 什么是 mono-move-testsuite

MonoMove 是 aptos-core 中对 Move 字节码解释器的重新实现(v2),其核心目标是与既有 V1 VM 保持行为一致性。mono-move-testsuite正是为验证这一目标而设计的端到端差分(differential)测试套件:一个输入文件,两份执行结果,强制比对。

核心机制如下(引自 AGENTS.md):

End-to-end differential tests for the MonoMove VM: each Move source (or assembly) input runs on both the V1 VM and MonoMove (v2), and their behavior is compared. Inputs may also pin specializer golden output (bytecode, stackless IR, micro-ops).

即每个输入同时满足两类约束:

  • 行为一致性:V1 与 V2 对同一输入的执行结果必须一致(返回值、事件、错误路径);
  • specializer golden 固定:输入还可以将--print请求的中间表示快照(字节码反汇编、stackless IR、micro-ops)固化为基线文件,防止编译器/降级器行为漂移。

2. 框架与测试发现机制

测试使用datatest-stable(数据驱动测试框架),用例全部位于tests/test_cases/differential/目录下。测试入口在 tests/differential.rs:

let files: Vec<(String, SourceKind)> = WalkDir::new("tests/test_cases/differential") .follow_links(false) .min_depth(1) .into_iter() .flatten() .filter_map(|e| { let path = e.path(); let kind = path .extension() .and_then(|ext| ext.to_str()) .and_then(SourceKind::from_extension)?; Some((path.display().to_string(), kind)) }) .collect();

从源码可见,发现规则是按扩展名选择输入类型(见 src/compile.rs):

扩展名SourceKind编译方式
.moveMove使用move-compiler-v2(v2 编译器)编译,见compile_move_source
.masmMasm使用move-asm汇编器汇编为单一模块,见assemble_masm_source

对应的编译入口(src/compile.rs):

pub fn compile(source: &str, kind: SourceKind) -> Result<Vec<CompiledModule>> { match kind { SourceKind::Move => compile_move_source(source), SourceKind::Masm => assemble_masm_source(source).map(|m| vec![m]), } }

Cargo.toml中,该测试被声明为harness = false的自定义测试目标,由differential.rsmain自行调度(Cargo.toml):

[[test]] name = "differential" harness = false

实际用例覆盖了十分广泛的语言特性目录,均位于 tests/test_cases/differential/:arithmetic(算术与窄整型宽度)、boolcalls(跨模块调用与多返回值)、castsclosuresconstantscontrol_flowenumsgenerics(泛型实例化)、global_storagenativesoptimizerprogramsrefsstructsvectors

3. 指令系统(Directives)完全解析

每个输入文件通过嵌入的行内注释// RUN:指令驱动测试管线。指令的解析实现在 src/parser.rs,其文件头注释即为最权威的语义文档。一个测试文件是有序的 publish 与 execute 步骤序列,每个 execute 步骤可附带若干 check 指令。

3.1// RUN: publish [--print(<sections>)]— 发布模块

publish之后的所有非指令行(直到下一个// RUN:或文件末尾)会被原样收集为 Move 源码文本,编译成一个或多个模块,并同时发布到两个 VM 的测试存储中。多个 publish 块的模块在整个测试期间持续累积。

--print(<sections>)可选修饰符请求生成.exp快照,支持的分区为(src/parser.rs):

分区含义注意事项
bytecodeMove 字节码反汇编.masm输入非法——字节码本身就是输入,无汇编意义
stacklessstackless 执行 IR
micro-ops降级后的 micro-ops
frame-layout每函数的 GC 帧布局较新的扩展分区

分区无论书写顺序如何,都按上表顺序输出;未知 token 或空分区列表会在解析期直接导致测试失败(bail!)。重复分区也会报错。

对于 publish 期无法完成降级的函数(如未被实例化的泛型函数),micro-ops 分区中会渲染为skipped (<reason>)。例如 generic_identity.exp 中:

fun identity(): skipped (generic function not instantiated)

3.2// RUN: execute <addr>::<mod>::<fn> --args ... [--heap-size <n>]— 双 VM 执行

execute两个 VM 上调用指定函数并对比行为。语法为:

// RUN: execute <addr>::<module>::<function> [--args <v1>, <v2>, ...] [--heap-size <n>]
  • --args:逗号分隔的十进制字面量列表,按函数参数类型解析;支持全部整数类型u8..u256/i8..i256,以及bool(仅接受true/false,见parse_bool_arg)、addresssigner
  • --heap-size <n>:将 MonoMove 堆设置为n字节。小堆会在分配压力下强制触发垃圾回收(GC),用于确定性验证 GC 行为。V1 VM 没有该旋钮,会直接忽略它(见 src/parser.rs 与 src/runner.rs)。
  • 修饰符可以任意顺序出现;--边界按" --"切分,避免参数值内的--被误切。

执行产生的规范化输出形如results: v1, v2(成功)或error: <message>(失败,如 abort)。多个返回值和事件会组合渲染:返回值以results:开头、事件以events:开头、中间以|连接;无返回值且无事件的调用渲染为results:(见 src/runner.rs)。

3.3// CHECK:家族 — 输出匹配

// CHECK:指令必须紧跟 execute 指令,其字面量与两个 VM 的规范化输出进行比较。解析逻辑见 src/parser.rs 与 src/matcher.rs:

指令匹配对象匹配方式适用场景
// CHECK: <literal>V1V2精确匹配(trim 后)两 VM 应当一致的场景
// CHECK-V1: <literal>仅 V1精确匹配两 VM 有意分歧时(如不同的 abort 错误消息)
// CHECK-V2: <literal>仅 V2精确匹配同上
// CHECK-SUBSTR: <pattern>V1V2子串匹配abort 消息含易变片段(函数定义索引、代码偏移)时
// CHECK-V1-SUBSTR: <pattern>仅 V1子串匹配同上,单侧
// CHECK-V2-SUBSTR: <pattern>仅 V2子串匹配同上,单侧

单个 execute 步骤之后可以跟随多个 check 指令,每个都被独立验证(src/parser.rs)。

值得注意的源码细节:只有步骤实际检查到的 VM 才会被执行(needs_v1/needs_v2判定,见 src/runner.rs)。这让测试可以只断言一侧——某些 native 函数只有一个 VM 能运行(例如aggregator_v2,V1 测试环境未安装其扩展,直接运行会 panic)。

3.4// CHECK-GC-COUNT: <n>— GC 次数断言

该指令断言 MonoMove(v2)在上一个 execute 步骤中恰好执行了n次垃圾回收。它是 v2 专属指令(V1 VM 没有 GC)。与--heap-size配合可以确定性地驱动 GC 次数,并直接读取 runner 记录的gc_count进行比对(src/matcher.rs)。

实际示例见 generic_copy_gc.move:通过0x0::test_utils::force_gc()主动触发 GC 后,用// CHECK-GC-COUNT: 1断言确实发生了 1 次回收,同时用// CHECK: results: 203验证跨 GC 的对象拷贝语义正确(拷贝后修改副本不影响原对象)。

3.5// CHECK-ERROR-PARITY— VM 错误一致性

该指令不带参数,断言两个 VM 都以 VM 错误失败,且 v2 的失败在映射回 V1 术语后,与 V1 报告的状态码(status code)、子状态(sub-status)、消息、错误位置完全一致。预期值不是测试中的字面量,而是运行时取自 V1 的实际输出(见 src/parser.rs)。

实现要点(src/runner.rs 与 src/matcher.rs):

  • 这是唯一读取映射后描述(而非 MonoMove 原生错误文本)的指令,因此它直接验证了mono-move-outputv1_error::describe的 V1 等价映射逻辑;
  • 若错误没有 V1 等价形式、映射无法复现 V1 的消息或子状态、或 MonoMove 未能定位失败位置,指令都会失败;
  • Move abort 不在覆盖范围内:abort 不携带可映射的 VM 错误,且两个 VM 本就以相同方式渲染 abort(aborted: code <n> (<msg>) in <location>,见render_abort),所以用CHECK:直接比较即可。若对 abort 误用本指令,matcher 会明确报错提示改用CHECK:

4. 基线(Golden)文件与UPBL=1更新机制

每个带有--print分区的输入都有一个同名<name>.exp基线文件,与输入文件同目录存放。例如 basic_arithmetic.move 对应 basic_arithmetic.exp,其中依次固化了bytecodestacklessmicro-ops三个分区:

=== bytecode === // Bytecode version v10 module 0x1::test // Function definition at index 0 fun add(l0: u64, l1: u64): u64 move_loc l0 move_loc l1 add ret === stackless === // module 0x1::test fun add(r0, r1): u64 { slots: params(2), locals(0), temps(1) r0: u64 r1: u64 r2: u64 code: L0: 0: r2 := add r0, r1 @2 1: ret [r2] @3 } === micro-ops === // module 0x1::test fun add() { frame_data_size: 24 entry_gas: 59 code: 0: AddU64 [16] <- [0] + [8] @2 1: Move8 [0] <- [16] @3 2: Return @3 }

基线验证与更新的写入逻辑位于 src/runner.rs:

if !snapshot.is_empty() { let baseline = test_path.with_extension("exp"); move_prover_test_utils::baseline_test::verify_or_update_baseline(&baseline, &snapshot)?; }

规范要求:基线更新必须是可解释的(针对本次变更,而不是盲目覆盖)。每次修改编译器、specializer 或降级器后,都应检查.exp的 diff 是否符合预期,再决定是否用UPBL=1刷新。

5. 运行测试

两条核心命令(引自 AGENTS.md):

# 对照基线进行验证(推荐日常使用) cargo test -p mono-move-testsuite --test differential # 更新基线(仅当你确认变更导致 .exp 内容变化是正确且可解释的时候) UPBL=1 cargo test -p mono-move-testsuite --test differential

UPBL=1环境变量切换verify_or_update_baseline的行为:验证模式检查快照与.exp一致并失败于不一致;更新模式则将新快照写回.exp文件。

6. 执行管线源码剖析

理解 src/runner.rs 的run_test即可把握整个管线,核心步骤如下:

  1. 初始化:创建GlobalContext(1 个执行 worker)与执行守卫;为 V1 构建RuntimeEnvironmentInMemoryStorage;为 V2 构建InMemoryModuleProvider
  2. 发布 prelude:将完整 Move stdlibtest_utils库模块同时注入两个 VM(prelude_modules,见 src/runner.rs),因此测试源码可以直接调用真实 stdlib native。stdlib 只编译一次并通过OnceLock全局共享。
  3. 逐步处理
    • Publish步骤:compile(&sources, kind)编译/汇编模块 → V1 侧序列化后直接插入内存存储(跳过完整发布工作流的兼容性检查,对差分测试足够)→ V2 侧由 loader惰性构建可执行体 → 若请求了--print分区,则渲染快照累积到snapshot
    • Execute步骤:按needs_v1/needs_v2决定运行哪些 VM → V1 用MoveVM::execute_loaded_function,V2 用共享管线引擎with_mono_function→ 归一化输出(返回值 + 事件)→check_output逐条验证 check 指令。
  4. 基线落盘snapshot非空时,与.exp基线验证或更新。

两个关键归一化机制保证了对比的公平性:

  • 参数/返回值类型统一:V1 与 V2 共享PrimitiveKind描述(src/runner.rs),它镜像了 MonoMove 的帧槽布局(size/align),因此同一字节缓冲既可用于 V1 的 BCS 序列化,也可用于 V2 的原始帧存储。V1 的结果用于推导参数种类(param_kinds/return_kinds),当步骤跳过 V1 时则通过load_signature_v1仅加载签名来推导。
  • native 扩展对齐:V1 侧 seed 了与 MonoMove 侧相同的固定输入(交易哈希、脚本哈希、链 ID、状态大小等,见TEST_*常量与seed_extensions),保证扩展支持的 native(如transaction_contextevent)在两侧产生一致输出。V1 的 native 表还会用测试专用 native 覆盖同名生产 native,确保比较的是相同实现(src/runner.rs)。

7. 完整示例解读

7.1 最简 Move 用例

basic_arithmetic.move 是理解整套机制的最小闭环:

// RUN: publish --print(bytecode,stackless,micro-ops) module 0x1::test { fun add(a: u64, b: u64): u64 { a + b } } // RUN: execute 0x1::test::add --args 3, 5 // CHECK: results: 8 // RUN: execute 0x1::test::add --args 0, 0 // CHECK: results: 0

它同时演示了:发布 + 三分区 golden 固定、两个 execute 步骤、以及CHECK对双 VM 的联合断言。

7.2 汇编输入(.masm)

add_values.masm 演示了直接用字节码汇编编写用例,且只请求stackless, micro-ops两个分区(bytecode对 masm 非法):

// RUN: publish --print(stackless,micro-ops) module 0x66::test fun add_values(a: u64, b: u64): u64 copy_loc a copy_loc b add ret // RUN: execute 0x66::test::add_values --args 1, 2 // CHECK: results: 3

这类用例的价值在于:可以构造编译器前端无法直接产出的特殊字节码序列(如copy_loc使用、非标准参数排布),直接考验降级器的正确性。

7.3 泛型与 skipped 标记

generic_identity.move 展示泛型实例化:发布阶段无法为未实例化的identity<T>生成 micro-ops,因此 golden 中标记skipped (generic function not instantiated);但通过call_identity_u64完成实例化后,执行路径完全可用:

// RUN: publish --print(bytecode,stackless,micro-ops) module 0x1::test { fun identity<T>(x: T): T { x } public fun call_identity_u64(x: u64): u64 { identity<u64>(x) } } // RUN: execute 0x1::test::call_identity_u64 --args 42 // CHECK: results: 42

8. 为 MonoMove 贡献差分用例的实践建议

基于上述机制,为 MonoMove 新增或修复功能时建议:

  1. 优先添加.move用例:绝大多数语言特性都可以用 v2 编译器前端直接表达,先到tests/test_cases/differential/下合适子目录添加源码与// RUN: publish --print(...)
  2. 特殊降级路径用.masm:当需要覆盖编译器前端无法产出的字节码形态(寄存器复用、非常规参数排布、非单调返回等),参考arithmetic/add_values.masmcalls/下的 masm 用例手工汇编。
  3. GC 相关改动必配--heap-size+CHECK-GC-COUNT:参考 generic_copy_gc.move,用小堆配合test_utils::force_gc()确定性触发回收,再断言 GC 次数与跨 GC 语义。
  4. VM 错误路径用CHECK-ERROR-PARITY:凡是会产生 VM 错误的场景(而非 Move abort),优先用该指令锁定 V1 的 status / sub-status / message / location,同时验证mono-move-output的错误映射。
  5. 合并前审查.expdiff:跑UPBL=1更新基线后,人工确认每个分区(尤其 micro-ops)的变化都对应本次有意为之的降级变化。

9. 进一步阅读

  • 指令语义权威定义:src/parser.rs 文件头注释
  • 执行与基线逻辑:src/runner.rs
  • CHECK 匹配实现:src/matcher.rs
  • 编译/汇编入口与SourceKind:src/compile.rs
  • 测试入口与用例发现:tests/differential.rs
  • 全部差分用例:tests/test_cases/differential/
  • 测试包配置:Cargo.toml

【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询