nvme-cli 测试与 CI 实战指南:容器化构建复现、Valgrind 内存检测与 ASan/UBSan 清理
nvme-cli 测试与 CI 实战指南:容器化构建复现、Valgrind 内存检测与 ASan/UBSan 清理
nvme-cli 是 NVMe 管理命令行工具,其质量保障体系覆盖从单元测试到真机端到端(e2e)测试的多层测试套件,并配套了与 GitHub Actions 一致的 CI 容器、内存检测与 Sanitizer 流程。本文以 Documentation/TESTING.md 为核心骨架,结合仓库内 scripts/build.sh、valgrind.supp、meson.build 与 tests/ 目录的实际实现,完整讲解如何本地复现 CI 构建、完成跨架构交叉编译,并系统排查 C 代码中的内存泄漏与未定义行为问题。读完本文,你将掌握一套可复制、可落地的 nvme-cli 开发自测工作流。
一、测试套件全景:先弄清你在测什么
在进入具体工具之前,先了解 nvme-cli 的测试是如何组织的。根据 tests/README 的说明,所有测试按运行条件分为三层:
| 套件 | 目录 | 运行前提 | 覆盖内容 |
|---|---|---|---|
| 单元测试 | tests/unit | 无需构建产物、无需硬件 | 快速 C 与 Python 单测,如 test-argconfig-parse.c、test-plugin.c、命令元数据 schema 校验等 |
| CLI 测试 | tests/cli | 需要已构建的 nvme 二进制,无需硬件 | registry、keys、config-convert/create、nbft 等不触碰设备的功能 |
| 端到端测试 | tests/e2e | 需要真实(或模拟的)控制器/命名空间 | 真实下发 nvme 命令,例如 nvme_id_ctrl_test.py、nvme_format_test.py、nvme_read_write_test.py 等 |
单元与 CLI 测试默认构建(由 meson tests 选项控制,默认开启);e2e 测试因为会读写真实设备、创建/删除命名空间,属于破坏性测试,必须显式用 -De2e-tests=true 才启用,且目标设备从不猜测,必须显式传入 -De2e-controller=/dev/nvme0 与 -De2e-ns1=/dev/nvme0n1,否则 meson setup 直接报错(见 tests/e2e/meson.build 与 meson_options.txt)。e2e 目录下还提供了 nvme_simple_template_test.py 模板,供编写新用例时复制参考。
二、快速体验:预构建静态二进制
对于只想快速验证 CLI 行为、不想从头编译的场景,项目在常规 CI 之外额外提供一份基于当前 HEAD 构建的 x86_64 静态二进制(详见 Documentation/TESTING.md 的 "Pre-built binary" 一节)。静态构建意味着不依赖目标系统上的动态库版本,拿到即可运行,适合在无编译环境的机器上做快速冒烟测试。其构建方式与仓库内 scripts/build.sh 的 static 配置一致(--default-library=static、-Dc_link_args="-static" 等,见 scripts/build.sh)。
三、容器化调试与 CI 构建复现
nvme-cli 官方 CI 工作流运行在预构建的容器镜像中,因此你可以在本地用 Docker 或 Podman 拉取与 CI 完全一致的环境,复现任何构建或调试问题,无需自己手工搭建依赖环境。
3.1 拉取 CI 容器
所有 CI 容器都以 OCI/Docker 镜像形式发布在 ghcr.io/linux-nvme/ 命名空间下。以 Debian 最新版 CI 镜像为例:
docker pull ghcr.io/linux-nvme/debian:latest
使用 Podman 的等价命令:
podman pull ghcr.io/linux-nvme/debian:latest
3.2 启动容器并登录
启动一个交互式 shell:
docker run --rm -it \
--name nvme-cli-debug \
ghcr.io/linux-nvme/debian:latest \
bash
Podman 用户:
podman run --rm -it \
--name nvme-cli-debug \
ghcr.io/linux-nvme/debian:latest \
bash
进入后,你得到的就是 CI 使用的同一套构建环境(同样的编译器、依赖库版本、构建工具链)。--rm 保证退出即清理,-it 提供交互式终端。
3.3 克隆仓库并检出目标分支
容器内克隆 nvme-cli 源码:
git clone https://github.com/linux-nvme/nvme-cli.git
cd nvme-cli
(可选)检出指定分支或提交,用于复现某个 PR 或某个历史版本的构建问题:
git checkout <branch-or-commit>
3.4 运行 CI 构建脚本
GitHub Actions 工作流实际调用的是仓库根目录下的 scripts/build.sh。在容器内直接执行默认配置即可复现 CI 的常规构建:
./scripts/build.sh
默认配置会做三件事:meson setup(开启 --werror,把警告视为错误)、meson compile 编译、meson test 运行测试。构建产物默认留在容器内部,除非挂载宿主卷,否则退出容器即丢失——需要保留产物时,可在 docker run 时用 -v 挂载宿主目录。
四、交叉编译示例:为 s390x 构建
CI 支持使用专用交叉编译容器进行跨架构构建。以 s390x(IBM Z)为例:
4.1 拉取交叉编译容器
docker pull ghcr.io/linux-nvme/ubuntu-cross-s390x:latest
Podman 用户:
podman pull ghcr.io/linux-nvme/ubuntu-cross-s390x:latest
4.2 启动交叉编译容器
docker run --rm -it \
--name nvme-cli-cross \
ghcr.io/linux-nvme/ubuntu-cross-s390x:latest \
bash
Podman 用户:
podman run --rm -it \
--name nvme-cli-cross \
ghcr.io/linux-nvme/ubuntu-cross-s390x:latest \
bash
4.3 克隆仓库
git clone https://github.com/linux-nvme/nvme-cli.git
cd nvme-cli
4.4 执行交叉构建
./scripts/build.sh -b release -c gcc -t s390x cross
参数含义:-b release 指定 release 构建类型,-c gcc 指定编译器,-t s390x 指定交叉编译目标,最后一个位置参数 cross 选择交叉编译配置。从 scripts/build.sh 的实现可以看到,cross 配置会通过 --cross-file=.github/cross/ubuntu-cross-${CROSS_TARGET}.txt 加载对应架构的交叉工具链文件(仓库中已有 armhf、i386、ppc64le、s390x 四个:.github/cross),并针对 i386 特殊处理 pkg-config 的 multiarch 查找路径。具体支持哪些目标取决于容器内安装的工具链,例如 -t arm、-t ppc64le、-t i386 都可尝试。
五、scripts/build.sh 深度解析:CI 构建矩阵
scripts/build.sh 是整个 CI 构建矩阵的入口,支持大量配置与开关,理解它等于理解官方 CI 在测什么。其完整用法可执行 ./scripts/build.sh -h 查看,核心参数如下:
位置参数(config,选择 meson 构建配置):
| 配置 | 作用 |
|---|---|
default |
默认配置:--werror + ioctl 测试 |
libdbus |
启用 libdbus 构建(-Dlibdbus=enabled) |
liburing |
启用 liburing 构建(-Dliburing=enabled) |
fallback |
下载所有依赖并作为共享库构建(--wrap-mode=forcefallback) |
cross |
使用交叉工具链构建(配合 -t 指定目标) |
distro |
分别构建 libnvme 与 nvme-cli,模拟发行版打包流程 |
docs / man_docs / html_docs / rst_docs |
构建全部/手册/HTML/rst 文档 |
static |
构建静态二进制 |
minimal_static |
构建不含 fabrics 支持的最小静态二进制(使用 musl-gcc) |
nofabrics |
关闭 fabrics 支持并运行单元测试 |
libnvme |
只构建 libnvme |
libnvme_abi |
只构建 libnvme3.so,不构建测试与示例(用于 ABI 对比) |
tests |
面向夜间(nightly)构建的测试配置,配合 e2e 参数使用 |
命令行开关:
| 开关 | 作用 |
|---|---|
-a |
用 clang 静态分析器(scan-build)做静态分析 |
-b [release|debug] |
构建类型,默认 release |
-c [gcc|clang] |
编译器,默认 gcc |
-m [meson|muon] |
构建工具,默认 meson;muon 是 meson 的轻量替代 |
-p |
启用覆盖率报告(仅 meson 支持,产出 coverage.xml) |
-s |
使用 ASan+UBSan 测试(对应 asanubsan setup) |
-t [arm|ppc64le|s390x|i386] |
交叉编译目标 |
-x |
使用 Valgrind 测试(对应 valgrind setup) |
tests 配置专属的 e2e 参数(与 meson 的 e2e-* 选项一一对应,见 meson_options.txt):
--e2e-controller /dev/nvme0 # 控制器设备(必填)
--e2e-ns1 /dev/nvme0n1 # 命名空间设备(必填)
--e2e-log-dir nvmetests # e2e 日志目录
--e2e-log-level DEBUG # 日志级别:DEBUG/INFO/WARNING/ERROR/CRITICAL
--e2e-nvme-bin /path/to/nvme # 被测 nvme 二进制路径
--plugin-tests micron,ocp # 针对真实硬件运行的厂商插件测试套件
值得注意的两个实现细节(从 scripts/build.sh 与 scripts/build.sh 可以确认):
- 除
tests配置外,任何配置传入--e2e-*或--plugin-tests参数都会直接报错退出,避免误用; - e2e 测试破坏性极强,脚本明确"拒绝猜测目标设备",所以
--e2e-controller与--e2e-ns1必须显式给出。
在 test_meson()(scripts/build.sh)中可以看到,-x 与 -s 最终分别映射为 meson test --setup valgrind 与 meson test --setup asanubsan,与下文的内存检测流程一一对应。
六、Valgrind 内存测试:查泄漏与越界
Valgrind 是检测内存泄漏、非法读写的第一道防线。nvme-cli 在 meson.build 中注册了一个命名测试 setup:meson test --setup valgrind 会通过 exe_wrapper 把所有单元测试都跑在 valgrind --leak-check=full 之下,并将任何泄漏视为测试失败(--error-exitcode=1)。
6.1 前置条件与构建
Valgrind 必须预先安装(Debian/Ubuntu 系:apt install valgrind,其他发行版同理)。然后按常规 meson 流程构建:
meson setup .build
meson compile -C .build
meson test -C .build --setup valgrind
测试输出会单独写到 .build/meson-logs/testlog-valgrind.txt,便于事后分析。
6.2 泄漏判定策略:definite/possible 才失败
从 meson.build 可见,Valgrind 命令行启用了 --show-leak-kinds=all,即日志中会列出每一种泄漏类型并附完整调用栈,包括进程退出时仍被全局变量可达(still reachable)的块。但只有 "definite"(确定)与 "possible"(可能)泄漏才会导致测试失败,这是 Valgrind 的默认行为:still-reachable 的块通常是进程持有到退出为止的第三方全局状态,因此只报告供人工检视,而不作为 CI 门禁。若检视后发现某个 still-reachable 块其实是自己的代码且无界增长,应当修复或加入抑制文件。
6.3 内置抑制文件 valgrind.supp
仓库根目录自带的 valgrind.supp 会被 meson.build 自动加载,针对三类已知误报做了抑制:
- Shell 脚本测试:Valgrind 包装的是 shell 解释器而非被测 C 二进制,bash 自身的内部分配被抑制(
obj:*/bash); - CPython 内部机制:Python 测试脚本通过 SWIG 导入 libnvme 的 Python 绑定,解释器的导入机制(import machinery)、marshal 与 tokenizer 代码会产生非本项目的报告;由于不同发行版中 Python 可能是静态二进制或动态链接
libpython3,抑制文件为两者各准备了一条obj模式; - SWIG 模块析构:SWIG 在模块初始化时分配一个 varlink 对象,CPython 在解释器关闭时从不释放它——这是 SWIG 的已知限制,而非 libnvme 的泄漏,抑制规则精确匹配
SWIG_Python_newvarlink调用栈(valgrind.supp)。
此外,如果系统提供了 /usr/lib/valgrind/python3.supp,配置时也会被自动加载(见 meson.build)。抑制之后仍然出现的失败,就是 C 代码中的真实问题,需要认真对待。
七、ASan / UBSan:编译期注入的守卫
ASan(AddressSanitizer)与 UBSan(UndefinedBehaviourSanitizer)直接编译进二进制,因此各自需要独立的构建目录。二者检测的问题类别不同:
- ASan(
address):堆溢出、use-after-free(释放后使用)、内存泄漏; - UBSan(
undefined):有符号整数溢出、指针未对齐访问、空指针解引用、越界索引,以及其他 C 未定义行为。
7.1 构建与运行
meson setup .build-asanubsan -Db_sanitize=address,undefined
meson compile -C .build-asanubsan
meson test -C .build-asanubsan --setup asanubsan
关键点:不需要任何包装二进制——Sanitizer 在检测到违规时直接把报告输出到 stderr(meson test 的默认输出流)。asanubsan setup 在 meson.build 中只设置了 timeout_multiplier: 2(Sanitizer 运行较慢),而没有额外包装命令。
7.2 已知限制与排查建议
与 Valgrind 相同的限制依然适用:Python 测试与 Shell 脚本测试可能产生误报(原因见上文 6.3 节)。排查时应把重点放在 nvme-cli - * 命名的单元测试上——它们直接针对 C 代码,出现报告即基本可判定为真实缺陷。另外注意:Sanitizer 检测到的内存泄漏默认只在退出时报告,且 ASan 的泄漏检测与 Valgrind 的判定口径不同,两者可以互为补充,不必互相替代。
八、覆盖率与 CI 工作流:从本地到流水线
理解 CI 工作流能帮助你判断本地复现的价值。仓库的 .github/workflows 目录中与本主题直接相关的工作流有:
- coverage.yml:手动触发的覆盖率工作流,在
ghcr.io/linux-nvme/debian:latest容器内执行scripts/build.sh -p,产出coverage.xml后上报; - run-nightly-tests.yml:每日定时(cron
0 01 * * *)的夜间测试,在真实 NVMe 硬件虚拟机上运行;其中 nvme-cli 测试步骤完整演示了tests配置的实战用法:
scripts/build.sh -b release -c gcc -p \
--e2e-controller "${CONTROLLER}" \
--e2e-ns1 "${BDEV0}" \
--e2e-log-dir "/nvme-cli/tests/nvmetests" \
--e2e-log-level "DEBUG" \
--e2e-nvme-bin "/nvme-cli/.build-ci/nvme" \
--plugin-tests ocp,micron \
tests
这也印证了 Documentation/TESTING.md 的核心主张:在容器/虚拟机中跑 scripts/build.sh 就是在本地复现 CI。夜间流水线还把 e2e 与 blktests 的 coverage-*.xml 合并上报,形成多维度覆盖率数据。
九、一套完整的本地自测流程
综合以上内容,推荐在提交代码前按如下顺序执行自测(无需真实硬件):
# 1. 常规构建 + 单元/CLI 测试(对应 CI 默认配置)
./scripts/build.sh
# 2. 内存泄漏检查(需先 apt install valgrind)
./scripts/build.sh -x
# 3. 地址/未定义行为清理(Sanitizer 编译进二进制)
./scripts/build.sh -s
# 4. 静态分析(可选,需 clang 与 scan-build)
./scripts/build.sh -a
若手头有真实 NVMe 设备且确认了解其风险(e2e 会读写真实数据),可进一步在 CI 容器中运行:
./scripts/build.sh -b release -c gcc \
--e2e-controller /dev/nvme0 \
--e2e-ns1 /dev/nvme0n1 \
--e2e-log-level DEBUG \
tests
三个实战要点务必牢记:Valgrind 与 Sanitizer 的误报高发区是 Python/Shell 测试,以 C 单元测试结果为准;e2e 测试必须显式指定设备且默认拒绝猜测;构建产物默认只存在于容器内,需要持久化就挂载宿主卷。 掌握这套流程后,任何 CI 报错都可以在本地快速还原、定位并验证修复,这正是 nvme-cli 测试体系设计的核心价值所在。