CommunityProgramación y desarrollogithub.com

luk036/dedup-rust-tests

Encuentra y elimina pruebas unitarias redundantes en Rust, y luego llena los vacíos de cobertura con pruebas específicas, todo verificado mediante una comparación de cobertura antes/después con cargo llvm-cov.

¿Qué es dedup-rust-tests?

Esta skill está diseñada para optimizar la suite de pruebas unitarias de un proyecto Rust mediante la identificación y eliminación de pruebas redundantes (modo DEDUP), seguido de un análisis de los informes de cobertura para agregar pruebas que cubran código insuficientemente probado (modo GAP-FILL). Es útil para desarrolladores que buscan reducir el tiempo de ejecución de pruebas en CI sin perder cobertura, o que necesitan asegurarse de que cada camino de código esté probado de manera eficiente. Soporta el análisis de cobertura con `cargo llvm-cov`, y el proceso se verifica con una comparación detallada antes y después de los cambios. La skill se enfoca en pruebas unitarias tradicionales (no property-based), maneja monomorfizaciones de genéricos, implementaciones de traits, código condicional por features y métodos privados. El flujo de trabajo incluye la medición de la línea base, eliminación segura de pruebas redundantes, y la escritura precisa de nuevas pruebas basadas en los informes LCOV.

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

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Habilidad: dedup-rust-tests

Las referencias a archivos (@ruta) en esta habilidad son relativas a este directorio.

Lo que hago

Tengo dos modos de operación que trabajan juntos:

  1. DEDUP — Identificar y eliminar pruebas unitarias redundantes de Rust donde otra prueba ya cubre las mismas rutas de código con valores de entrada idénticos o equivalentes. Verificado mediante comparación de cobertura cargo llvm-cov (antes vs. después).

  2. GAP-FILL — Después de la deduplicación, analizar el informe de llvm-cov para encontrar código no probado o insuficientemente probado (monomorfizaciones específicas, implementaciones de traits, código condicional por features) y luego escribir pruebas unitarias específicas para llenar esos vacíos. Verificado mediante mejora de cobertura.

Ambos modos comparten un flujo de trabajo común de construcción base y medición de cobertura.

Cuándo usarme

