Community코딩 & 개발github.com

luk036/dedup-rust-tests

중복된 Rust 유닛 테스트를 찾아 제거하고, 대상이 지정된 테스트로 커버리지 격차를 메우며, 전후 cargo llvm-cov 비교로 검증합니다.

dedup-rust-tests란 무엇인가요?

이 스킬은 Rust 프로젝트의 테스트 스위트를 최적화하기 위해 설계되었습니다. DEDUP 모드에서는 동일한 코드 경로를 중복해서 테스트하는 불필요한 테스트를 식별하고 제거하여 CI 시간을 단축하고 코드를 간결하게 유지합니다. GAP-FILL 모드에서는 llvm-cov 보고서를 분석하여 특정 단형화(monomorphization)나 트레이트 구현, 기능 플래그가 적용된 코드 등 테스트되지 않은 부분을 찾아내고, 이를 위한 목표 지향적인 테스트를 작성합니다. 이 스킬은 cargo llvm-cov를 사용하여 전후 커버리지를 비교함으로써 모든 변경 사항이 실제 코드 커버리지를 유지하거나 개선하도록 보장합니다. 주로 Claude Code, Cursor, Codex와 같은 AI 에이전트에서 사용할 수 있으며, 개발자가 수동으로 작업할 때보다 훨씬 빠르고 정확하게 테스트 정리와 커버리지 개선을 수행합니다.

지원 대상~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에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

스킬: dedup-rust-tests

이 스킬의 파일 참조(@path)는 이 디렉토리를 기준으로 합니다.

내가 하는 일

저는 함께 작동하는 두 가지 모드를 가지고 있습니다:

  1. DEDUP — 다른 테스트가 이미 동일한 코드 경로를 동일하거나 동등한 입력 값으로 커버하고 있는 중복된 Rust 유닛 테스트를 식별하고 제거합니다. cargo llvm-cov 커버리지 비교(전후)를 통해 검증합니다.

  2. GAP-FILL — 중복 제거 후, llvm-cov 보고서를 분석하여 테스트되지 않았거나 부족하게 테스트된 코드(특정 단형화, 트레이트 구현, 기능 플래그가 적용된 코드)를 찾고, 해당 격차를 메우기 위해 대상이 지정된 테스트를 작성합니다. 커버리지 개선을 통해 검증합니다.

두 모드 모두 공통 기본 빌드 및 커버리지 측정 워크플로우를 공유합니다.

나를 사용해야 하는 경우

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>)가 드러납니다.
  • 트레이트 구현(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 (유지 — mul/div 및 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));
}

규칙: 테스트 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 전후의 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_  # From<i8> for Fraction — 테스트되지 않음

Rust의 일반적인 격차 패턴:

패턴예시누락된 것
테스트되지 않은 단형화Fraction<i8>가 인스턴스화되지 않음Fraction::<i8>::new(...)로 테스트 추가
테스트되지 않은 트레이트 구현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 구현 for Fraction<i8>-Fraction::<i8>::new(...) 추가

lcv 맹글된 이름 디코딩

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

테스트되지 않은 트레이트 구현의 경우

#[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 — 오버플로우 방지)의 경우 작고 안전한 값을 사용합니다.
  • 양수와 음수 값을 모두 테스트합니다.
  • 가장자리 경우 테스트: 분모 0(= 무한대), 0/0(= 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.

관련 스킬