Community编程与开发github.com

luk036/dedup-rust-tests

查找并移除重复的 Rust 单元测试,然后通过针对性测试填补覆盖率缺口—整个过程通过 `cargo llvm-cov` 前后的覆盖率对比进行验证。

dedup-rust-tests 是什么?

该技能(luk036/dedup-rust-tests)专为 Rust 开发者设计,用于自动清理和优化单元测试套件。它包含两个协同工作的模式:DEDUP 和 GAP-FILL。DEDUP 模式通过分析 `cargo llvm-cov` 生成的覆盖率报告,识别并移除那些覆盖相同代码路径、使用相同或等效输入值的冗余测试。GAP-FILL 模式则在去重后,分析报告中未覆盖或覆盖不足的代码(如特定单态化、trait 实现、特性门控代码等),并自动编写针对性的单元测试来填补这些缺口。该技能支持在 Claude Code、Cursor 等 AI 编码代理中运行,通过执行 `cargo llvm-cov` 命令系列来建立基线、验证变更,并最终报告覆盖率的提升。它非常适合测试套件因多人协作而变得臃肿的项目,以及需要精确提高代码覆盖率的场景。

兼容平台~Claude Code~Codex CLI~Cursor✓OpenCode
npx skills add https://github.com/luk036/agent-skills/tree/HEAD/skills/dedup-rust-tests

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

技能:dedup-rust-tests

本技能中的文件引用(@path)均相对于此目录。

我的作用

我有两种协同工作的模式:

  1. DEDUP — 识别并移除重复的 Rust 单元测试(即其他测试已使用相同或等效输入值覆盖同一代码路径的测试)。通过 cargo llvm-cov 的覆盖率对比(删除前 vs. 删除后)进行验证。

  2. GAP-FILL — 去重后,分析 llvm-cov 报告,查找未测试或测试不足的代码(特定单态化、trait 实现、特性门控代码),然后编写针对性单元测试以填补这些缺口。通过覆盖率提升情况进行验证。

两种模式共享一个通用的基线构建和覆盖率衡量工作流程。

何时使用我

