第 14 章:测试策略与设计启示 —— 如何验证一个异步 P2P 内核
核心测试目录约 60 个测试源文件,覆盖值类型、协议 parser、状态机、文件系统和 Session 集成。真正值得借鉴的是:怎样把时间、网络、随机性和全局容器切成可控边界。
一、测试金字塔不是按 UI 划分
Transmission 的高价值测试集中在 tests/libtransmission:
| 层级 | 示例 | 测什么 |
|---|---|---|
| 纯值类型 | block-info、bitfield、history、values、quark | 边界算术、容器不变量 |
| Parser/serializer | benc、json、magnet-metainfo、announce-list | 任意输入、round-trip、兼容格式 |
| 策略对象 | wishlist、completion、alt-speeds、torrent-queue | 排序、时间与状态转换 |
| 协议状态机 | handshake、peer-msgs、announcer-udp、DHT、LPD | 分片输入、超时、恶意消息 |
| 文件系统 | open-files、move/remove/rename、watchdir | 临时目录、平台行为、错误路径 |
| 聚合集成 | session、torrent、RPC、Qt RpcClient | 生命周期与跨层契约 |
UI 像素测试较少,但 RPC 和模型边界测试能覆盖多个前端共同依赖的行为。
二、值类型是最便宜的正确性投资
tr_block_info、tr_file_piece_map、tr_bitfield 把容易出错的算术从大对象中抽出。测试可以穷举:
- total size 为 0、正好整除、只差 1 byte;
- 最后 piece/block;
- 文件边界恰好落在 piece 上或穿过 piece;
- bitfield all/none/partial;
- IPv4/IPv6 compact 编码 round-trip。
这些测试运行快,却保护了协议、选块和 I/O 三条链路。
三、Mediator 是测试缝
Handshake、DHT、LPD、Verify、Web、Port Forwarding 等类不直接抓全局 Session,而依赖小型 Mediator。测试替身可以:
- 返回固定 torrent/info hash;
- 提供可控 timer maker;
- 收集发现的 Peer;
- 模拟 web response;
- 注入固定 DH private key 或 padding;
- 记录 verify callback 顺序。
这使网络状态机可以在没有真实公网和 Tracker 的情况下测试。Mediator 的价值不只是“解耦好看”,而是让副作用可观察、可替换。
四、分片输入是 Parser 的必测项
正常网络不会保证一个回调得到完整消息。Handshake 与 Peer Messages 测试应把同一字节串按每个可能边界拆分:
[68 bytes 一次到达]
[1 + 67]
[20 + 28 + 20]
[每次 1 byte]期望状态机在数据不足时只返回 Later,不多消费、不重复发送、不越界;完整后恰好触发一次 done/event。
对 length prefix、bencode/JSON parser 还要测试超长、截断、错误类型、深度上限和尾随垃圾。
五、时间要成为依赖
Announcer、DHT、LPD、rechoke、alt speeds、request timeout 都依赖时间。源码通过 TimerMaker、显式 now 参数、可设置 verify sleep 等方式减少直接调用 wall clock。
仍有 tr_time() 等全局时间入口,这会增加集成测试等待和边界不稳定。维护时应优先把新策略写成:
decision = policy(state, now);再由 timer 回调负责取得 now 和执行副作用。纯函数策略更容易覆盖午夜、星期切换、退避上限等边界。
六、文件测试必须使用隔离目录
测试 fixture 创建临时 config/download 目录和小型 torrent assets。关键检查包括:
- 预分配模式与最终大小;
.part重命名;- move/rename 跨目录和目标已存在;
- 删除任务与删除数据的区别;
- open-files LRU 淘汰与 writable 升级;
- resume 文件损坏/字段缺失;
- watchdir 重复事件和未完成写入。
文件系统测试比 mock write() 更能发现权限、路径规范化和平台句柄问题,但要避免依赖用户真实目录。
七、RPC 测试是多产品契约测试
RPC test 不只是 Server 单元测试。每个字段名、默认值、错误码和 notification 行为同时影响 Web、Qt、remote 以及第三方 SDK。
应重点覆盖:
- JSON-RPC 2.0 request/notification/batch;
- id 类型、缺失 params、未知 method;
- sync 与 async handler 的同构响应;
- table/object 两种 torrent_get;
- recently_active removed;
- snake_case 与旧协议兼容转换;
- Session/Torrent 字段 getter/setter 对称性;
- 409 token、认证、Host/IP whitelist(server 层)。
八、什么最容易在审查中漏掉
8.1 生命周期竞态
异步 callback 捕获 tr_torrent*,完成前任务被删除;Session 正在关闭时 timer/web response 到达;验证线程回调遇到对象销毁。审查应搜索 lambda 捕获、run_in_session_thread 和 raw pointer。
8.2 计数重复
重试造成的重复 PIECE、取消后迟到数据、重连后旧 timeout,都可能让 downloaded/corrupt/active request 计数重复增减。测试要发送重复事件而不只 happy path。
8.3 边界 piece
wanted/unwanted 文件共享 piece、最后 block 非标准长度、piece 与 block 不对齐,是 I/O 与选块最常见的 off-by-one 来源。
8.4 协议兼容
旧 RPC 风格、奇怪但合法 torrent、不同 Peer 的扩展 id、Tracker 非标准响应。生产客户端必须在“严格拒绝危险输入”和“兼容生态现实”间找边界。
九、从源码提炼出的九条设计原则
- 一个主序列管理核心状态:事件线程把多数竞态变成事件顺序问题。
- 慢工作离开主序列:完整验证、HTTP 与 UI 各在适合的执行域。
- 发现与资源分配分开:地址来源不能直接决定连接预算。
- 协议 parser 与策略对象分开:消息层发布事件,Wishlist/Peer Manager 做决策。
- 稳定身份与临时句柄分开:info hash 稳定,torrent id 只在一次 Session 内稳定。
- 缓存永远可重建:Wishlist、resume progress、Peer replication 都是派生状态,要有失效路径。
- 资源约束用结构表达:带宽树比散落的 if/sleep 更能组合全局、分组和局部规则。
- 错误要进入可观察状态:网络错误是响应,文件错误进入 torrent state,不只打印日志。
- 平台差异留在边缘:核心依赖统一文件、socket、timer、crypto 接口。
十、仍值得改进的地方
成熟不等于完美。基于当前快照,可以看到这些维护压力:
rpcimpl.cc、torrent.cc、peer-mgr.cc、peer-msgs.cc仍是超大聚合文件;- RPC schema 与实现手工同步,缺少可生成的类型定义;
- 部分时间与全局函数仍降低确定性测试能力;
tr_verify_worker的 detached thread + 轮询式析构等待较难推理;- public C API、旧 API compatibility 与新 C++ 内部表示同时演进,迁移成本高;
- Web 主要靠轮询,超大实例的实时性与网络成本需要权衡。
这些不是简单“重写即可”的问题。每项改动都要保护跨平台、协议兼容、低资源设备和多年用户数据。
十一、如果你要继续读源码
完成 14 章后,推荐做三个小实验:
- 在
peer-mgr-wishlist-test.cc增加一个跨文件边界 + sequential-from-piece 场景; - 用
transmission-remote --debug观察 409 后的 JSON-RPC 重试; - 从 Web
torrent_start追到tr_torrent::start_in_session_thread(),记录每次线程/协议边界。
能独立完成这三件事,说明你已经从“读过文档”进入“能在项目中定位问题”。
源码锚点
tests/libtransmission/CMakeLists.txt:核心测试清单tests/libtransmission/test-fixtures.h:Session/临时目录 fixturetests/libtransmission/handshake-test.cc:握手状态机tests/libtransmission/peer-mgr-wishlist-test.cc:选块策略tests/libtransmission/rpc-test.cc:协议契约tests/qt/rpcclient-test.cc:本地/远端 RPC 客户端边界docs/Testing-Transmission.md:上游测试说明