Community코딩 & 개발github.com

luk036/dedup-cpp-tests

중복된 C++ 단위 테스트를 찾아 제거하고, gcov 리포트를 분석하여 테스트되지 않은 코드를 대상으로 테스트를 추가한 후 전/후 커버리지 비교로 검증합니다.

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

이 스킬은 C++ 테스트 스위트에서 중복 테스트 파일을 식별하여 제거하고, 커버리지 격차를 메우는 자동화된 워크플로우를 제공합니다. GCC gcov, Clang llvm-cov 또는 MSVC OpenCppCoverage 등 다양한 커버리지 도구를 지원하며, CMake 기반 프로젝트에 최적화되어 있습니다. DEDUP 모드는 파일 수준에서 중복 테스트 시나리오(90% 이상 겹치는 경우)를 찾아 제거하고, GAP-FILL 모드는 gcov 리포트를 분석해 테스트되지 않은 소스 파일, 커버리지가 낮은 헤더, 조건부 분기 등을 대상으로 테스트를 추가합니다. 전/후 커버리지 비교를 통해 테스트 제거나 추가의 영향이 항상 검증되며, 속성 기반 테스트나 다른 입력 값을 사용하는 테스트는 중복으로 간주하지 않습니다. 각 단계는 상세한 지침과 함께 제공되어 개발자가 수동으로 수행할 수 있도록 안내합니다.

지원 대상~Claude Code~Codex CLI~Cursor✓OpenCode
npx skills add https://github.com/luk036/agent-skills/tree/HEAD/skills/dedup-cpp-tests

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

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

문서

내가 하는 일

두 가지 모드로 작동합니다:

  1. 중복 제거 (DEDUP) — 다른 파일이 동일한 입력 값으로 동일한 코드 경로를 커버하는 중복 테스트 파일/시나리오를 식별하여 제거합니다. gcov 커버리지 비교(전 vs 후)로 검증합니다.

  2. 격차 메우기 (GAP-FILL) — 중복 제거 후 커버리지 리포트를 분석하여 테스트되지 않았거나 부족하게 테스트된 코드(헤더 및 컴파일된 소스)를 찾고, 대상이 지정된 단위 테스트를 작성하여 격차를 메웁니다. gcov 커버리지 개선(전 vs 후)으로 검증합니다.

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

사용 시기

중복 제거 (DEDUP)의 경우:

  • C++ 테스트 스위트가 유기적으로 성장하여 여러 파일이 동일한 부분을 커버하는 경우
  • *_gaps.cpp 갭 필러 테스트 파일이 *_comprehensive.cpp 포괄 테스트 파일과 함께 추가된 경우
  • 중복된 테스트 시나리오가 있는 코드베이스를 상속받은 경우
  • 커버리지를 잃지 않고 CI 테스트 시간을 줄이려는 경우

격차 메우기 (GAP-FILL)의 경우:

  • 해당하는 테스트 파일이 없는 컴파일된 .cpp 소스 파일이 있는 경우
  • 커버리지 리포트에 라인 커버리지가 90% 미만인 헤더가 있는 경우
  • 중복 테스트를 제거한 후 절약된 테스트 시간을 격차를 메우는 데 재투자하려는 경우
  • 릴리스 마감일 전에 커버리지를 높여야 하는 경우

다음과 같은 경우에는 사용하지 마십시오:

  • 테스트가 동일한 속성을 매개변수화/생성 테스트하는 경우 (이러한 중복은 의도적입니다)
  • 프로젝트에 커버리지 도구가 설정되지 않은 경우
  • 중복 여부를 먼저 확인하지 않고 테스트만 추가해야 하는 경우

작동 방식

0단계: 테스트 환경 파악

테스트 디렉토리와 CMakeLists.txt를 읽습니다:

