dedup-rust-tests

Encontra e remove testes unitários Rust redundantes e preenche lacunas de cobertura com testes direcionados — tudo verificado com comparação antes/depois do cargo llvm-cov.

O que é dedup-rust-tests?

Esta habilidade automatiza a otimização de suítes de teste Rust em dois modos combinados: DEDUP (remoção de testes redundantes) e GAP-FILL (adição de testes para cobrir lacunas). No modo DEDUP, identifica testes que duplicam as mesmas asserções ou caminhos de código, removendo-os sem perda de cobertura, verificando a equivalência via relatórios `cargo llvm-cov` antes e depois. No modo GAP-FILL, analisa o relativo lcov para detectar monomorfizações não testadas de tipos genéricos, implementações de traits ausentes, métodos privados não cobertos e código atrás de features flags, gerando automaticamente testes unitários direcionados que aumentam a cobertura. A habilidade é especialmente útil em projetos Rust com crescimento orgânico de testes, onde múltiplos módulos de teste ou smoke tests duplicam cenários, e suporta qualquer crate Rust com `cargo llvm-cov` configurado. O fluxo inclui construção de baseline, remoção segura com rollback automático se a cobertura cair, e geração de relatório final de melhoria.

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

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Habilidade: dedup-rust-tests

As referências a arquivos (@path) nesta habilidade são relativas a este diretório.

O que eu faço

Eu tenho dois modos de operação que trabalham juntos:

  1. DEDUP — Identificar e remover testes unitários Rust redundantes onde outro teste já cobre os mesmos caminhos de código com valores de entrada idênticos ou equivalentes. Verificado via comparação de cobertura cargo llvm-cov (antes vs. depois).

  2. GAP-FILL — Após a deduplicação, analisar o relatório llvm-cov para encontrar código não testado ou subtestado (monomorfizações específicas, implementações de traits, código atrás de features), então escrever testes unitários direcionados para preencher essas lacunas. Verificado via melhoria de cobertura.

Ambos os modos compartilham um fluxo de trabalho comum de build baseline e medição de cobertura.

Quando me usar

