Skip to content

第 14 章:测试策略与设计启示 —— 如何验证一个异步 P2P 内核

核心测试目录约 60 个测试源文件,覆盖值类型、协议 parser、状态机、文件系统和 Session 集成。真正值得借鉴的是:怎样把时间、网络、随机性和全局容器切成可控边界。

一、测试金字塔不是按 UI 划分

Transmission 的高价值测试集中在 tests/libtransmission

层级示例测什么
纯值类型block-info、bitfield、history、values、quark边界算术、容器不变量
Parser/serializerbenc、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_infotr_file_piece_maptr_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 测试应把同一字节串按每个可能边界拆分:

text
[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() 等全局时间入口,这会增加集成测试等待和边界不稳定。维护时应优先把新策略写成:

cpp
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 非标准响应。生产客户端必须在“严格拒绝危险输入”和“兼容生态现实”间找边界。

九、从源码提炼出的九条设计原则

  1. 一个主序列管理核心状态:事件线程把多数竞态变成事件顺序问题。
  2. 慢工作离开主序列:完整验证、HTTP 与 UI 各在适合的执行域。
  3. 发现与资源分配分开:地址来源不能直接决定连接预算。
  4. 协议 parser 与策略对象分开:消息层发布事件,Wishlist/Peer Manager 做决策。
  5. 稳定身份与临时句柄分开:info hash 稳定,torrent id 只在一次 Session 内稳定。
  6. 缓存永远可重建:Wishlist、resume progress、Peer replication 都是派生状态,要有失效路径。
  7. 资源约束用结构表达:带宽树比散落的 if/sleep 更能组合全局、分组和局部规则。
  8. 错误要进入可观察状态:网络错误是响应,文件错误进入 torrent state,不只打印日志。
  9. 平台差异留在边缘:核心依赖统一文件、socket、timer、crypto 接口。

十、仍值得改进的地方

成熟不等于完美。基于当前快照,可以看到这些维护压力:

  • rpcimpl.cctorrent.ccpeer-mgr.ccpeer-msgs.cc 仍是超大聚合文件;
  • RPC schema 与实现手工同步,缺少可生成的类型定义;
  • 部分时间与全局函数仍降低确定性测试能力;
  • tr_verify_worker 的 detached thread + 轮询式析构等待较难推理;
  • public C API、旧 API compatibility 与新 C++ 内部表示同时演进,迁移成本高;
  • Web 主要靠轮询,超大实例的实时性与网络成本需要权衡。

这些不是简单“重写即可”的问题。每项改动都要保护跨平台、协议兼容、低资源设备和多年用户数据。

十一、如果你要继续读源码

完成 14 章后,推荐做三个小实验:

  1. peer-mgr-wishlist-test.cc 增加一个跨文件边界 + sequential-from-piece 场景;
  2. transmission-remote --debug 观察 409 后的 JSON-RPC 重试;
  3. 从 Web torrent_start 追到 tr_torrent::start_in_session_thread(),记录每次线程/协议边界。

能独立完成这三件事,说明你已经从“读过文档”进入“能在项目中定位问题”。

源码锚点

文档采用 CC BY-SA 4.0;源码片段保留 Transmission 上游许可。