用于 DEDUP:

  • 您的 Rust 测试套件是逐步增长的,存在多个测试模块覆盖相同代码路径的情况
  • 一个“旧式”内联测试模块(#[cfg(test)] mod tests)与包含更广泛测试的新测试文件(如 more_tests.rs)并存
  • “冒烟测试”(如 it_works)复制了多个针对性测试中的断言
  • 您接手了一个含有重复测试场景的代码库(例如,test_abs 和 test_fraction_abs 都使用重叠的输入测试同一个方法)
  • 您希望在保持覆盖率的前提下缩短 CI 测试时间

用于 GAP-FILL:

  • 泛型类型仅针对一种具体类型测试(例如,Fraction<i32>),而覆盖率报告揭示了未被测试的单态化(如 Fraction<i8>、Fraction<i128>)
  • Trait 实现(如 From<i8>、TryFrom<...>)覆盖率为 0%
  • 特性门控代码(#[cfg(feature = "...")])未经过测试
  • 私有方法(如 signum()、is_positive())未被覆盖,因为它们只能从自己的模块内部进行测试

在以下情况下不要使用我:

  • 测试是基于属性的/生成式测试(quickcheck, proptest),用于验证不变性——这些测试的冗余是刻意设计的
  • 某测试是唯一覆盖特定代码路径的测试,即使其值看起来相似
  • 项目尚未配置代码覆盖工具
  • 您只需要添加测试,而无需先检查是否存在冗余

我的工作方式

阶段 0:了解测试环境

# 列出所有 Rust 源文件
Get-ChildItem -Recurse -Filter *.rs

# 查找所有测试注解和模块
Select-String -Path "src/**/*.rs" -Pattern "#\[test\]|#\[cfg\(test\)\]|#\[quickcheck\]|mod tests"

# 统计每个文件中的测试数量
Select-String -Path "src/**/*.rs" -Pattern "^\\s*fn (test_|check_)" | Group-Object -Property Filename

映射测试结构:

  • 源文件内的内联 #[cfg(test)] mod tests { ... } —— 可以在此处访问私有方法
  • 独立的 tests/*.rs 集成测试文件
  • 特性门控的测试模块(#[cfg(feature = "...")])
  • Quickcheck/proptest 属性测试 —— 切勿移除
  • 文档测试(`/// ``` ... ````)—— 也有助于覆盖率

关键问题:

  • 是否有多个测试模块覆盖相同代码?(例如,fractions.rs::tests vs more_tests.rs)
  • 是否存在“旧式”测试与“全面”测试并存?
  • 是否存在一个冒烟测试(it_works)复制了针对性测试中的断言?
  • 使用了哪些泛型类型参数?

阶段 1:检测冗余

级别 1:相同断言与相同值

如果两个测试使用相同或严格来说是子集的输入值测试同一个函数,则它们是冗余的:

// test_abs(冗余——是 test_fraction_abs 的子集)
#[test]
fn test_abs() {
    assert_eq!(Fraction::new(-3, 4).abs(), Fraction::new(3, 4));  // 和下面的相同
}

// test_fraction_abs(保留——覆盖更多)
#[test]
fn test_fraction_abs() {
    assert_eq!(Fraction::new(-3, 4).abs(), Fraction::new(3, 4));  // 重复
    assert_eq!(Fraction::new(3, -4).abs(), Fraction::new(3, 4));  // 独有的
}

规则:如果测试 A 的所有断言都包含在测试 B 中,则测试 A 是冗余的。移除 A。

级别 2:相同代码路径与等效值

如果两个测试使用等效输入执行同一个函数路径,则它们是冗余的:

// test_mul_div(冗余——被 test_fraction_arithmetic 包含)
#[test]
fn test_mul_div() {
    assert_eq!(Fraction::new(3, 4) * Fraction::new(5, 6), Fraction::new(5, 8));
    assert_eq!(Fraction::new(3, 4) / Fraction::new(5, 6), Fraction::new(9, 10));
}

// test_fraction_arithmetic(保留——覆盖了乘/除以及加/减/取反/倒数)
#[test]
fn test_fraction_arithmetic() {
    assert_eq!(Fraction::new(1, 2) + Fraction::new(1, 3), Fraction::new(5, 6));
    assert_eq!(Fraction::new(1, 2) - Fraction::new(1, 3), Fraction::new(1, 6));
    assert_eq!(Fraction::new(1, 2) * Fraction::new(1, 3), Fraction::new(1, 6));
    assert_eq!(Fraction::new(1, 2) / Fraction::new(1, 3), Fraction::new(3, 2));
    assert_eq!(-Fraction::new(1, 2), Fraction::new(-1, 2));
    assert_eq!(Fraction::new(1, 2).inv(), Fraction::new(2, 1));
}

规则:如果测试 A 仅覆盖操作 X 和 Y,但测试 B 覆盖了 X、Y 以及 Z,则移除 A。

级别 3:跨模块重复测试

同一个函数在两个独立的测试模块中进行了测试,且包含重叠的场景:

// fractions.rs::tests::test_fraction_from_i32_to_i64(冗余)
#[test]
fn test_fraction_from_i32_to_i64() {
    let frac: Fraction<i64> = Fraction::from(3_i32);
    assert_eq!(frac, Fraction::new(3, 1));
}

// more_tests.rs::test_from_i32_to_i64(保留——完全相同,保留较新的)
#[test]
fn test_from_i32_to_i64() {
    let f_i32 = 3;
    let f_i64 = Fraction::<i64>::from(f_i32);
    assert_eq!(f_i64, Fraction::new(3i64, 1i64));
}

规则:当测试同时存在于内联和独立模块中,并且它们完全相同地测试同一件事时,移除重复的。通常保留专用于测试文件中的那个。

级别 4:被针对性测试包含的冒烟测试

// it_works(冗余的冒烟测试——移除)
#[test]
fn it_works() {
    assert_eq!(const_gcd(4, -6), 2);              // 已被 test_const_gcd 覆盖
    assert_eq!(Fraction::new(30, -40), Fraction::new(-3, 4));  // 已被 test_fraction_construction 覆盖
    assert_eq!(Fraction::from(30), Fraction::new(30, 1));      // 已被 test_fraction_construction 覆盖
    assert_eq!(Fraction::<i32>::default(), Fraction::new(0, 1)); // 已被 test_fraction_construction 覆盖
}

规则:复制了多个针对性测试中断言的冒烟测试应被移除。它没有增加覆盖价值。

切勿移除

  • Quickcheck/proptest 测试——它们使用随机输入运行并验证不变性
  • 特定类型的唯一测试——即使看起来相似,它也在测试不同的单态化
  • 某代码路径的唯一执行者测试——请检查覆盖率报告

阶段 2:建立基线(移除前)

# 生成基线覆盖率
cargo llvm-cov --all-features --workspace --summary-only

# 同时保存详细的逐行覆盖率报告以便比较
cargo llvm-cov --all-features --workspace --lcov --output-path coverage-before.lcov

记录基线指标:

  • 区域、行、函数覆盖率百分比
  • 未覆盖区域计数
  • 总测试数:cargo test --all-features --workspace 2>&1 | Select-String "test result: ok. (\d+) passed"

阶段 3:移除冗余测试

通过精确编辑,从文件中移除每个冗余的测试函数。每完成一批移除后,运行:

cargo test --all-features --workspace

如果编译因未使用的导入而失败(例如,之前只被移除的测试使用的 use num_integer::gcd;),请清理这些导入。

常见的清理任务:

  • 移除仅用于已移除测试的 #[cfg(test)] use ...; 导入
  • 如果模块级别的 use num_integer::gcd; 仅用于已移除的冒烟测试,则移除它
  • 移除引用了已移除函数的注释掉的测试代码

阶段 4:重新构建并收集去重后指标

cargo llvm-cov --all-features --workspace --summary-only
cargo llvm-cov --all-features --workspace --lcov --output-path coverage-after-dedup.lcov

DEDUP 的验证标准:

检查项要求
所有测试通过必须通过
每个文件的行覆盖率不能下降(上升是可以的——二进制文件中测试行数减少 = 覆盖率比率提高)
未覆盖区域计数应减少或保持不变(移除死测试代码可能会提高覆盖率比率)
无新的未覆盖代码路径比较 lcov 文件中 before/after 的 FNDA:0 条目

如果任何文件的覆盖率下降,立即回退——该测试不是冗余的。

阶段 5:分析覆盖率缺口(GAP-FILL 模式)

解析 lcov 文件中的未覆盖函数

lcov 文件中的 FNDA:0 行揭示了确切哪些单态化尚未测试:

FNDA:0,_RNvMNt...INtB2_8FractionnE7new_rawB4_   # Fraction<type_n>::new_raw — 未测试
FNDA:0,_RNvMs2_Nt...INtB5_8FractionpE11is_positiveB7_  # Fraction<type_p>::is_positive — 未测试
FNDA:0,_RNvXs7_Nt...INtB5_8FractionlEINtNt...4FromhE4fromB7_  # Fraction 的 From<i8> — 未测试

Rust 中的常用缺口模式:

模式示例缺少什么
未测试的单态化Fraction<i8> 从未实例化使用 Fraction::<i8>::new(...) 添加测试
未测试的 trait 实现Fraction<i32> 的 From<i8>添加 Fraction::from(5i8) 测试
未测试的 TryFrom 实现TryFrom<Fraction<i64>> for Fraction<i32>添加 Fraction::<i32>::try_from(f_i64) 测试
私有方法signum(), is_positive() 对于 i8 未覆盖必须从同一个 #[cfg(test)] mod tests 内进行测试
特性门控代码#[cfg(feature = "serde")] 未测试使用 --features serde 添加测试
特定类型的取反Neg 为 Fraction<i8> 实现添加 -Fraction::<i8>::new(...)

解码 lcov 中的混淆名称

lcov 中 Rust 的混淆名称表示了类型参数。需要注意的关键后缀:

  • NtB5_8FractionlE... — 带有类型 l(通常是 i64)的 Fraction
  • NtB5_8FractionnE... — 带有类型 n(通常是 i8)的 Fraction
  • NtB5_8FractionxE... — 带有类型 x(通常是 i128)的 Fraction
  • NtB5_8FractionpE... — 带有类型 p(某些构建中通常是 i8)的 Fraction

确切的映射取决于单态化顺序。请与测试中实际使用的类型进行交叉引用:

  • i32 — 几乎总是已被覆盖(广泛使用)
  • i64 — 检查是否已被覆盖(通常通过 From<i32> for Fraction<i64>)
  • i8 — 很少被覆盖(需要显式测试)
  • i16 — 很少被覆盖
  • i128 — 可能通过 quickcheck 被覆盖
  • BigInt — 通过特性门控测试被覆盖

阶段 6:编写针对性测试

针对未测试的单态化(i8, i16 等)

编写涵盖构造、设置、算术、比较和特殊值的全面测试:

#[test]
fn test_i8_construction() {
    let f = Fraction::new(1i8, 2i8);
    assert_eq!(f, Fraction::new(1i8, 2));

    let f_raw = Fraction::new_raw(3i8, 4i8);
    assert_eq!(*f_raw.numer(), 3i8);
    assert_eq!(*f_raw.denom(), 4i8);
}

#[test]
fn test_i8_setters() {
    let mut f: Fraction<i8> = Fraction::new(1, 2);
    f.set_zero();
    assert!(f.is_zero());
    f.set_one();
    assert!(f.is_one());
    f.set_infinite();
    assert!(f.is_infinite());
    f.set_nan();
    assert!(f.is_nan());
}

#[test]
fn test_i8_arithmetic() {
    let f1: Fraction<i8> = Fraction::new(1, 2);
    let f2: Fraction<i8> = Fraction::new(1, 3);
    assert_eq!(f1 + f2, Fraction::new(5i8, 6));
    assert_eq!(f1 - f2, Fraction::new(1i8, 6));
    assert_eq!(f1 * f2, Fraction::new(1i8, 6));
    assert_eq!(f1 / f2, Fraction::new(3i8, 2));
    assert_eq!(-f1, Fraction::new(-1i8, 2));
    assert_eq!(f1.abs(), Fraction::new(1i8, 2));
}

针对私有方法(必须在同一个模块内)

像 signum()、is_positive()、is_negative() 这样的私有方法只能在定义它们的同一个源文件中的 #[cfg(test)] mod tests 块内进行测试:

// 在 fractions.rs 文件内的 #[cfg(test)] mod tests 块中:
#[test]
fn test_i8_signum_and_predicates() {
    let pos: Fraction<i8> = Fraction::new(3, 4);
    assert!(pos.is_positive());
    assert!(!pos.is_negative());
    assert_eq!(pos.signum(), Fraction::new(1i8, 1));

    let neg: Fraction<i8> = Fraction::new(-3, 4);
    assert!(neg.is_negative());
    assert_eq!(neg.signum(), Fraction::new(-1i8, 1));

    let zero: Fraction<i8> = Fraction::new(0, 1);
    assert_eq!(zero.signum(), Fraction::new(0i8, 1));
}

针对未测试的 trait 实现

#[test]
fn test_from_i8_to_i32() {
    let f: Fraction<i32> = Fraction::from(5i8);
    assert_eq!(f, Fraction::new(5i32, 1));
}

#[test]
fn test_try_from_i64_to_i32() {
    use core::convert::TryFrom;
    let f_i64 = Fraction::new(5i64, 1i64);
    let f_i32 = Fraction::<i32>::try_from(f_i64).unwrap();
    assert_eq!(f_i32, Fraction::new(5i32, 1));
}

测试值规则

  • 对于窄类型,使用小的、安全的值(例如,i8 最大值是 127——避免溢出)
  • 测试正值和负值
  • 测试边界情况:分母为零(= 无穷大)、零/零(= NaN)、分母带有相反符号
  • 测试恒等运算:-(-x) == x、x * 1 == x、x + 0 == x
  • 不要从单独的测试文件中测试私有方法——会导致编译失败

阶段 7:重新构建并收集填充缺口后指标

cargo test --all-features --workspace
cargo clippy --all-targets --all-features --workspace
cargo llvm-cov --all-features --workspace --summary-only

阶段 8:报告覆盖率提升情况

| 指标                | 前        | 后        | 变化        |
|-----------------------|----------|----------|----------|
| 测试数量            | N        | N'       | +Δ       |
| fractions.rs 区域    | 93.76%   | 98.45%   | +4.69%   |
| fractions.rs 行      | 92.77%   | 98.97%   | +6.20%   |
| fractions.rs 函数    | 88.10%   | 98.67%   | +10.57%  |
| 未覆盖区域        | 70       | 15       | -55      |

已移除的冗余测试:11
已填补的覆盖率缺口:Fraction<i8>, From<i8>, From<u8>, TryFrom<

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

相关技能