Para DEDUP:

  • Sua suíte de testes Rust cresceu organicamente com múltiplos módulos de teste cobrindo os mesmos caminhos de código
  • Um módulo de teste inline "no estilo antigo" (#[cfg(test)] mod tests) coexiste com arquivos de teste separados e mais novos (more_tests.rs) que subsomem os testes antigos
  • Um "teste fumigatório" (it_works) duplica asserções de múltiplos testes focados
  • Você herdou uma base de código com cenários de teste duplicados (ex.: test_abs + test_fraction_abs ambos testando o mesmo método com entradas sobrepostas)
  • Você quer reduzir o tempo de teste no CI sem perder cobertura

Para GAP-FILL:

  • Tipos genéricos só são testados para um tipo concreto (ex.: Fraction<i32>) — o relatório de cobertura revela monomorfizações não testadas (Fraction<i8>, Fraction<i128>)
  • Implementações de traits (From<i8>, TryFrom<...>) mostram 0% de cobertura
  • Código atrás de features (#[cfg(feature = "...")]) não está testado
  • Métodos privados (signum(), is_positive()) estão descobertos porque só podem ser testados dentro de seu próprio módulo

NÃO me use quando:

  • Testes são baseados em propriedades / generativos (quickcheck, proptest) testando invariantes — estes são intencionalmente redundantes por design
  • O teste é o único que exercita um caminho de código particular, mesmo se os valores parecerem similares
  • O projeto não tem ferramentas de cobertura de código configuradas
  • Você só precisa adicionar testes sem primeiro verificar redundância

Como eu funciono

Fase 0: Entender o cenário dos testes

# Listar todos os arquivos fonte Rust
Get-ChildItem -Recurse -Filter *.rs

# Encontrar todas as anotações de teste e módulos
Select-String -Path "src/**/*.rs" -Pattern "#\[test\]|#\[cfg\(test\)\]|#\[quickcheck\]|mod tests"

# Contar testes por arquivo
Select-String -Path "src/**/*.rs" -Pattern "^\\s*fn (test_|check_)" | Group-Object -Property Filename

Mapear a estrutura de testes:

  • Módulos inline #[cfg(test)] mod tests { ... } dentro de arquivos fonte — métodos privados acessíveis aqui
  • Arquivos de teste de integração autônomos tests/*.rs
  • Módulos de teste atrás de features (#[cfg(feature = "...")])
  • Testes de propriedade quickcheck/proptest — nunca remova estes
  • Doc-tests (`/// ``` ... ````) — também contribuem para a cobertura

Perguntas-chave:

  • Existem múltiplos módulos de teste cobrindo o mesmo código? (ex.: fractions.rs::tests vs more_tests.rs)
  • Existem testes "no estilo antigo" junto com testes "abrangentes"?
  • Existe um teste fumigatório (it_works) que duplica asserções de testes focados?
  • Quais parâmetros de tipo genérico são exercitados?

Fase 1: Detectar redundância

Nível 1: Mesmas asserções com valores idênticos

Dois testes são redundantes se exercitam a mesma função com valores de entrada idênticos ou estritamente subconjunto:

// test_abs (redundante — subconjunto de test_fraction_abs)
#[test]
fn test_abs() {
    assert_eq!(Fraction::new(-3, 4).abs(), Fraction::new(3, 4));  // igual ao abaixo
}

// test_fraction_abs (manter — cobre mais)
#[test]
fn test_fraction_abs() {
    assert_eq!(Fraction::new(-3, 4).abs(), Fraction::new(3, 4));  // duplicado
    assert_eq!(Fraction::new(3, -4).abs(), Fraction::new(3, 4));  // único
}

Regra: Se todas as asserções do teste A estão contidas no teste B, o teste A é redundante. Remova A.

Nível 2: Mesmo caminho de código com valores equivalentes

Dois testes são redundantes se exercitam o mesmo caminho de função com entradas equivalentes:

// test_mul_div (redundante — subsumido por 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 (manter — cobre mul/div E também add/sub/neg/reciprocal)
#[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));
}

Regra: Se o teste A cobre apenas as operações X e Y, mas o teste B cobre X, Y e Z, remova A.

Nível 3: Testes duplicados entre módulos

A mesma função é testada em dois módulos de teste separados com cenários sobrepostos:

// fractions.rs::tests::test_fraction_from_i32_to_i64 (redundante)
#[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 (manter — idêntico, manter o mais novo)
#[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));
}

Regra: Quando testes existem tanto em módulos inline quanto em separados, e testam a mesma coisa de forma idêntica, remova o duplicado. Geralmente mantenha o que está no arquivo de teste dedicado.

Nível 4: Teste fumigatório subsumido por testes focados

// it_works (teste fumigatório redundante — remover)
#[test]
fn it_works() {
    assert_eq!(const_gcd(4, -6), 2);              // coberto por test_const_gcd
    assert_eq!(Fraction::new(30, -40), Fraction::new(-3, 4));  // coberto por test_fraction_construction
    assert_eq!(Fraction::from(30), Fraction::new(30, 1));      // coberto por test_fraction_construction
    assert_eq!(Fraction::<i32>::default(), Fraction::new(0, 1)); // coberto por test_fraction_construction
}

Regra: Um teste fumigatório que duplica asserções de vários testes focados deve ser removido. Ele não adiciona valor de cobertura.

NUNCA remova

  • Testes quickcheck/proptest — eles rodam com entradas aleatórias e verificam invariantes
  • O único teste para um tipo específico — mesmo que pareça similar, ele exercita uma monomorfização diferente
  • Testes que são os únicos a exercitar um caminho de código — verifique o relatório de cobertura

Fase 2: Construir baseline (ANTES da remoção)

# Gerar cobertura baseline
cargo llvm-cov --all-features --workspace --summary-only

# Também salvar relatório detalhado para comparação em nível de linha
cargo llvm-cov --all-features --workspace --lcov --output-path coverage-before.lcov

Registrar métricas baseline:

  • Porcentagens de cobertura de regiões, linhas, funções
  • Contagem de regiões perdidas
  • Contagem total de testes: cargo test --all-features --workspace 2>&1 | Select-String "test result: ok. (\d+) passed"

Fase 3: Remover os testes redundantes

Remova cada função de teste redundante de seu arquivo usando edições exatas. Após cada lote de remoção, execute:

cargo test --all-features --workspace

Se a compilação falhar devido a imports não utilizados (ex.: use num_integer::gcd; que só era usado pelo teste removido), limpe-os.

Tarefas comuns de limpeza:

  • Remover imports #[cfg(test)] use ...; que eram apenas para testes removidos
  • Remover use num_integer::gcd; em nível de módulo se usado apenas pelo teste fumigatório removido
  • Remover código de teste comentado que referencia funções removidas

Fase 4: Reconstruir e coletar métricas APÓS-DEDUP

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

Critérios de veredito para DEDUP:

VerificaçãoRequisito
Todos os testes passamDeve passar
Cobertura de linha por arquivoNenhuma diminuição (aumento é OK — menos linhas de teste no binário = maior taxa de cobertura)
Contagem de regiões perdidasDeve diminuir ou permanecer igual (remover código de teste morto pode aumentar a taxa de cobertura)
Nenhum novo caminho de código descobertoComparar entradas FNDA:0 no lcov antes/depois

Se a cobertura cair em qualquer arquivo, reverta imediatamente — o teste NÃO era redundante.

Fase 5: Analisar lacunas de cobertura (modo GAP-FILL)

Analisar o arquivo lcov em busca de funções descobertas

As linhas FNDA:0 no arquivo lcov revelam exatamente quais monomorfizações não estão testadas:

FNDA:0,_RNvMNt...INtB2_8FractionnE7new_rawB4_   # Fraction<type_n>::new_raw — não testado
FNDA:0,_RNvMs2_Nt...INtB5_8FractionpE11is_positiveB7_  # Fraction<type_p>::is_positive — não testado
FNDA:0,_RNvXs7_Nt...INtB5_8FractionlEINtNt...4FromhE4fromB7_  # From<i8> for Fraction — não testado

Padrões comuns de lacunas em Rust:

PadrãoExemploO que está faltando
Monomorfização não testadaFraction<i8> nunca instanciadoAdicionar testes com Fraction::<i8>::new(...)
Implementação de trait não testadaFrom<i8> for Fraction<i32>Adicionar teste Fraction::from(5i8)
Implementação de TryFrom não testadaTryFrom<Fraction<i64>> for Fraction<i32>Adicionar teste Fraction::<i32>::try_from(f_i64)
Métodos privadossignum(), is_positive() descobertos para i8Deve testar de dentro do mesmo #[cfg(test)] mod tests
Código atrás de feature#[cfg(feature = "serde")] não testadoAdicionar teste com --features serde
Neg para tipo específicoImplementação de Neg para Fraction<i8>Adicionar -Fraction::<i8>::new(...)

Decodificar nomes ofuscados do lcov

Os nomes ofuscados do Rust no lcov indicam parâmetros de tipo. Sufixos-chave a procurar:

  • NtB5_8FractionlE... — Fraction com tipo l (comumente i64)
  • NtB5_8FractionnE... — Fraction com tipo n (comumente i8)
  • NtB5_8FractionxE... — Fraction com tipo x (comumente i128)
  • NtB5_8FractionpE... — Fraction com tipo p (comumente i8 em algumas compilações)

O mapeamento exato depende da ordem de monomorfização. Faça referência cruzada com quais tipos são realmente usados nos testes:

  • i32 — quase sempre coberto (usado em toda parte)
  • i64 — verificar se está coberto (frequentemente via From<i32> for Fraction<i64>)
  • i8 — raramente coberto (precisa de teste explícito)
  • i16 — raramente coberto
  • i128 — pode estar coberto via quickcheck
  • BigInt — coberto via testes atrás de feature

Fase 6: Escrever testes direcionados

Para monomorfizações não testadas (i8, i16, etc)

Escreva um teste abrangente cobrindo construção, setters, aritmética, comparação e valores especiais:

#[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));
}

Para métodos privados (devem estar no mesmo módulo)

Métodos privados como signum(), is_positive(), is_negative() só podem ser testados de dentro de um bloco #[cfg(test)] mod tests dentro do mesmo arquivo fonte onde estão definidos:

// Em fractions.rs, dentro de #[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));
}

Para implementações de trait não testadas

#[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));
}

Regras para valores de teste

  • Use valores pequenos e seguros para tipos estreitos (ex.: máximo de i8 é 127 — evite estouro)
  • Teste valores positivos e negativos
  • Teste casos de borda: denominador zero (= infinito), zero/zero (= NaN), denominador com sinal oposto
  • Teste operações identidade: -(-x) == x, x * 1 == x, x + 0 == x
  • NÃO teste métodos privados de arquivos de teste separados — eles falharão na compilação

Fase 7: Reconstruir e coletar métricas APÓS-GAP-FILL

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

Fase 8: Relatar melhoria de cobertura

| Métrica                | Antes   | Depois  | Δ        |
|-----------------------|----------|----------|----------|
| Contagem de testes    | N        | N'       | +Δ       |
| Regiões em fractions.rs  | 93.76%   | 98.45%   | +4.69%   |
| Linhas em fractions.rs    | 92.77%   | 98.97%   | +6.20%   |
| Funções em fractions.rs| 88.10%   | 98.67%   | +10.57%  |
| Regiões perdidas        | 70       | 15       | -55      |

Testes redundantes removidos: 11
Lacunas de cobertura preenchidas: Fraction<i8>, From<i8>, From<u8>, TryFrom<...>

Individual skills in this repo

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

Habilidades Relacionadas