Para DEDUP:

  • Tu suite de pruebas Rust ha crecido orgánicamente con múltiples módulos de prueba cubriendo las mismas rutas de código.
  • Un módulo de prueba en línea "al estilo antiguo" (#[cfg(test)] mod tests) coexiste con archivos de prueba separados más nuevos (more_tests.rs) que subsumen las pruebas antiguas.
  • Una "prueba de humo" (it_works) duplica aserciones de múltiples pruebas enfocadas.
  • Heredaste un código base con escenarios de prueba duplicados (ej., test_abs + test_fraction_abs que prueban el mismo método con entradas superpuestas).
  • Quieres reducir el tiempo de prueba en CI sin perder cobertura.

Para GAP-FILL:

  • Tipos genéricos solo se prueban para un tipo concreto (ej., Fraction<i32>); el informe de cobertura revela monomorfizaciones no probadas (Fraction<i8>, Fraction<i128>).
  • Implementaciones de traits (From<i8>, TryFrom<...>) muestran cobertura del 0%.
  • Código condicional por features (#[cfg(feature = "...")]) no está probado.
  • Métodos privados (signum(), is_positive()) no están cubiertos porque solo pueden probarse desde su propio módulo.

NO me uses cuando:

  • Las pruebas están basadas en propiedades / generativas (quickcheck, proptest) probando invariantes; estas son intencionalmente redundantes por diseño.
  • La prueba es la única que ejercita una ruta de código particular, incluso si los valores parecen similares.
  • El proyecto no tiene configurada ninguna herramienta de cobertura de código.
  • Solo necesitas agregar pruebas sin verificar primero la redundancia.

Cómo trabajo

Fase 0: Entender el panorama de pruebas

# Listar todos los archivos fuente Rust
Get-ChildItem -Recurse -Filter *.rs

# Encontrar todas las anotaciones y módulos de prueba
Select-String -Path "src/**/*.rs" -Pattern "#\[test\]|#\[cfg\(test\)\]|#\[quickcheck\]|mod tests"

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

Mapear la estructura de pruebas:

  • Módulos en línea #[cfg(test)] mod tests { ... } dentro de archivos fuente; aquí son accesibles los métodos privados.
  • Archivos de prueba de integración independientes tests/*.rs.
  • Módulos de prueba condicionales por features (#[cfg(feature = "...")]).
  • Pruebas de propiedades quickcheck/proptest; nunca eliminar estas.
  • Doc-tests (/// ``` ... `` ); también contribuyen a la cobertura.

Preguntas clave:

  • ¿Hay múltiples módulos de prueba cubriendo el mismo código? (ej., fractions.rs::tests vs more_tests.rs)
  • ¿Hay pruebas "al estilo antiguo" junto a pruebas "completas"?
  • ¿Hay una prueba de humo (it_works) que duplica aserciones de pruebas enfocadas?
  • ¿Qué parámetros de tipos genéricos se ejercitan?

Fase 1: Detectar redundancia

Nivel 1: Mismas aserciones con valores idénticos

Dos pruebas son redundantes si ejercitan la misma función con valores de entrada idénticos o estrictamente 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 que abajo
}

// test_fraction_abs (conservar — cubre más)
#[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
}

Regla: Si todas las aserciones de la prueba A están contenidas en la prueba B, la prueba A es redundante. Eliminar A.

Nivel 2: Misma ruta de código con valores equivalentes

Dos pruebas son redundantes si ejercitan la misma ruta de función con entradas equivalentes:

// test_mul_div (redundante — subsumida 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 (conservar — cubre mul/div Y también 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));
}

Regla: Si la prueba A cubre solo las operaciones X e Y, pero la prueba B cubre X, Y y Z, eliminar A.

Nivel 3: Prueba duplicada entre módulos

La misma función se prueba en dos módulos de prueba separados con escenarios superpuestos:

// 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 (conservar — idéntico, conservar el más nuevo)
#[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));
}

Regla: Cuando las pruebas residen tanto en módulos en línea como separados, y prueban lo mismo de manera idéntica, eliminar la duplicada. Generalmente conservar la que está en el archivo de prueba dedicado.

Nivel 4: Prueba de humo subsumida por pruebas enfocadas

// it_works (prueba de humo redundante — eliminar)
#[test]
fn it_works() {
    assert_eq!(const_gcd(4, -6), 2);              // cubierto por test_const_gcd
    assert_eq!(Fraction::new(30, -40), Fraction::new(-3, 4));  // cubierto por test_fraction_construction
    assert_eq!(Fraction::from(30), Fraction::new(30, 1));      // cubierto por test_fraction_construction
    assert_eq!(Fraction::<i32>::default(), Fraction::new(0, 1)); // cubierto por test_fraction_construction
}

Regla: Una prueba de humo que duplica aserciones de múltiples pruebas enfocadas debe ser eliminada. No aporta valor de cobertura.

NUNCA eliminar

  • Pruebas quickcheck/proptest — se ejecutan con entradas aleatorias y verifican invariantes.
  • La única prueba para un tipo específico — incluso si parece similar, ejercita una monomorfización diferente.
  • Pruebas que son las únicas que ejercitan una ruta de código — verificar el informe de cobertura.

Fase 2: Construir línea base (ANTES de la eliminación)

# Generar cobertura base
cargo llvm-cov --all-features --workspace --summary-only

# También guardar informe detallado para comparación a nivel de línea
cargo llvm-cov --all-features --workspace --lcov --output-path coverage-before.lcov

Registrar métricas base:

  • Porcentajes de cobertura de regiones, líneas, funciones.
  • Número de regiones no cubiertas.
  • Conteo total de pruebas: cargo test --all-features --workspace 2>&1 | Select-String "test result: ok. (\d+) passed"

Fase 3: Eliminar las pruebas redundantes

Eliminar cada función de prueba redundante de su archivo usando ediciones exactas. Después de cada lote de eliminación, ejecutar:

cargo test --all-features --workspace

Si la compilación falla debido a importaciones no utilizadas (ej., use num_integer::gcd; que solo se usaba en la prueba eliminada), limpiarlas.

Tareas comunes de limpieza:

  • Eliminar importaciones #[cfg(test)] use ...; que solo se usaban para pruebas eliminadas.
  • Eliminar importaciones a nivel de módulo use num_integer::gcd; si solo se usaban en una prueba de humo eliminada.
  • Eliminar código de prueba comentado que haga referencia a funciones eliminadas.

Fase 4: Reconstruir y recopilar métricas DESPUÉS DE DEDUP

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

Criterios de veredicto para DEDUP:

VerificaciónRequisito
Todas las pruebas pasanDebe pasar
Cobertura de líneas por archivoSin disminución (aumento es aceptable — menos líneas de prueba en el binario = mayor proporción de cobertura)
Número de regiones no cubiertasDebe disminuir o mantenerse igual
Sin nuevas rutas de código descubiertasComparar entradas FNDA:0 en lcov antes/después

Si la cobertura disminuye en algún archivo, revertir inmediatamente: la prueba NO era redundante.

Fase 5: Analizar vacíos de cobertura (modo GAP-FILL)

Analizar el archivo lcov para funciones descubiertas

Las líneas FNDA:0 en el archivo lcov revelan exactamente qué monomorfizaciones no están probadas:

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

Patrones comunes de vacíos en Rust:

PatrónEjemploLo que falta
Monomorfización no probadaFraction<i8> nunca instanciadoAgregar pruebas con Fraction::<i8>::new(...)
Implementación de trait no probadaFrom<i8> para Fraction<i32>Agregar prueba Fraction::from(5i8)
Implementación TryFrom no probadaTryFrom<Fraction<i64>> para Fraction<i32>Agregar prueba Fraction::<i32>::try_from(f_i64)
Métodos privadossignum(), is_positive() descubiertos para i8Debe probarse desde dentro del mismo #[cfg(test)] mod tests
Código condicional por features#[cfg(feature = "serde")] no probadoAgregar prueba con --features serde
Neg para tipo específicoImplementación de Neg para Fraction<i8>Agregar -Fraction::<i8>::new(...)

Decodificar nombres modificados de lcov

Los nombres modificados de Rust en lcov indican parámetros de tipo. Sufijos clave a buscar:

  • NtB5_8FractionlE... — Fraction con tipo l (comúnmente i64)
  • NtB5_8FractionnE... — Fraction con tipo n (comúnmente i8)
  • NtB5_8FractionxE... — Fraction con tipo x (comúnmente i128)
  • NtB5_8FractionpE... — Fraction con tipo p (comúnmente i8 en algunas compilaciones)

El mapeo exacto depende del orden de monomorfización. Referencia cruzada con los tipos realmente usados en pruebas:

  • i32 — casi siempre cubierto (usado en toda la base de código)
  • i64 — verificar si está cubierto (a menudo a través de From<i32> for Fraction<i64>)
  • i8 — raramente cubierto (necesita prueba explícita)
  • i16 — raramente cubierto
  • i128 — puede estar cubierto vía quickcheck
  • BigInt — cubierto vía pruebas condicionales por features

Fase 6: Escribir pruebas específicas

Para monomorfizaciones no probadas (i8, i16, etc.)

Escribir una prueba completa que cubra construcción, setters, aritmética, comparación y valores especiales:

#[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 (deben estar en el mismo módulo)

Los métodos privados como signum(), is_positive(), is_negative() solo pueden probarse desde dentro de un bloque #[cfg(test)] mod tests dentro del mismo archivo fuente donde están definidos:

// En 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 implementaciones de traits no probadas

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

Reglas para valores de prueba

  • Usar valores pequeños y seguros para tipos estrechos (ej., el máximo de i8 es 127; evitar desbordamiento).
  • Probar valores tanto positivos como negativos.
  • Probar casos límite: denominador cero (= infinito), cero/cero (= NaN), denominador con signo opuesto.
  • Probar operaciones de identidad: -(-x) == x, x * 1 == x, x + 0 == x.
  • NO probar métodos privados desde archivos de prueba separados; fallarán en la compilación.

Fase 7: Reconstruir y recopilar métricas DESPUÉS DE GAP-FILL

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

Fase 8: Informar mejora de cobertura

| Métrica                | Antes     | Después   | Δ        |
|------------------------|-----------|-----------|----------|
| Número de pruebas      | N         | N'        | +Δ       |
| Regiones fractions.rs | 93.76%    | 98.45%    | +4.69%   |
| Líneas fractions.rs   | 92.77%    | 98.97%    | +6.20%   |
| Funciones fractions.rs| 88.10%    | 98.67%    | +10.57%  |
| Regiones no cubiertas  | 70        | 15        | -55      |

Pruebas redundantes eliminadas: 11
Vacios de cobertura llenados: Fraction<i8>, From<i8>, From<u8>, TryFrom<...>

Individual skills in this repo

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

Skills relacionados