test/source/*.cpp
test/CMakeLists.txt
include/<project>/*.hpp
source/*.cpp              # 컴파일된 소스

다음 사항을 확인합니다:

  • 패턴 쌍: *_gaps.cpp + *_comprehensive.cpp, *_basic.cpp + *_detailed.cpp
  • source/에 해당하는 test_*.cpp가 없는 컴파일된 .cpp 파일
  • 전용 테스트 파일이 없는 include/의 헤더
  • 테스트 프레임워크 (doctest, Catch2, Google Test 등) 및 CMake 글로빙

참고:

  • 테스트 프레임워크 및 사용된 매크로 (TEST_CASE, CHECK_EQ 등)
  • CMakeLists.txt가 file(GLOB ...) 또는 명시적 소스 목록을 사용하는지 여부
  • 커버리지 플래그 패턴 (일반적으로 -DENABLE_TEST_COVERAGE=ON)

1단계: 파일 수준에서 중복 감지

각 후보 파일에 대해, 테스트 스위트의 다른 모든 테스트 파일과 테스트 시나리오를 비교합니다.

구체적인 비교 방법:

  1. 두 파일을 나란히 읽습니다. 테스트 시나리오를 실행하는 코드별로 그룹화합니다:

    • 자유 함수 (gcd, lcm 등)
    • 클래스 메서드 (생성자, 연산자, 접근자, 변경자)
    • 템플릿 인스턴스화 및 특수화
    • 경계 사례 커버리지 (0, 무한대, NaN, 경계값)
  2. 값 수준의 중복을 확인합니다. 두 파일이 동일한 값으로 동일한 함수를 테스트하는 경우 (예: 두 파일 모두에서 gcd_recur(12, 8) = 4), 해당 시나리오는 중복입니다. 약간의 변형(한 파일에는 피보나치 수, 다른 파일에는 없음)은 중복이 아닙니다. 이는 고유한 입력입니다.

  3. 파일의 테스트 시나리오 중 90% 이상이 다른 파일에서 동일하거나 더 포괄적인 커버리지로 나타나는 경우 해당 파일을 "중복"으로 표시합니다. 나머지 10% 미만의 고유 시나리오(사소한 변형)는 안전하게 제거할 수 있습니다.

2단계: 기준 측정 (제거 전)

커버리지 계측으로 테스트 스위트를 빌드합니다:

도구구성
GCC + gcovcmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
Clang + llvm-covcmake -S test -B build/cov -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS="-fprofile-instr-generate -fcoverage-mapping" -G "Ninja"
MSVCOpenCppCoverage 또는 VS Enterprise의 내장 커버리지 사용

그런 다음 빌드하고 실행합니다:

cmake --build build/cov
./build/cov/<TestExecutable>

라이브러리 헤더에 대한 커버리지 생성

cd build/cov

# 각 라이브러리 헤더에 대해, 해당 헤더를 #include하는 테스트 오브젝트 파일에 대해 gcov 실행
gcov -o CMakeFiles/<Tests>.dir/source/<test_file>.cpp.o <path>/include/<project>/<header>.hpp

# 각 컴파일된 소스에 대해, 라이브러리 오브젝트 파일에 대해 gcov 실행
gcov -o _deps/<project>-build/CMakeFiles/<Project>.dir/source/<source>.cpp.o <path>/source/<source>.cpp

기준 측정항목 기록:

Headers:
  array_like.hpp:    100%  (28/28)
  bpqueue.hpp:       100%  (73/73)
  ...
Compiled sources:
  ell1d.cpp:           0%   (0/23)   ← 미테스트!
  ldlt_mgr.cpp:      100%  (10/10)
  ...

Test cases: N  |  Assertions: M  |  Status: SUCCESS

또한 각 파일의 정확히 커버되지 않은 라인(.gcov 출력의 #####: 라인)을 기록합니다.

3단계: 중복 파일 제거 (DEDUP 모드)

Remove-Item -Force test/source/test_*_gaps.cpp    # PowerShell
git status test/
git diff --stat

4단계: 재빌드 및 중복 제거 후 측정항목 수집

# 재구성 (CMake GLOB이 누락된 파일을 감지할 수 있음)
cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<TestExecutable>

커버리지를 기준과 비교합니다. 헤더의 커버리지가 떨어지거나 커버되지 않은 라인이 변경되면 즉시 되돌립니다. 해당 파일은 중복이 아니었습니다.

DEDUP에 대한 판정 기준:

확인 사항요구 사항
모든 테스트 통과반드시 통과 (전후 모두)
헤더별 라인 커버리지전후 정확히 동일
커버되지 않은 라인커버되지 않은 라인의 동일한 집합
테스트 수 감소예상됨 — 테스트 케이스 제거됨
어서션 수 감소예상됨 — 어서션 제거됨

5단계: 커버리지 격차 분석 (GAP-FILL 모드)

중복 제거 후 (또는 처음부터 시작하여) 커버리지 리포트에 격차가 있으면 테스트되지 않은 코드를 식별합니다:

격차 유형 A: 커버리지가 0%인 컴파일된 소스

source/*.cpp를 test/source/*.cpp와 비교합니다. source/의 .cpp 파일 중 일치하는 test_*.cpp가 없으면 테스트되지 않았을 가능성이 높습니다.

# 테스트 커버리지가 없는 소스 파일 찾기
Get-ChildItem source/*.cpp | ForEach-Object {
    $base = $_.Name -replace '\.cpp$'
    $test = "test/source/test_$base.cpp"
    if (-not (Test-Path $test)) { Write-Host "미테스트: $($_.Name)" }
}

격차 유형 B: 커버리지가 낮은 라이브러리 헤더

라인 커버리지가 90% 미만이거나 "실행 가능한 라인이 없음"인 헤더에 대한 gcov 리포트를 확인합니다. 템플릿이 많은 헤더는 커버리지를 .cpp 파일에 귀속시킬 수 있습니다. 둘 다 확인하십시오.

격차 유형 C: 커버된 파일 내의 함수/분기

부분적으로 커버된 파일 내의 커버되지 않은 라인(gcov에서 #####:)은 실행되지 않는 특정 분기를 나타냅니다. 이는 가장 가치가 높은 대상입니다.

6단계: 대상 테스트 작성

6a: 테스트되지 않은 소스 코드 읽기

소스 파일을 완전히 읽어 다음을 이해합니다:

  • 클래스/함수 API (생성자, 메서드, 매개변수)
  • 모든 조건 분기 (if/else, switch 케이스, 조기 반환)
  • 경계 사례 (null/0 입력, 경계값, 오류 조건)
  • 수학적 또는 알고리즘적 동작

6b: 모든 분기를 커버하는 테스트 케이스 설계

각 메서드에 대해 필요한 테스트 시나리오를 열거합니다:

메서드조건입력예상 동작
update(cut)beta > taug=1, beta=10 및 _r=5NoSoln 반환, 상태 변경 없음
update(cut)beta < -taug=1, beta=-10 및 _r=5NoEffect 반환, 상태 변경 없음
update(cut)|beta| <= tau, g>0g=1, beta=2 및 _r=5Success 반환, 새 중심 l+_r
update(cut)|beta| <= tau, g<0g=-1, beta=2 및 _r=5Success 반환, 새 중심 l+_r
update_central_cut(cut)g > 0g=1, beta=0_r 절반, 중심 왼쪽으로 이동
update_central_cut(cut)g < 0g=-1, beta=0_r 절반, 중심 오른쪽으로 이동

경계 사례 포함:

  • 너비가 0인 구간
  • 경계값 (beta == tau, beta == -tau)
  • 기울기가 0인 경우 (해당하는 경우)
  • 상태 전이를 확인하기 위한 반복/연쇄 연산

6c: 테스트 파일 작성

프로젝트의 기존 테스트 패턴에 따라 test/source/에 새 test_<module>.cpp를 작성합니다:

#define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
#include <doctest/doctest.h>

#include <ellalgo/<module>.hpp>   // 테스트 대상 헤더
#include <ellalgo/ell_config.hpp>  // 유형 정의에 필요한 경우
#include <utility>                 // std::pair에 필요한 경우

TEST_CASE("<Module>: construction") {
    auto x = <Module>(...);
    CHECK_EQ(x.some_method(), expected_value);
}

TEST_CASE("<Module>: method with success path") {
    auto x = <Module>(...);
    auto status = x.update(...);
    CHECK_EQ(status, CutStatus::Success);
    CHECK_EQ(x.state(), expected_state);
}

TEST_CASE("<Module>: method with failure path") {
    auto x = <Module>(...);
    auto status = x.update(...);  // beta > tau 분기 트리거
    CHECK_EQ(status, CutStatus::NoSoln);
}

TEST_CASE("<Module>: edge cases") {
    // 너비가 0인 구간, 경계 조건 등
}

TEST_CASE("<Module>: chained operations") {
    // 여러 업데이트를 순서대로
}

테스트 값 규칙:

  • 하드코딩된 예상 값 사용 (테스트 중인 동일한 코드에서 계산하지 않음)
  • 각 예상 값에 주석으로 수학적 유도 과정을 주석으로 표시 (예: // tau = |5*1| = 5, bound = 5 - 2/1 = 3, _r = half(3-0) = 1.5, _xc = 1.5)
  • 각 if 분기를 적어도 한 번 테스트
  • 각 switch 케이스를 적어도 한 번 테스트
  • 상태 지속성을 확인하기 위해 적어도 하나의 연쇄/순차 연산 테스트

7단계: 재빌드 및 갭 필 후 측정항목 수집

cmake -S test -B build/cov -DENABLE_TEST_COVERAGE=ON -DCMAKE_CXX_COMPILER=g++ -G "Unix Makefiles"
cmake --build build/cov
./build/cov/<TestExecutable>

# 커버리지 리포트 재생성
gcov -o CMakeFiles/<Tests>.dir/source/test_<module>.cpp.o <path>/include/<project>/<module>.hpp
gcov -o _deps/<project>-build/CMakeFiles/<Project>.dir/source/<module>.cpp.o <path>/source/<module>.cpp

8단계: 커버리지 개선 보고

| 측정항목 | 전 | 후 | Δ |
|--------|--------|-------|---|
| 테스트 케이스 | N | N' | +Δ |
| 어서션 | M | M' | +Δ |
| <module>.hpp | XX% | 100% | +Δ |
| <module>.cpp (컴파일됨) | 0% | 100% | +100% |
| 기타 모든 파일 | ≥90% | ≥90% | 0% |

커버리지 격차 메움: ✅

중요 규칙

규칙 1: 커버리지가 진실이며, 테스트 수가 아님

테스트 파일을 제거한 후 모든 라이브러리 헤더의 라인 커버리지가 변경되지 않는 경우에만 테스트 파일이 중복입니다. 커버리지가 떨어지면 파일이 고유한 코드 경로를 커버하고 있던 것입니다. 즉시 되돌리십시오.

테스트를 추가할 때, 새 테스트는 대상 파일의 커버리지를 증가시키는 경우에만 성공한 것입니다. 대상 파일의 커버리지가 증가하지 않으면 테스트가 코드를 실행하지 않는 것입니다. 테스트 로직을 다시 검토하십시오.

규칙 2: 백분율뿐만 아니라 커버되지 않은 라인 확인

두 번의 실행이 "98.06%"를 보여줄 수 있지만 커버되지 않은 라인이 다를 수 있습니다. 항상 .gcov 파일 출력에서 #####: 라인의 정확한 집합을 비교하십시오:

Get-Content build/cov/foo.hpp.gcov | Select-String -Pattern "^    #####:"

중복 제거 전후에 이 값이 동일해야 합니다. 갭 필의 경우 대상 파일의 #####: 라인이 감소해야 합니다.

규칙 3: 속성 기반 테스트를 중복 제거하지 마십시오

속성 기반 / 생성 테스트(예: RapidCheck)는 설계 상 의도적으로 중복됩니다. 무작위 입력으로 불변성을 검증합니다. 예제 기반 테스트와 겹치더라도 제거해서는 안 됩니다.

규칙 4: 값이 다른 테스트를 중복 제거하지 마십시오

동일한 연산자에 대해 다른 입력 값을 가진 두 테스트는 다른 코드 경로를 실행합니다. 입력 값과 예상 출력이 동일한 경우에만 제거하십시오.

새 테스트를 작성할 때는 기존 테스트와 다른 입력 값을 사용하십시오. 동일한 값을 복제하면 커버리지가 증가하지 않습니다.

규칙 5: 항상 깨끗한 상태에서 재빌드

.gcda 파일은 gcov에 의해 사용되며 소스 또는 바이너리 변경 후 오래됩니다. 측정하기 전에 항상 재빌드하고 다시 실행하십시오:

cmake --build build/cov --clean-first   # 또는 처음부터 재빌드
./build/cov/<TestExecutable>

규칙 6: CMake GLOB 동작 확인

CMakeLists.txt가 CONFIGURE_DEPENDS와 함께 file(GLOB ...)을 사용하는 경우, CMake는 재빌드 시 추가/제거된 파일을 자동으로 감지합니다(CMake 3.12+). 이전 CMake에서는 테스트 파일을 추가하거나 제거한 후 수동으로 재구성하십시오.

새 테스트 파일을 작성할 때 CMake가 이를 인식하는지 확인하십시오:

  • 빌드 출력에서 test_<module>.cpp를 찾으십시오
  • 없으면 재구성: cmake -S test -B build/cov

규칙 7: 예상 값을 하드코딩하고 재계산하지 마십시오

테스트를 작성할 때 테스트 중인 동일한 코드를 사용하여 예상 값을 계산하지 마십시오. 항상 수동으로 유도하고 리터럴로 하드코딩하십시오. 이렇게 하면 구현이 변경되었지만 양쪽에서 동일한 잘못된 코드를 사용하기 때문에 테스트가 여전히 통과하는 회귀를 잡을 수 있습니다.

복잡한 유도 과정에 주석을 추가하십시오:

// tau = |5*1| = 5, bound = 5 - 2/1 = 3
// g>0이므로 u=3, l=5-5=0
// _r = half_nonnegative(3-0) = 1.5, _xc = 0 + 1.5 = 1.5
CHECK_EQ(e.xc(), 1.5);

규칙 8: 모든 분기 테스트

조건부 로직이 있는 각 함수에 대해 모든 분기를 열거하고 분기당 적어도 하나의 테스트를 작성하십시오:

분기테스트 시나리오
if (beta > tau)해 없음 경로
if (beta < -tau)효과 없음 경로
else (성공)성공 경로
g > 0 (성공 내부)양의 기울기
g < 0 (성공 내부)음의 기울기
경계 beta == tau여전히 성공 (≤)
경계 beta == -tau< vs ≤에 따라 다름

규칙 9: 헤더보다 컴파일된 소스 우선시

헤더 전용 템플릿 코드는 다른 테스트에 의해 우연히 실행되는 경우가 많기 때문에 컴파일된 .cpp 파일이 커버리지가 0%일 가능성이 더 높습니다. 다음 순서로 테스트 우선순위를 지정하십시오:

  1. 일치하는 test_*.cpp가 없는 source/*.cpp → 최우선 순위
  2. 라인 커버리지가 <90%인 헤더 → 중간 우선 순위
  3. 그 외에는 커버된 파일 내의 커버되지 않은 분기 → 낮은 우선 순위

규칙 10: 프로젝트 테스트 규칙 준수

테스트를 작성하기 전에 2-3개의 기존 테스트 파일을 연구하여 일치시킵니다:

  • …

[번역을 위해 잘림]

Individual skills in this repo

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

관련 스킬