18|最终实战:构建与验收 TextLab
目标:从已完成的源文件构建 TextLab,并用正常输入、边界输入和失败输入验收完整行为。
你已经分别实现并验证了四层:第 12 课的纯统计函数,第 13 课的测试,第 14 课的文件读取,第 15 课的命令行入口。最后一步是把它们作为一个工具使用,检查组合以后是否仍然满足最初的承诺。
调用关系是 main → analyze_file → analyze → Report,报告再由入口输出。原始文件内容由读取函数中的 String 拥有,统计函数只借用它,报告返回时已经不依赖文本。错误沿 Result 返回到入口,由入口统一决定消息和退出状态。无需引入新的框架来完成这个流程。
确认可以恢复的项目状态
本课不新增业务实现。起点是此前课程的完整代码:
| 文件 | 来源与作用 |
|---|---|
Cargo.toml、Cargo.lock | 第 1 课的包配置,包名 textlab,edition 2024,无第三方依赖 |
src/stats.rs | 第 12 课完整统计实现,加上第 13 课派生属性与测试 |
src/lib.rs | 第 14 课完整实现,导出 Report、analyze、analyze_file |
src/main.rs | 第 15 课完整 CLI,帮助分支可选 |
tests/public_api.rs | 第 13 课公共 API 测试 |
examples/prepare_inputs.rs、examples/read_file.rs | 第 14 课样本生成与读取实验 |
其他 examples/*.rs | 各课保留的可运行实验 |
若你在独立练习中修改过项目,先按表中来源恢复主示例;第 13 课新增边界测试和第 15 课帮助分支可以保留。语言实验中故意制造的编译错误必须先恢复,Cargo 检查全部目标时也会编译 examples。
从源文件到可执行程序
先读完下列步骤,再运行。运行前先预测:没有网络和第三方服务参与,哪些问题仍可能让程序失败?
cargo run --quiet --example prepare_inputs
cargo fmt
cargo check --all-targets
cargo test --quiet
cargo build --release
cargo fmt 根据 Rust 格式约定整理源文件,不改变设计意图;需要 rustfmt 组件。--all-targets 把库、二进制、测试和示例都纳入检查。--release 使用发布配置构建,产物位于 target/release/;它不表示任何平台都能运行同一个二进制,产物仍针对当前构建目标。
预期观察:样本生成器输出 fixtures ready,检查和构建成功;基线有 4 个单元测试、1 个集成测试通过,保留第 13 课练习则多 1 个单元测试。编译耗时和进度消息不固定。
构建后,macOS/Linux 可直接运行:
./target/release/textlab fixtures/sample.txt
./target/release/textlab fixtures/empty.txt
./target/release/textlab fixtures/crlf.txt
Windows PowerShell 对应为:
.\target\release\textlab.exe fixtures/sample.txt
.\target\release\textlab.exe fixtures/empty.txt
.\target\release\textlab.exe fixtures/crlf.txt
三次成功调用的预期输出依次为:
lines=1 words=2 bytes=12 chars=8
lines=0 words=0 bytes=0 chars=0
lines=2 words=2 bytes=6 chars=6
这与通过 Cargo 运行时相同,说明 Cargo 只是构建与运行入口,并不是程序执行统计时必需的运行时服务。
验收失败行为
以当前平台的同一可执行程序运行以下参数组合。退出状态应在每次运行后立即检查,POSIX shell 用 echo $?,PowerShell 用 $LASTEXITCODE。
| 传入参数 | 标准输出 | 标准错误 | 状态 |
|---|---|---|---|
| 无参数 | 空 | usage: textlab FILE | 2 |
fixtures/sample.txt extra | 空 | usage: textlab FILE | 2 |
fixtures/invalid.bin | 空 | 路径与编码错误原因 | 1 |
fixtures/not-created.txt,确认该文件不存在 | 空 | 路径与文件不存在的原因 | 1 |
例如,POSIX shell 的无效编码实验命令为:
./target/release/textlab fixtures/invalid.bin
echo $?
PowerShell 对应为:
.\target\release\textlab.exe fixtures/invalid.bin
$LASTEXITCODE
最后一行应为 1。操作系统和 Rust 版本可能影响错误全文,但不能影响“没有成功报告、非零退出”这个契约。
引导复盘与独立交付
- 把正常调用改为读空文件,确认“空文本”是成功结果,不是读取失败。
- 读无效编码文件,确认它与空文件走不同分支。
- 打开
src/lib.rs,指出是哪一个?阻止错误输入进入统计函数。
独立练习:用自己的编辑器创建 fixtures/my-note.txt,内容严格为 Rust 很好,不带尾换行。运行发布版,先手算四项结果,再对照程序。然后增加一个尾换行,再运行。最后用几句话解释为什么字节数和标量值数增加了,而行数没有增加。
参考结果与可执行复核
不带换行时应为 lines=1 words=2 bytes=11 chars=7;增加一个 LF 后应为 lines=1 words=2 bytes=12 chars=8。如果编辑器写入的是 CRLF,后者应为 13 字节、9 个标量值,行数仍为 1。若保存了 UTF-8 BOM,它也会进入文本统计,因此先确认编辑器使用无 BOM 的 UTF-8。
为了排除编辑器差异,可以创建 examples/final_check.rs 并运行 cargo run --quiet --example final_check:
use std::{fs, io, path::Path};
use textlab::analyze_file;
fn main() -> io::Result<()> {
fs::write("fixtures/final-check.txt", "Rust 很好")?;
let report = analyze_file(Path::new("fixtures/final-check.txt"))?;
assert_eq!(report.summary(), "lines=1 words=2 bytes=11 chars=7");
fs::write("fixtures/final-check.txt", "Rust 很好\n")?;
let report = analyze_file(Path::new("fixtures/final-check.txt"))?;
assert_eq!(report.summary(), "lines=1 words=2 bytes=12 chars=8");
println!("final checks passed");
Ok(())
}
起点已由样本生成器创建 fixtures/。这个完整检查会写入并覆盖专用练习文件 fixtures/final-check.txt,不会改写你的笔记。预期输出 final checks passed。
若发布版行为与源码不同,修改后重新执行 cargo build --release;旧二进制不会自动更新。若 cargo fmt 不可用,按 rustup 的组件说明安装 rustfmt 后重试。若全目标检查失败,先定位具体 example,通常是前面故意制造的移动或借用错误还没恢复。若计数只差一两个字节,检查尾换行、CRLF 和 BOM,而不是先改统计函数。
到这里,课程的交付物是一份能构建、能测试、能处理失败的小型 Rust 工具。继续扩展时可以一次选择一个方向:按词频统计需要映射表;处理大文件需要流式读取;大量文件处理需要受控并发。每次先明确新输入和预期行为,再扩展已有分层。