Community程式設計與開發github.com

luk036/dedup-rust-tests

尋找並移除冗餘的 Rust 單元測試,然後用針對性的測試填補覆蓋率缺口——全部通過前後的 cargo llvm-cov 比較驗證。

dedup-rust-tests 是什麼?

此技能用於優化 Rust 測試套件。它提供兩種協同工作模式:DEDUP(重複檢測)和 GAP-FILL(缺口填補)。DEDUP 模式會識別並刪除覆蓋相同程式碼路徑的冗餘測試(例如,被更全面的測試涵蓋的煙霧測試或舊式內聯測試),並通過 `cargo llvm-cov` 覆蓋率比較(前後對比)進行驗證。GAP-FILL 模式則分析覆蓋率報告,以發現未測試或測試不足的程式碼,例如未測試的泛型具體化(如 `Fraction<i8>`)、未測試的 trait 實現或私有方法,然後編寫專門的測試來填補這些缺口。該技能支援所有 Rust 專案,但特別適用於有機增長的測試套件,其中重複和缺口很常見。它強烈依賴於 `llvm-cov` 來量化覆蓋率的改善,並且對於基於屬性的測試(quickcheck/proptest)嚴謹地維持不刪除的原則。

相容平台~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 覆蓋率比較(前後對比)進行驗證。

  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)測試不變量——這些重複是有意為之
  • 該測試是唯一執行特定程式碼路徑的測試,即使輸入值看起來相似
  • 該專案沒有配置程式碼覆蓋率工具
  • 你只需要添加測試,而無需先檢查重複性

我的工作方式

第零階段:了解測試環境

# 列出所有 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:相同斷言與相同值

如果兩個測試使用完全相同或嚴格子集的輸入值測試同一個函數,則它們是重複的:

// 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 測試——它們使用隨機輸入執行並驗證不變量
  • 特定類型唯一的測試——即使看起來相似,它測試的是不同的泛型具體化
  • 程式碼路徑的唯一執行測試——檢查覆蓋率報告

第二階段:建立基準線(移除前)

# 生成基準覆蓋率
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"

第三階段:移除重複測試

使用精確編輯從其檔案中移除每個重複的測試函數。每次移除批次後,執行:

cargo test --all-features --workspace

如果編譯因未使用的導入而失敗(例如,use num_integer::gcd; 僅被移除的測試使用),請清理它們。

常見清理任務:

  • 移除僅用於已移除測試的 #[cfg(test)] use ...; 導入
  • 如果 use num_integer::gcd; 僅被已移除的煙霧測試使用,則移除模組級別的導入
  • 移除引用已移除函數的註解掉的測試程式碼

第四階段:重新構建並收集去重後指標

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

DEDUP 模式的驗證標準:

檢查項要求
所有測試通過必須通過
每個檔案的行覆蓋率不得下降(上升是可以的——二進位制檔案中測試行數減少 = 更高的覆蓋率比例)
遺漏區域計數應下降或保持不變(移除死測試程式碼可能會提高覆蓋率比例)
沒有新的未覆蓋程式碼路徑比較前後 lcov 中的 FNDA:0 條目

如果任何檔案的覆蓋率下降,立即恢復——該測試並非重複。

第五階段:分析覆蓋率缺口(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_  # From<i8> for Fraction — 未測試

Rust 中常見的缺口模式:

模式範例缺少什麼
未測試的泛型具體化Fraction<i8> 從未被實例化使用 Fraction::<i8>::new(...) 添加測試
未測試的 trait 實現From<i8> for Fraction<i32>添加 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 添加測試
特定類型的 NegNeg 實現對於 Fraction<i8>添加 -Fraction::<i8>::new(...)

解碼 lcov 中的修飾名稱

lcov 中 Rust 的修飾名稱指示類型參數。要查找的關鍵後綴:

  • NtB5_8FractionlE... — 類型為 l 的 Fraction(通常是 i64)
  • NtB5_8FractionnE... — 類型為 n 的 Fraction(通常是 i8)
  • NtB5_8FractionxE... — 類型為 x 的 Fraction(通常是 i128)
  • NtB5_8FractionpE... — 類型為 p 的 Fraction(在某些構建中通常是 i8)

確切的映射取決於泛型具體化順序。與測試中實際使用的類型交叉引用:

  • i32——幾乎總是覆蓋的(到處使用)
  • i64——檢查是否覆蓋(通常是通過 From<i32> for Fraction<i64>)
  • i8——很少覆蓋(需要顯式測試)
  • i16——很少覆蓋
  • i128——可能通過 quickcheck 覆蓋
  • BigInt——通過功能門控測試覆蓋

第六階段:編寫針對性測試

對於未測試的泛型具體化(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
  • 不要從單獨的測試檔案測試私有方法——它們會編譯失敗

第七階段:重新構建並收集填補缺口後指標

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

第八階段:報告覆蓋率改善

| 指標                | 之前     | 之後     | 差值       |
|-----------------------|----------|----------|----------|
| 測試計數            | 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.

相關技能