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:
-
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). -
GAP-FILL — Após a deduplicação, analisar o relatório
llvm-covpara 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_absambos 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::testsvsmore_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ção | Requisito |
|---|---|
| Todos os testes passam | Deve passar |
| Cobertura de linha por arquivo | Nenhuma diminuição (aumento é OK — menos linhas de teste no binário = maior taxa de cobertura) |
| Contagem de regiões perdidas | Deve diminuir ou permanecer igual (remover código de teste morto pode aumentar a taxa de cobertura) |
| Nenhum novo caminho de código descoberto | Comparar 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ão | Exemplo | O que está faltando |
|---|---|---|
| Monomorfização não testada | Fraction<i8> nunca instanciado | Adicionar testes com Fraction::<i8>::new(...) |
| Implementação de trait não testada | From<i8> for Fraction<i32> | Adicionar teste Fraction::from(5i8) |
| Implementação de TryFrom não testada | TryFrom<Fraction<i64>> for Fraction<i32> | Adicionar teste Fraction::<i32>::try_from(f_i64) |
| Métodos privados | signum(), is_positive() descobertos para i8 | Deve testar de dentro do mesmo #[cfg(test)] mod tests |
| Código atrás de feature | #[cfg(feature = "serde")] não testado | Adicionar teste com --features serde |
| Neg para tipo específico | Implementaçã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 tipol(comumentei64)NtB5_8FractionnE...— Fraction com tipon(comumentei8)NtB5_8FractionxE...— Fraction com tipox(comumentei128)NtB5_8FractionpE...— Fraction com tipop(comumentei8em 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 viaFrom<i32> for Fraction<i64>)i8— raramente coberto (precisa de teste explícito)i16— raramente cobertoi128— pode estar coberto via quickcheckBigInt— 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<...>