rtk CLI 过滤器的测试体系:从单元测试到 Token 精度验证的完整策略
【免费下载链接】rtkCLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies项目地址: https://gitcode.com/GitHub_Trending/rtk4/rtk
rtk(Rust Token Killer)是一个通过过滤、分组、截断和去重来压缩命令输出的 CLI 代理,其核心价值主张——"在常见开发命令上减少 60–90% 的输出"——必须靠一套严密的测试体系来兑现。本文基于仓库内的测试策略规则 cli-testing.md 展开,完整讲解 RTK 的五层测试结构:单元测试、Token 精度测试、跨平台测试、集成测试与性能测试,并给出可直接复制的测试模板、夹具(fixture)捕获工作流与合并前检查清单,帮助你在为 RTK 新增或修改过滤器时,做到"既有可复现的实操步骤,又有源码级依据"。
一、测试金字塔总览:五层测试各司其职
RTK 的测试策略按优先级和触发时机分为五层,每一层对应不同的变更风险:
| 测试层 | 优先级 | 触发时机 | 测试位置 |
|---|---|---|---|
| 单元测试(Unit Testing) | 关键 | 所有过滤器变更、输出格式修改 | 与被测文件同目录的#[cfg(test)] mod tests |
| Token 精度测试(Token Accuracy) | 关键 | 所有过滤器实现、token 节省声明 | 同单元测试模块 |
| 跨平台测试(Cross-Platform) | 关键 | Shell 转义变更、命令执行逻辑 | #[cfg(target_os)]条件编译 |
| 集成测试(Integration) | 重要 | 新过滤器、命令路由变更、发布准备 | tests/顶层文件 |
| 性能测试(Performance) | 重要 | 性能相关变更、发布准备 | hyperfine//usr/bin/time外部基准 |
这个分层的背后是 RTK 的命令代理架构:main.rs通过 Clap 的Commands枚举把 CLI 命令路由到src/cmds/*/下的专用过滤模块,每个模块执行底层命令并压缩其输出(见 CLAUDE.md 的架构说明)。正因为输出正确性完全依赖各过滤器,"每个过滤器都要被单独验证"才成为测试体系的第一原则。
二、单元测试:与过滤器共置的测试块
基本写法
单元测试采用最朴素的形式:在与过滤器同一文件内放置#[cfg(test)] mod tests块,用assert_eq!/assert!直接对期望输出做断言:
#[cfg(test)] mod tests { use super::*; #[test] fn test_git_log_output() { let input = "abc1234 fix: handle empty commit\ndef5678 feat: add filter\n"; let output = filter_git_log(input); assert_eq!(output, "abc1234 fix: handle empty commit\ndef5678 feat: add filter"); } }这一约定在仓库中得到了全面践行:从源码结构看,src/cmds/下几乎每个过滤器文件都带有测试模块,例如 git.rs 中就有 3 处#[cfg(test)]块,而cargo、php、js、system等子目录下的 50 余个命令文件基本都有 1–2 处,测试与实现"同文件共置"是整个代码库的统一风格。
两种夹具策略并存
RTK 的单元测试存在两种夹具(fixture)模式,根据过滤器的实际需求选择:
- 内联字面量字符串(
src/cmds/**单元测试中最常见)——在测试体内直接构造一小段有代表性的字符串,适合快速覆盖某种特定格式或边界情况。src/cmds/git/git.rs、src/cmds/git/gh_cmd.rs等处广泛使用此模式。 - 真实捕获夹具 +
include_str!——当原始输出体积大、或格式敏感到内联字符串已经不可读、容易与现实漂移时使用。参考范本是 mvn_cmd.rs:该文件是include_str!夹具模式的"标准答案",当前实际包含 40 余处include_str!调用(文档写作时记载为 23+,此后仍在增长),覆盖mvn test通过/失败/多失败/编译错误、mvnd(Maven Daemon)reactor 通过/失败、expected快照对比等多种场景。这些夹具位于tests/fixtures/,例如:- mvn_test_pass_slice_raw.txt
- mvn_test_multifail_slice_raw.txt
- mvnd_reactor_pass_raw.txt(另有配套
mvnd_reactor_pass_expected.txt期望输出) - gradlew_build_raw.txt
- glab_mr_list_raw.json
何时使用
- 每个新过滤器:覆盖常见用例,外加至少一个边界用例(空输入、错误输出);
- 输出格式变更:过滤器逻辑变化时,同步更新相关的
assert_eq!期望值; - 回归检测:一旦输出体积或格式让手写字符串变得脆弱或失真,就优先切换到真实夹具(
include_str!),而不是继续加长内联字符串。
夹具驱动的完整工作流
以新增一个include_str!模式的mvn测试为例,完整流程是"捕获 → 写测试 → 运行"三步:
# 1. 捕获真实输出作为夹具(仅 include_str! 模式需要) mvn test > tests/fixtures/mvn_test_example_raw.txt # 2. 在被测模块内追加测试 cat >> src/cmds/jvm/mvn_cmd.rs <<'EOF' #[cfg(test)] mod tests { use super::*; #[test] fn test_mvn_test_example() { let input = include_str!("../../../tests/fixtures/mvn_test_example_raw.txt"); let output = filter_mvn_test(input); assert!(output.contains("FAILED") || output.contains("PASSED")); } } EOF # 3. 运行该测试 cargo test test_mvn_test_example注意夹具路径是相对源文件的../../../tests/fixtures/(src/cmds/jvm/上跳三级到仓库根),include_str!会在编译期把夹具内容嵌入二进制,因此新增夹具文件后无需任何运行时配置。
三、Token 精度测试:60% 是发布底线
这是 RTK 测试体系中最具项目特色的一层:所有过滤器都必须用真实夹具验证其 token 节省声明。RTK 的全部产品价值就是压缩率,如果测试只验证"不 panic"而不验证"省了多少",核心价值主张就成了无法证伪的口号。
Token 计数测试模板
#[cfg(test)] mod tests { fn count_tokens(text: &str) -> usize { text.split_whitespace().count() } #[test] fn test_git_log_savings() { let input = "..."; // 内联字符串或 include_str! 夹具 let output = filter_git_log(input); let input_tokens = count_tokens(input); let output_tokens = count_tokens(&output); let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0); assert!( savings >= 60.0, "Git log filter: expected ≥60% savings, got {:.1}%", savings ); } }关于count_tokens有两个值得注意的实现事实:
- 该辅助函数在每个测试模块内各自重复定义,而不是放在共享的
tests/common/mod.rs中——从源码结构看,fn count_tokens分散在 mvn_cmd.rs、gradlew_cmd.rs、gh_cmd.rs、glab_cmd.rs、git.rs、sbt_cmd.rs 等多个文件中。编写新测试时不要假设存在共享 helper,直接本地定义即可。 - 测试侧的
split_whitespace()词数估算与产品侧的计量口径并不相同:RTK 本身不捆绑分词器,tracking.rs 用bytes / 4来估算 token(见 CLAUDE.md)。也就是说测试衡量的是压缩比(词数比),而运行时统计的是字节比,两者方向一致但口径独立。
夹具制作:用真实命令输出,拒绝合成数据
# 捕获真实输出 git log -20 > tests/fixtures/git_log_raw.txt cargo test 2>&1 > tests/fixtures/cargo_test_raw.txt gh pr view 123 > tests/fixtures/gh_pr_view_raw.txt pnpm list > tests/fixtures/pnpm_list_raw.txt # 然后在测试中使用: # let input = include_str!("../tests/fixtures/git_log_raw.txt");tests/fixtures/目录现有约 70 个夹具文件,覆盖 mvn/mvnd、gradlew、ctest、glab、phpstan、dotnet、sbt、aws 等生态的真实输出(含.gz压缩的大文件和expected期望输出对),是这一原则的集中体现。
节省率目标:单一底线,而非逐命令表格
文档对节省率目标做了明确的纪律约束:
- 不存在逐过滤器的百分比表格,只有一条强制底线:≥60% 节省是发布阻断项(release blocker),与 CLAUDE.md 中"Pre-commit Gate / 性能目标"的约定一致(该项目对 bash 输出声明 60–90% 的压缩区间);
- 各过滤器通常会大幅超过这条底线,但不要在测试中断言具体的逐命令百分比(例如"gh pr view 达 87%"),除非你已用该过滤器自己的夹具实测过真实数字——断言阈值因过滤器而异,而文档里罗列杜撰数字的表格会立刻腐烂(rot);
- 发布阻断:任何过滤器的节省率跌破 60%,必须在合并前查明原因并修复。
四、跨平台测试:Shell 转义的三分支断言
RTK 需要同时工作于 macOS(zsh)、Linux(bash)、Windows(PowerShell),三者的 Shell 转义规则不同,因此涉及 Shell 转义与命令执行逻辑的变更必须触发跨平台测试。
用cfg按平台条件编译期望值
测试策略文档给出的模式是:为每个平台定义一份期望 Shell 常量,并在同一测试内按目标 OS 分支断言:
#[cfg(target_os = "windows")] const EXPECTED_SHELL: &str = "cmd.exe"; #[cfg(target_os = "macos")] const EXPECTED_SHELL: &str = "zsh"; #[cfg(target_os = "linux")] const EXPECTED_SHELL: &str = "bash"; #[test] fn test_shell_escaping() { let cmd = r#"git log --format="%H %s""#; let escaped = escape_for_shell(cmd); #[cfg(target_os = "windows")] assert_eq!(escaped, r#"git log --format=\"%H %s\""#); #[cfg(not(target_os = "windows")] assert_eq!(escaped, r#"git log --format="%H %s""#); }这里escape_for_shell是规则文档为转义类函数定义的测试模式示例名,用于演示"一个测试函数 + 多个#[cfg]断言分支"的写法;从源码结构看,仓库中并没有名为escape_for_shell的公开函数(路径转义相关的实现散落在如 tee.rs 的模块内部),落地时请替换为你实际被测的转义函数。
各平台 Shell 差异速查
| 平台 | Shell | 引号转义 | 路径分隔符 |
|---|---|---|---|
| macOS | zsh | '单引号'或"双引号" | / |
| Linux | bash | '单引号'或"双引号" | / |
| Windows | PowerShell | `反引号或"双引号" | \ |
测试执行的平台分工
# Linux/macOS(主力平台):本地直接跑 cargo testWindows 通过 CI 覆盖:信任 CI/CD 流水线的 Windows 作业,或在手边有 Windows 机器时手动验证。CLAUDE.md 也把"过度测试跨平台行为"列为要避开的兔子洞——本地覆盖 macOS + Linux,Windows 交给 CI。
五、集成测试:tests/顶层文件与#[ignore]真进程测试
顶层集成测试的分工
集成测试以顶层文件形式放在tests/目录(不随src/共置),当前仓库包含:
- grep_context_test.rs、grep_faithful_format_test.rs、search_compress_test.rs、search_error_test.rs、search_faithful_test.rs —— 覆盖搜索/grep 压缩、忠实格式化等横切行为;
- guard_integration_test.rs —— 守护(guard)行为,例如"当过滤反而会让极小输入膨胀时输出原始内容"以及"不阻塞真实压缩"等场景,会借助
tempfile初始化临时 git 仓库并调用rtk二进制; - copilot_selfheal_test.rs、pipeline_stdin_test.rs —— 自愈与管道 stdin 行为。
这些测试验证的是横切行为(search/grep 压缩、guard rails、忠实格式化)而非单个过滤器模块,其中多个测试同时使用tests/fixtures/中真实捕获的 aws/glab/gradlew/mvn/phpstan/dotnet 输出与各自的内联用例。
#[ignore]真进程测试
真正拉起 RTK 二进制、执行真实命令的测试标记为#[ignore],避免默认测试跑(它依赖已安装的rtk二进制和 git 仓库):
#[test] #[ignore] // 运行方式:cargo test --ignored fn test_real_git_log() { // 依赖: // 1. RTK 二进制已安装(cargo install --path .) // 2. 可用的 Git 仓库 let output = std::process::Command::new("rtk") .args(&["git", "log", "-10"]) .output() .expect("Failed to run rtk"); assert!(output.status.success()); assert!(!output.stdout.is_empty()); // 验证输出是压缩后的(而非原始 git 输出) let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.len() < 5000, "Output too large, filter not working"); }这类断言的精髓在于反向验证压缩生效:不是检查输出等于某段字符串,而是检查"输出必须小于某个体积上界"——一旦过滤器失效、原始输出直通,测试立即失败。
运行顺序
# 1. 本地安装 RTK cargo install --path . # 2. 运行全部测试,包含 tests/*.rs 顶层集成测试 cargo test --all # 3. 运行被忽略的(真进程)集成测试 cargo test --ignored # 4. 运行指定测试 cargo test --ignored test_real_git_log此外仓库还提供了 scripts/test-all.sh 作为冒烟脚本(需要已安装的 RTK 二进制),与 CLAUDE.md 中记录的bash scripts/test-all.sh用法一致。
何时运行
- 发布前:始终运行集成测试;
- 过滤器变更之后:确认真实命令输出下过滤器仍然有效;
- Hook 变更之后:确认 Claude Code 集成仍正常工作(仓库
hooks/目录维护了 claude、copilot、cursor 等各 Agent 的重写钩子,src/hooks/则是对应的 Rust 侧实现)。
六、性能测试:<10ms 启动、<5MB 内存
RTK 的性能目标是启动时间 <10ms、内存占用 <5MB、二进制体积 <5MB。这一约束直接影响了架构决策(CLAUDE.md 明确"无 async,单线程设计,启动 <10ms"),因此性能测试不是可选项。
用 hyperfine 基准测试启动时间
# 安装 hyperfine brew install hyperfine # macOS cargo install hyperfine # 或经 cargo 安装 # 基准对比:RTK vs 原始命令 hyperfine 'rtk git status' 'git status' --warmup 3 # 应显示 RTK 启动 <10ms # 示例输出: # rtk git status 6.2 ms ± 0.3 ms # git status 8.1 ms ± 0.4 ms内存占用测量
# macOS /usr/bin/time -l rtk git status # 查看 "maximum resident set size",应 <5MB # Linux /usr/bin/time -v rtk git status # 查看 "Maximum resident set size",应 <5000 kbytes性能回归检测:before/after 对比法
# 变更前 hyperfine 'rtk git log -10' --warmup 3 > /tmp/before.txt # 变更后 cargo build --release hyperfine 'target/release/rtk git log -10' --warmup 3 > /tmp/after.txt # 对比 diff /tmp/before.txt /tmp/after.txt # 若启动时间增加 >2ms,需要调查原因性能目标与验证手段
| 指标 | 目标 | 验证方式 |
|---|---|---|
| 启动时间 | <10ms | hyperfine 'rtk <cmd>' |
| 内存占用 | <5MB | time -l rtk <cmd> |
| 二进制体积 | <5MB | ls -lh target/release/rtk |
七、测试组织:目录结构与最佳实践
完整的测试目录结构如下:
rtk/ ├── src/ │ ├── cmds/ │ │ ├── git/ │ │ │ ├── git.rs # 过滤器实现 │ │ │ │ └── #[cfg(test)] mod tests { ... } │ │ ├── jvm/ # gradlew、mvn — include_str! 夹具的参考范本 │ │ ├── php/ # php, artisan, phpunit, phpstan, pest, paratest, ecs, pint │ │ └── ... │ ├── core/ # 共享基础设施 │ ├── hooks/ # Hook 系统 │ └── analytics/ # Token 节省分析 ├── tests/ │ ├── fixtures/ # 真实捕获的命令输出 │ │ ├── mvn_test_pass_slice_raw.txt │ │ ├── gradlew_build_raw.txt │ │ ├── glab_mr_list_raw.json │ │ └── ... │ ├── grep_context_test.rs # 顶层集成测试 │ ├── grep_faithful_format_test.rs │ ├── guard_integration_test.rs │ ├── search_compress_test.rs │ ├── search_error_test.rs │ └── search_faithful_test.rs最佳实践总结:
- 单元测试:嵌入模块内(
#[cfg(test)] mod tests),与过滤器共置; - 夹具:小而快的用例优先内联字符串;用例变大或格式敏感后切换到
tests/fixtures/的include_str!真实输出——模式参照 mvn_cmd.rs; count_tokenshelper:目前是各测试模块各自重复定义,不要假设存在共享的tests/common/mod.rs;- 集成测试:顶层
tests/*.rs文件,部分含#[ignore]标记的真进程测试。
新增过滤器时的完整检查清单见 src/cmds/README.md 的"Adding a New Command Filter"章节。
八、测试检查清单:从实现到发布
新增或修改过滤器时,按阶段逐项核对:
实现阶段
- 在该过滤器自己的
#[cfg(test)] mod tests块中编写单元测试(内联字符串,或较大/真实输出用include_str!夹具) - 添加 token 精度测试(用本地定义的
count_tokens验证 ≥60% 节省) - 测试跨平台 Shell 转义(如适用)
质量检查
cargo test --all全部通过cargo test --ignored集成测试通过- 用
hyperfine基准测试启动时间(<10ms)
合并前
- 所有测试通过(
cargo test --all) - token 节省 ≥60% 已验证
- 跨平台测试通过(Linux + macOS)
- 性能基准通过(启动 <10ms)
发布前
- 集成测试通过(
cargo test --ignored) - 性能回归检查(hyperfine before/after 对比)
- 内存占用验证(
time -l下 <5MB) - 跨平台 CI 通过(Linux + macOS + Windows)
这些检查项最终汇入 CLAUDE.md 定义的"Pre-commit Gate"——每次 Rust 文件编辑后必须通过cargo fmt --all && cargo clippy --all-targets && cargo test --all三道闸口,clippy 警告零容忍。
九、常见测试模式与反模式
模式一:内联夹具 + Token 精度(小而快的合成用例)
#[cfg(test)] mod tests { use super::*; fn count_tokens(text: &str) -> usize { text.split_whitespace().count() } #[test] fn test_output_format() { let input = "raw command output here"; let output = filter_cmd(input); assert_eq!(output, "expected filtered output"); } #[test] fn test_token_savings() { let input = "raw command output here"; let output = filter_cmd(input); let savings = 100.0 - (count_tokens(&output) as f64 / count_tokens(input) as f64 * 100.0); assert!(savings >= 60.0, "Expected >=60% savings, got {:.1}%", savings); } }模式二:include_str!夹具(真实捕获输出)
适用于输出足够大或格式敏感、手写字符串会与现实漂移的过滤器(见 mvn_cmd.rs):
#[test] fn test_mvn_test_pass() { let input = include_str!("../../../tests/fixtures/mvn_test_pass_slice_raw.txt"); let output = filter_mvn_test(input); assert!(output.contains("BUILD SUCCESS")); }模式三:边界用例(过滤器健壮性)
#[test] fn test_empty_input() { let output = filter_cmd(""); assert_eq!(output, ""); } #[test] fn test_malformed_input() { let malformed = "not valid command output"; let output = filter_cmd(malformed); // 二选一: // 1. 返回尽力而为的过滤输出,或 // 2. 原样返回输入(fallback) // 两者都可接受——关键是绝不能 panic! assert!(!output.is_empty()); } #[test] fn test_unicode_input() { let unicode = "commit 日本語メッセージ"; let output = filter_cmd(unicode); assert!(output.contains("commit")); } #[test] fn test_ansi_codes() { let ansi = "\x1b[32mSuccess\x1b[0m"; let output = filter_cmd(ansi); // 应去除或保留 ANSI 码,但不能弄坏输出 assert!(output.contains("Success") || output.contains("\x1b[32m")); }"失败时回退、绝不 panic"与 CLAUDE.md 编码规则中的 fallback 模式(过滤器失败时原样执行原始命令)一脉相承。
模式四:集成测试(端到端行为)
#[test] #[ignore] fn test_real_command_execution() { let output = std::process::Command::new("rtk") .args(&["cmd", "args"]) .output() .expect("Failed to run rtk"); assert!(output.status.success()); assert!(!output.stdout.is_empty()); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.len() < 5000, "Output too large"); }反模式清单
❌ 用硬编码合成数据测试(合成数据不能反映真实命令输出)→ ✅直接断言期望输出:
// ❌ 错误:let input = "commit abc123\nAuthor: John"; 然后对合成输入下结论 // ✅ 正确: let output = filter_git_log(input); assert_eq!(output, "expected output");❌ 跳过跨平台测试(只测当前平台)→ ✅用cfg覆盖所有平台:
// ✅ 正确——同一测试内按平台分支断言 #[test] fn test_shell_escaping() { let escaped = escape("test"); #[cfg(target_os = "windows")] assert_eq!(escaped, "\"test\""); #[cfg(not(target_os = "windows"))] assert_eq!(escaped, "test"); }❌ 忽视性能回归(只测功能、不做性能跟踪)→ ✅基准并跟踪性能:
# ✅ 正确——变更前/后基准对比 hyperfine 'rtk cmd' --warmup 3 > /tmp/before.txt # 做修改 cargo build --release hyperfine 'target/release/rtk cmd' --warmup 3 > /tmp/after.txt diff /tmp/before.txt /tmp/after.txt❌ 接受 <60% 的 token 节省(无节省率验证的测试毫无意义)→ ✅验证节省声明:
// ✅ 正确——验证 ≥60% 节省 #[test] fn test_token_savings() { let savings = calculate_savings(input, output); assert!(savings >= 60.0, "Expected ≥60%, got {:.1}%", savings); }十、小结
RTK 的测试策略可以浓缩为一句话:每个过滤器都要被"格式正确、压缩达标、平台无偏、性能不回归"四个维度独立验证。内联字符串与include_str!真实夹具两种策略并存、count_tokens本地重复定义、集成测试用#[ignore]隔离真进程依赖——这些看似"不优雅"的约定,实际上都是为了降低新增过滤器的认知成本:任何贡献者只需要打开 mvn_cmd.rs 这一个参考文件,就能照抄出一套完整的、满足发布标准的测试。对于正在为代理型 CLI 工具设计测试体系的开发者,这套"以核心指标(压缩率)为发布阻断项、以真实夹具为回归基准"的做法尤其值得借鉴。
【免费下载链接】rtkCLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies项目地址: https://gitcode.com/GitHub_Trending/rtk4/rtk
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考