Solidity 属性测试与模糊测试指南:基于 Google FuzzTest 的 PROPERTY_BASED_TESTS 配置与实战
【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity
本文聚焦 Solidity 编译器仓库中的test/fuzztest目录——一套基于 Google FuzzTest 构建的**属性测试(property-based testing)/ 模糊测试(fuzzing)**基础设施。通过本文,你将掌握该框架的两个核心 CMake 开关(PROPERTY_BASED_TESTS与PROPERTY_BASED_TESTS_MODE)的含义与取舍、unittest与fuzzing两种运行模式的区别、从零配置构建的完整命令,以及如何读懂仓库中已落地的属性测试用例(以 Tarjan 强连通分量算法验证为例),为后续编写或扩展属性测试打下基础。
什么是属性测试:与传统单元测试的本质区别
test/fuzztest/README.md开门见山地定义了这套测试的定位:目录中存放的是基于 Google FuzzTest)构建的属性测试 / 模糊测试。
与传统单元测试不同,属性测试不为固定输入断言固定输出,而是针对某个输入域(domain)声明一条"对域内所有输入都必须成立"的属性(property),再由 FuzzTest 在该域内自动搜索反例(counterexample)。用一句话概括:
单元测试回答"这个输入对不对",属性测试回答"这类输入下不变量是否永远成立"。
这种范式对编译器这类输入空间巨大、组合爆炸的软件尤其有价值——人工编写的用例永远无法覆盖所有合法的中间表示、AST 或数据结构的形态,而属性测试可以用随机生成的输入把不变量验证铺开到远超手工用例的广度。
两个核心 CMake 选项:主开关与模式选择
两个控制属性测试的 CMake 选项定义在 cmake/EthOptions.cmake 的configure_project宏中:
| 选项 | 默认值 | 可选值 | 作用 |
|---|---|---|---|
PROPERTY_BASED_TESTS | OFF | ON/OFF | 总开关。为OFF时deps/fuzztest永远不会被加入构建,其所有传递FetchContent依赖(abseil、re2、gtest、antlr)都不会被配置 |
PROPERTY_BASED_TESTS_MODE | unittest | unittest/fuzzing | 运行模式选择器。仅当PROPERTY_BASED_TESTS为ON时才有意义 |
源码中这两处定义值得细读:
# cmake/EthOptions.cmake 中的定义 eth_default_option(PROPERTY_BASED_TESTS OFF) if (NOT DEFINED PROPERTY_BASED_TESTS_MODE) set(PROPERTY_BASED_TESTS_MODE "unittest" CACHE STRING "Property-based test mode: unittest or fuzzing") endif() set_property(CACHE PROPERTY_BASED_TESTS_MODE PROPERTY STRINGS "unittest" "fuzzing")关键设计点:PROPERTY_BASED_TESTS默认关闭,意味着普通构建完全不受影响——FuzzTest 及其庞大的传递依赖链只在显式开启时才进入配置流程,这保证了日常编译的开销与复杂度不因测试基础设施而增加。从 test/CMakeLists.txt 可以看到条件挂载逻辑:
if (PROPERTY_BASED_TESTS) add_subdirectory(fuzztest) endif()只有总开关打开,test/fuzztest子目录才会被加入构建。
两种运行模式的差异
根据 README 与 test/fuzztest/CMakeLists.txt 中的模式翻译逻辑:
unittest模式:每个FUZZ_TEST作为一条普通测试用例运行,对每个输入做1 秒的随机输入突发(burst)。编译与运行都不需要特殊编译器,默认的 g++ 构建即可工作。fuzzing模式:覆盖率引导的模糊测试(coverage-guided fuzzing),要求使用 Clang 编译器。该模式下每个测试会持续搜索输入空间,以最大化代码覆盖率为导向寻找反例。
在test/fuzztest/CMakeLists.txt中,本项目模式选项会被翻译成 FuzzTest 自身的内部标志:
if (PROPERTY_BASED_TESTS_MODE STREQUAL "fuzzing") set(FUZZTEST_FUZZING_MODE ON) elseif (PROPERTY_BASED_TESTS_MODE STREQUAL "unittest") set(FUZZTEST_FUZZING_MODE OFF) else() message(FATAL_ERROR "PROPERTY_BASED_TESTS_MODE must be 'unittest' or 'fuzzing', got '${PROPERTY_BASED_TESTS_MODE}'") endif()注意这里利用了 CMake 策略CMP0077(NEW)下普通变量对option()的覆盖行为,直接以同名普通变量改写 FuzzTest 的FUZZTEST_FUZZING_MODE;而GCC + fuzzing 模式的非法组合会由 FuzzTest 自身的FATAL_ERROR拦截,因此仓库没有重复实现这一守卫。
依赖注入与编译选项的细节
同一份 CMakeLists 中还做了两件值得注意的事:
- 子模块初始化:通过
include(${CMAKE_SOURCE_DIR}/cmake/submodules.cmake)的initialize_submodule(fuzztest)确保子模块就绪,再以EXCLUDE_FROM_ALL方式把deps/fuzztest加入构建,避免其产物混入默认目标。 - 警告抑制:项目全局通过
add_compile_options加了-Werror(以及-Wconversion等)严格标志,这些标志会继承到本目录;而 FuzzTest 及其依赖在这些标志下无法干净编译,因此追加add_compile_options(-w)关闭警告(追加在继承标志之后,-w生效)。
配置与构建:两种模式的完整命令
README 给出了两组可直接复制的配置命令。
单元测试模式(默认 g++ 即可)
# unit-test mode (works with the default g++ build) cmake -S . -B build-prop -G Ninja -DPROPERTY_BASED_TESTS=ONPROPERTY_BASED_TESTS_MODE缺省即为unittest,因此只需打开总开关;此时每个FUZZ_TEST会以普通 GoogleTest 用例的身份执行 1 秒的随机输入突发,适合在 CI 与日常开发中快速回归。
模糊测试模式(必须使用 Clang)
# fuzzing mode (Clang required) CC=clang CXX=clang++ cmake -S . -B build-fuzz -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/fuzztest.cmake这里通过 cmake/toolchains/fuzztest.cmake 工具链文件完成两件事:
- 强制打开属性测试并切换到 fuzzing 模式:
set(PROPERTY_BASED_TESTS ON CACHE BOOL "Enable FuzzTest property-based tests" FORCE) set(PROPERTY_BASED_TESTS_MODE "fuzzing" CACHE STRING "Property-based test mode" FORCE)- 注入 ASan + 覆盖率插桩标志。工具链文件在
project()之前被处理,因此插桩标志是全局生效的——被测的每个 solidity 库都会以 ASan + coverage 构建,而不仅是test/fuzztest下的翻译单元。标志取自 FuzzTest 自带的辅助脚本deps/fuzztest/cmake/FuzzTestFlagSetup.cmake(fuzztest_setup_fuzzing_flags()),因此要求在配置时子模块已初始化,否则会报错并提示:
Run: git submodule update --init deps/fuzztest工具链文件还特别处理了工具链文件会被 CMake 每次 pass 重新读取的问题:先重置CMAKE_CXX_FLAGS与CMAKE_EXE_LINKER_FLAGS再调用追加式辅助宏,保证无论运行多少次,最终插桩标志都完全一致。
CI 中的实际用法
仓库的 scripts/ci/build_property_tests.sh 展示了 CI 侧的标准做法:以-DPROPERTY_BASED_TESTS=ON配置(PROPERTY_BASED_TESTS_MODE保持默认的unittest,注释明确说明 fuzzing 模式有意不在 CI 中使用),并启用 ccache 加速,然后只构建property_tests目标而非整个项目:
cmake --build . --target property_tests这个便利目标在test/fuzztest/CMakeLists.txt末尾定义:
add_custom_target(property_tests) add_dependencies(property_tests yul_fuzztest)新增的每个<lib>_fuzztest可执行目标都应把自己注册到这个聚合目标下。
源码级实战:读懂第一个属性测试用例
目前仓库已落地的属性测试位于 test/fuzztest/libyul/TarjanSCCPropertyTest.cpp,它验证的是 libsolutil/TarjanSCC.h 中computeStronglyConnectedComponents的实现正确性——这是 Yul 调用图分析(CallGraph::recursiveFunctions())识别递归函数的底层依赖。
被测属性:三条不变量
对随机生成的有向图,测试将 Tarjan 算法结果与独立的可达性 oracle(对每个源节点各做一次 DFS 得到的全对可达矩阵)交叉比对,断言三条属性:
- SCC 划分完整性:返回的 SCC 集合恰好划分
0, n)——每个节点恰好出现在一个范围内(in-range)的 SCC 中,不多不少。 - SCC 等价于双向可达:两个节点属于同一 SCC 当且仅当它们互相可达(这正是 SCC 的定义)。
- 递归分类一致性:仿照
CallGraph::recursiveFunctions()由 SCC 推导递归分类(非平凡 SCC 或自环边)的方式,与独立的"节点位于有向环上"oracle 比对。这一条专门锻炼了纯 SCC 划分无法钉死的自环粘合逻辑——自环节点与孤立节点都是单元素 SCC,仅靠划分无法区分二者。
输入域的构造:FlatMap + PairOf 组合
测试使用 FuzzTest 的域组合 API 生成输入,值得仔细解读:
FUZZ_TEST(TarjanSCCProperty, SCCMatchesReachability) .WithDomains( fuzztest::FlatMap( [ { return fuzztest::PairOf( fuzztest::Just(_numNodes), fuzztest::VectorOf( fuzztest::StructOf<Edge>( fuzztest::InRange<NodeID>(0, _numNodes - 1), fuzztest::InRange<NodeID>(0, _numNodes - 1) ) ).WithMaxSize(maxEdges) ); }, fuzztest::InRange<NodeID>(1, maxNodes) ) );生成逻辑为:先用InRange<NodeID>(1, maxNodes)采样节点数(上限 128),再通过FlatMap依赖该节点数生成(节点数, 边列表)对——每条边由StructOf<Edge>生成,端点约束在[0, numNodes-1]内以保证稠密编号;边数上限maxEdges = 256。这种"先生成规模、再依赖规模生成结构"的依赖式域组合,正是 FuzzTest 处理结构化复杂输入的标准手法。
从实现看验证价值
libsolutil/TarjanSCC.h中的实现是**迭代式(显式工作栈)**的 Tarjan 算法,而非教科书常见的递归版:workStack上的帧记录(node, childIdx),入栈时预先进位childIdx,出栈时向父节点回传 lowlink。这类手写迭代版本极易在"先推进边再递归"、"lowlink 回传时机"等细节上出错,而递归版本在深层图上还有栈溢出风险。属性测试恰好能以海量随机图(含自环、重边、稠密/稀疏、强连通/非强连通)系统性轰击这些边界,这正是该测试用例存在的价值。
运行属性测试
配置完成后,在build-prop或build-fuzz构建目录中运行对应的可执行文件即可。unittest模式下,每个FUZZ_TEST表现为普通 gtest 用例,可用 gtest 的过滤参数(如--gtest_filter)定向执行;fuzzing模式下,可执行文件则是一个覆盖率引导的模糊器,可持续运行直至发现反例或达到资源上限,FuzzTest 会在发现反例时输出最小化后的复现输入。
小结
test/fuzztest是 Solidity 仓库中一套设计干净、侵入性低的属性测试基础设施:
- 总开关默认关闭,普通构建零负担;开启后由 cmake/EthOptions.cmake 统一管理两个选项;
- 两种模式覆盖不同场景:
unittest适合日常与 CI 的快速回归,fuzzing需要 Clang 且通过 cmake/toolchains/fuzztest.cmake 全局注入 ASan + 覆盖率插桩; - 新增测试的扩展路径清晰:在 test/fuzztest 下为各库添加
<lib>_fuzztest可执行目标(参考TarjanSCCPropertyTest.cpp的域组合写法),并注册进property_tests聚合目标即可。
对于希望为 Solidity 的 Yul 优化、类型系统、图算法等复杂逻辑补充不变量验证的开发者,这套框架提供了开箱即用的入口——从读懂SCCMatchesReachability这个用例开始,就是最好的起点。
【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考