12. 质量工程
1. 背景与原理
1.1 车规/嵌入式对质量的额外要求
| 要求 | 手段 |
|---|---|
| 无未定义行为 | 禁用unsafe,禁用越界索引与算术溢出 |
| 可预测的资源行为 | 无 panic 路径(unwrap/expectdeny)、无无界增长 |
| 可追溯 | SPDX 版权头(每文件)、依赖许可证审计、commit sha 编入产物 |
| 可复现 | 锁定工具链、Cargo.lock入库、Nix flake / devcontainer |
| 可回归 | 分层测试 + 覆盖率门禁 |
1.2 测试金字塔的价值
诊断服务的缺陷往往出现在跨层交互(协议序列化、错误映射、传输边界),因此既需要大量单测覆盖算法,也需要端到端测试覆盖真实 HTTP 行为。
2. 当前实现架构
2.1 分层测试
| 层 | 位置 | 手段 | 规模 |
|---|---|---|---|
| 单元 | 各 cratesrc/**内联mod tests | tokio::test、mock provider | 200+ 测试函数(topology 20、providers builder 12、jwt 11、mcp 7、cors 7…) |
| 集成 | opensovd-server/tests/、opensovd-client/tests/ | mock-http-connector打桩 HTTP、tower::ServiceExt::oneshot | server 4 个文件、client 7 个文件 |
| E2E | tests/(Python + pytest) | 真实进程 / Docker 中的 gateway,或预构建二进制 | tests/opensovd-gateway/**、tests/opensovd-mcp/** |
| API 集合 | tests/bruno/** | Bruno CLI(bru) | CORS 预检、version-info 等 |
| 基准 | benches/ | criterion | 3 个用例 |
2.2 E2E 框架(opensovd-e2e,pytest 插件)
提供的命令行选项(opensovd-e2e/src/opensovd_e2e/plugin.py:21-52):
| 选项 | 作用 |
|---|---|
--opensovd-run | 使用预构建二进制(跳过构建) |
--opensovd-args | 透传给被测进程的参数 |
--opensovd-profile | cargo profile |
--opensovd-target | 交叉编译目标 |
--opensovd-features | cargo features(逗号分隔) |
--opensovd-coverage | 启用覆盖率采集(不能与--opensovd-run同用) |
提供的 fixture(plugin.py:131-170):
| fixture | scope | 作用 |
|---|---|---|
crate_binary | module | 要构建/运行的 crate 名(如opensovd-gateway) |
binary_args | module | 传给二进制的参数 |
ready_banner | module | 就绪判定正则(等待日志出现该 banner 才认为可测) |
process | module | ProcessUnderTest实例 |
2.3 静态约束
[workspace.lints.rust] unused = "deny"; unsafe_code = "deny"; dead_code = "deny" [workspace.lints.clippy] pedantic = "deny"; cargo = "deny" expect_used = "deny"; unwrap_used = "deny"; indexing_slicing = "deny" string_slice = "deny"; arithmetic_side_effects = "deny" print_stdout = "deny"; print_stderr = "deny"; unwrap_in_result = "deny" clone_on_ref_ptr = "deny"; rc_buffer = "deny"; rc_mutex = "deny"2.4 CI 流水线
prepare(变更检测) ├─ git-lint:semantic PR 标题检查 ├─ build(矩阵:profile × target) │ ├─ cargo build --locked │ ├─ cargo test --locked --all-features │ ├─ uv run pytest opensovd-e2e/tests │ ├─ bruno CLI 运行 tests/bruno │ └─ 上传 artifacts ├─ licenses:cargo deny check licenses sources ├─ advisories:cargo deny check advisories └─ lint:rustfmt + clippy + pre-commit覆盖率:cargo-llvm-cov→ JSON →scripts/coverage-report.sh渲染 Markdown → 发布到 GitHub Pages(当前徽章87%)。
3. 核心流程与算法
3.1 E2E 进程管理算法
process = subprocess.Popen( ..., stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ...) # 启动读线程持续排空 pipe(防止缓冲区满导致子进程阻塞) proc.match = proc.wait_for(ready_banner, timeout_seconds)def wait_for(self, pattern, timeout_seconds): # 基于行事件(threading.Event)等待匹配 # 已消费的行不会丢失("below still wakes the next wait()") ... self.process.wait(timeout=PROCESS_TERMINATE_TIMEOUT) # 终止时先优雅后强制关键设计:
- 就绪判定靠日志 banner(而非固定 sleep 或端口探测)——避免 flaky,且能验证日志输出本身
- 读线程持续排空 stdout/stderr—— 防止管道缓冲区填满导致被测进程阻塞(最常见的 E2E 挂死原因)
wait_for保留已读行—— 多次等待不会丢失先前的输出- 终止:优雅 → 超时 → 强制(
PROCESS_TERMINATE_TIMEOUT)
3.2 二进制定位算法
def _cargo_metadata(project_root) -> dict: # cargo metadata 找 target_directory def _resolve_binary(project_root, crate) -> tuple[Path, str]: # 按 profile/target 拼路径 def _build_crate_binary(config, crate) -> Path: # 未给 --opensovd-run 时先 cargo build即:优先用预构建二进制(CI 中跨 job 复用)→ 否则从源码构建 → 用cargo metadata精确定位产物路径(避免硬编码target/debug/...)。
3.3 Rust 单测纳入统一入口
tests/test_rust.py解析cargo test --list的输出,把每个 Rust 测试动态展开为 pytest 参数化用例:
defparse_cargo_test_output(output:str)->list[tuple[str,str]]:# 匹配 "Running unittests src/lib.rs (target/debug/deps/foo-xxx)"# 匹配 "Doc-tests opensovd_core"价值:pytest一次运行即可得到 Rust 单测 + Python E2E 的统一报告(且--opensovd-run时可跳过)。
3.4 Bruno 集合
tests/bruno/是标准 Bruno 集合(bruno.json+environments/local.bru+.bru用例),CI 用@usebruno/cli运行。覆盖 CORS 预检与 version-info(含 schema、非法参数)。
4. 待完善与风险
4.1 测试覆盖盲区(严重)
- 索引一致性无属性测试(高):04 章 指出的三处索引缺陷之所以能存在,根本原因是没有随机增删改后的不变量断言。建议引入
proptest:随机操作序列后断言"正查/反查一致、无悬空引用、无幽灵索引"。这比补 10 个固定用例更有效。 - 写链路正向用例缺失(中):mock 拓扑 24 个数据项全部只读,
PUT只能命中 400。写成功路径、write-only 资源、并发写均无覆盖。 - 基准覆盖不足(中):仅
get_component/provider_read/data_read3 个用例,无写路径、无关系查询、无并发争用,且固定 10 000 规模、单线程运行时——恰好没有覆盖已知的 O(n) 删除问题。 - 并发/竞态测试缺失(中):拓扑读写并发、事件订阅与快照竞态、discovery 与请求并发,均无专门测试。
- 异常/故障注入缺失(中):provider 抛错、超时、panic;discovery 流中断;TLS 握手失败——这些路径缺少用例。
4.2 测试基础设施(中)
- 覆盖率 87% 但门禁不明(中):未见到失败阈值配置(
--fail-under),因此覆盖率是"展示"而非"门禁"。建议设定阈值并阻止下降。 - E2E 依赖 Docker 时的平台限制(低):
--network=host仅 Linux 可用,macOS 开发者无法本地复现 CI 的 Docker 模式。 - Bruno 集合较小(低):仅覆盖 CORS 与 version-info,未覆盖数据读写、鉴权、错误格式。
- 无契约测试(中):没有针对"响应必须始终为
GenericError"或"include-schema必须返回合法 JSON Schema"的跨端点一致性测试(tests/opensovd-gateway/test_api.py做了部分 schema 自校验,可推广)。
4.3 流程与工具(低)
- 无 fuzz 测试(中):HTTP 输入(查询参数、路径段、JSON body)解析器值得 fuzz(
cargo-fuzz),尤其是百分号编码与 ID 处理。 - 无变异测试(低):无法评估测试有效性(87% 覆盖可能包含大量"执行了但没断言"的用例)。
- pre-commit 与 CI 存在重叠(低):
.pre-commit-config.yaml与 CI lint job 可能重复执行同类检查,需注意一致性。
4.4 建议的改进顺序
| 优先级 | 事项 | 收益 |
|---|---|---|
| P0 | 引入proptest做拓扑不变量测试 | 一劳永逸防止索引类缺陷回归 |
| P0 | 覆盖率设阈值门禁 | 防止质量下滑 |
| P1 | 补写链路正向用例(可写 mock 数据) | 覆盖 PUT/204 路径 |
| P1 | 补并发与故障注入测试 | 车载场景的稳定性信心 |
| P2 | 扩展基准(写路径、关系查询、并发) | 暴露 O(n) 删除等性能问题 |
| P2 | 推广契约测试(错误格式、schema 合法性) | 协议一致性 |
| P3 | fuzz + 变异测试 | 深度质量 |
5. 关键代码位置
| 内容 | 路径 |
|---|---|
| Lint 策略 | Cargo.toml:73-97 |
| CI 流水线 | .github/workflows/ci.yaml:27-190 |
| 依赖与许可证审计 | deny.toml |
| E2E pytest 插件 | opensovd-e2e/src/opensovd_e2e/plugin.py:21-170 |
| E2E 进程管理 | opensovd-e2e/src/opensovd_e2e/process.py:25-300 |
| Rust 单测纳入 pytest | tests/test_rust.py |
| E2E 用例(gateway) | tests/opensovd-gateway/{cli,mocks}/*.py、test_api.py、test_signals.py |
| Bruno 集合 | tests/bruno/** |
| 覆盖率报告脚本 | scripts/coverage-report.sh |
| 基准 | benches/src/topology.rs:14-123 |
| 测试文档 | docs/testing.md |