深入 Ruby 39:综合交付与可复现使用
系列导航
导读 · 上一篇:38:输入与安全边界 · 下一篇:E01:Rails 与 ActiveSupport 的语言边界 · 完整源码包
交付物必须离开源码目录工作
在源码目录执行测试通过,只说明当前目录、加载路径和已有依赖共同支持这次运行。gem 安装后使用的是打包清单中的文件,命令行入口由 RubyGems 生成,当前目录也可能完全不同。最后一次验收需要模拟实际使用者从包安装后的路径。
Taskbook 主线提供任务校验、筛选、统计、JSON/CSV 导入导出、命令行入口和一个 Rack 形式的 HTTP 应用。本章把它们连接为独立使用流程:构建本地包,在新目录安装,通过命令行完成数据往返,再从已安装代码处理真实回环 HTTP 请求。整个过程只涉及本地合成任务,不发布 gem。
这一步不替代单元测试和类型检查。单元测试负责细粒度契约,类型工具检查其声明覆盖范围,安装实验负责打包与加载路径,网络实验负责传输边界。各层失败需要对应修复,不能用“能打包”覆盖业务失败,也不能用“HTTP 200”替代数据正确性。
冻结运行环境与文件清单
从 examples/ruby 目录运行:
1 | |
解释器使用系列固定的 Ruby 3.4.11,依赖由 Gemfile.lock 冻结。实验读取当前已安装依赖的 gem 缓存,复制需要的运行依赖到临时目录,然后使用本地安装模式。没有依赖缓存时直接失败,并指出缺失项,避免一次无意联网把复现环境变成不同依赖集合。
Specification 文档定义了包名、版本、文件清单、可执行入口和依赖声明。文件在仓库里存在,不代表自动进入包;本工程明确收集 lib、exe 和签名文件。把新模块写完却忘记打包清单,是源码测试无法发现的常见交付差异。
构建输出写入临时目录,不在源目录生成需要手工清理的版本包。构建成功后继续安装,没有把构建日志中的成功文字当作最终判定。包是否缺文件,只有离开源树的加载与使用才能充分暴露。
依赖缓存也属于复现条件。源码版本固定而依赖自由更新,行为仍可能变化;只记录顶层依赖而忽略传递依赖,同样不完整。实验记录实际依赖名称与版本,安装时使用冻结规格允许的缓存,便于检查差异来自哪一层。
新 GEM_HOME 排除隐式加载
脚本使用新的 GEM_HOME 与 GEM_PATH,并移除 Bundler 的项目定位和额外 Ruby 加载路径。执行目录设为临时目录,入口指向新安装生成的 taskbook 命令。这样即使源目录仍有未打包文件,也不会因为相对路径碰巧命中而通过。
使用者安装的包可能借助解释器自带 default gems,这属于明确环境的一部分,不是完全空操作系统。报告必须区分“新应用安装目录”和“全新容器镜像”。本实验验证前者,没有声称移除系统工具链或替换操作系统后仍完全相同。
HTTP 探针还检查加载的 taskbook-workshop 路径位于临时 GEM_HOME 内。这个断言能发现一个隐蔽错误:进程启动参数看似来自新入口,实际 require 却通过环境变量加载了源目录。路径检查与功能结果一起使用,才能确认被测对象。
可执行包装脚本也需要正确解释器。实验显式使用当前 Ruby 调用生成的入口,避免系统 PATH 先找到旧版本 Ruby。普通读者安装时则应先确认 ruby -v 与 gem env 的路径一致,否则安装到一个解释器、运行另一个解释器,会出现“已安装却找不到 gem”。
数据往返覆盖两个协议边界
输入有两条任务,一条优先级五,一条优先级一。先通过 CLI 按最低优先级四筛选,并输出 CSV;再把 CSV 作为下一次 CLI 输入,导出 JSON。最终要求只保留标识一,同时内容经过实际格式解析与序列化。
这条链比单独调用 filter 多覆盖了参数解析、标准输入、枚举状态转换、CSV 表头、标签字段表示和 JSON 输出。某个模块各自测试通过,却在边界上把状态符号写成意外字符串,往返结果就会揭示不一致。
输出顺序和字符编码也是契约。任务标题与标签不能因为格式切换丢失,CSV 中需要转义的内容应交给标准库,而不是手工按逗号切分。JSON 输出用结构比较,不要求空格与键显示格式逐字一致;对用户有意义的数据才进入断言。
如果接口允许导出空集合,应额外确认 CSV 仍保留正确表头、JSON 仍为数组。边界测试不应只选择最顺利的数据;空结果、缺失字段和不合法状态与正常往返共享相同入口,能覆盖不同失败分支。
失败必须保留状态与原文件
实验向 CLI 输入残缺 JSON,要求退出码二,且错误类别可识别。然后创建一个已存在的输出文件,再请求导出到相同路径。接口应拒绝覆盖,返回退出码三,并保持原内容不变。
文件内容不变是独立断言。只检查非零退出码不够:程序可能先截断文件,再发现其他错误并退出。使用独占创建标志把“目标必须不存在”交给文件系统,可以避免在检查存在和打开之间留下覆盖竞争。
错误消息不能泄漏临时目录路径。测试检查对外文本不含内部工作目录,但仍保留操作失败类别,帮助调用方决定修正输入还是重试 I/O。脚本把正常返回值、错误输出与退出码分开捕获,没有把 stderr 中有文字自动当成业务失败。
这些错误约定应进入使用说明。shell 管道与自动化工具依赖退出码判断是否继续;错误时仍返回零,会让下一步把空输出当成成功结果。对终端友好与对程序可组合并不冲突,关键是分别使用 stdout 输出数据、stderr 输出诊断。
真实回环 HTTP 与关闭证据
安装探针加载包内 HTTPApp,在 127.0.0.1 的动态端口启动仅用于测试的简单适配器。它接收三个请求,把请求方法、路径、内容类型和输入流交给应用,再把状态、头与 body 写回 socket。客户端使用 Net::HTTP,禁用代理与重试,分别发送合法任务、非法 JSON 和不存在路径。
预期状态为二百、四百、四百零四,合法响应中的 total 为二。状态码检查之外还解析响应 JSON,避免一个空的成功响应通过验收。三条连接均在 ensure 中关闭,最后关闭监听器并等待服务器线程结束。
这个适配器只实现实验需要的有限 HTTP 子集,没有声称是生产服务器,也没有处理任意分块上传、持续连接或并发请求。应用 Rack 契约需要由对应检查工具验证,网络实验则证明本次已安装代码经过真实 socket 往返。两者证据范围不同。
body 若提供 close 方法,适配器在 ensure 中调用;当前数组 body 没有需要释放的外部资源。输入流和连接仍分别关闭,关闭计数要求等于已接受连接数。把“服务器线程已结束”当成所有资源都释放,会漏掉仍被外部引用的描述符,因此实验同时检查监听器状态。
验收范围要和交付范围一致
主线讲过 Fiber、Ractor、YJIT 和各种元编程能力,Taskbook 不需要把所有实验技术都装进核心。独立 lab 用于理解机制,业务实现只保留当前需求需要的路径。综合交付的质量来自契约闭合,不来自用到了多少语言特性。
类型检查也有明确覆盖边界。一个独立带 RBS 的教学模块通过检查,不能写成全部 Taskbook 动态路径都有静态类型保证。动态输入仍由运行时验证负责,包入口与 HTTP 输入尤其不能因类型工具通过就删除检查。
复现说明应列出版本、安装命令、测试入口、输入示例和已知限制。日志中的临时目录只标识本次执行,读者不需要拥有相同绝对路径;脚本每次创建自己的临时安装。进程退出后目录清理,不把上一轮文件当作下一轮成功条件。
本次完整交付结果
本次实际构建并安装 taskbook-workshop 0.1.0,JSON、CSV 与 Rack 的版本分别为 2.13.2、3.3.5、3.2.1;net-http 0.6.0 和 uri 1.1.1 来自解释器可用依赖。筛选往返只保留标识一,非法输入退出二,既有输出拒绝覆盖并退出三,原内容保持不变。
已安装代码处理的三次回环请求分别返回二百、四百与四百零四,成功响应含两条任务的统计。三条连接关闭,监听器关闭,临时安装目录已清理。该结果覆盖本机此版本的交付链;平台信号、JIT性能与静态检查仍由对应章节的独立实验描述。
练习与验收
临时从打包清单中排除一个实际 require 的模块,确认源码测试与安装测试会出现不同结果,随后恢复清单并重跑。这个练习证明安装验收确实检查了包内容,没有偷偷依赖源目录。
再增加一个包含逗号、换行和中文标签的任务,完成 JSON 到 CSV 再到 JSON 的往返,比较完整字段。最后让 HTTP 请求体不合法,确认错误响应、连接关闭和服务器退出仍全部成立。
| 验收层 | 直接观察 | 不扩大为 |
|---|---|---|
| 源码测试 | 明确业务契约 | 包一定可安装 |
| 类型检查 | 已纳入检查的模块 | 全部动态路径安全 |
| 临时安装 | 新目录加载与CLI行为 | 所有平台均兼容 |
| 回环HTTP | 请求、响应与关闭 | 生产容量和完整服务器 |
