파일이 아닌 코드 블록(Hunks) 단위의 속성 커버리지: 에이전트 패치용 소유권 지도
요약
이 아티클은 코드 변경(hunk) 단위로 테스트 커버리지를 분석하는 방법을 제시합니다. 단순히 파일 전체의 커버리지 비율을 보는 것이 아니라, 특정 코드가 어떤 테스트에 의해 보호받는지 '소유권 지도'를 구축하여 패치의 안정성을 깊이 있게 검증할 수 있습니다.
핵심 포인트
- Hunk 단위로 소유권을 파악하여 코드 변경의 실제 위험도를 측정합니다.
- 테스트 커버리지 컨텍스트를 수집하는 것이 핵심이며, `pytest-cov`와 `coverage.py` 사용법을 알아야 합니다.
- OWNED(소유됨), INCIDENTAL(우발적), ORPHAN(고아)으로 hunk의 상태를 분류할 수 있습니다.
패치는 변경된 라인에서 100% 커버리지를 보여도 여전히 테스트되지 않은 상태일 수 있습니다. 중요한 것은 얼마나 많은 변경된 라인이 실행되었는지가 아니라, 만약 변경 사항이 잘못되었다면 어떤 테스트가 실패할지—그리고 그 테스트가 패치에 의해 추가되었는지 아니면 저장소에서 상속받았는지입니다.
본 게시물은 코드 블록(hunk) 소유권 지도를 구축합니다: 테스트별 커버리지 컨텍스트를 diff hunk에 매핑하고, 이를 OWNED (소유됨), INCIDENTAL (우발적), 또는 ORPHAN (고아)으로 분류하며, 픽스처 도달 감사와 안정성 전제 조건을 포함합니다. 이 아티팩트는 사용자의 저장소에서 실행하는 스크립트입니다.
한 가지 예시에서의 실패 모드
에이전트가 재시도(retry) 래퍼와 백오프 제한을 추가합니다. 또한 오래된 스모크 테스트가 우연히 사용하는 헬퍼 함수도 건드립니다. diff의 라인 커버리지는 매우 좋아 보입니다.
실제 발생한 일은 다음과 같습니다: 4148 라인을 건드리는 유일한 테스트는 2년 전에 작성되었으며 재시도에 대해 아무것도 단언하지 않는 55 라인에는 커버하는 테스트가 전혀 없습니다. 오직 60~63 라인만이 패치 자체가 도입한 테스트에 의해 보호받고 있습니다.tests/test_client.py::test_get_ok뿐이었습니다. 52
| Hunk | Instrumented lines | Owner | Would fail on wrong behavior? |
|---|---|---|---|
src/retry.py:41-48 | executed | pre-existing smoke test | no |
| ... | |||
| 파일 수준의 커버리지는 이 세 가지를 하나의 백분율 뒤에 숨깁니다. |
1단계 — 테스트별 커버리지 컨텍스트 수집
coverage.py는 측정당 _context_를 기록하며, pytest-cov는 이 컨텍스트를 실행 중인 테스트 ID로 설정할 수 있습니다. 이것이 핵심 트릭입니다. 컨텍스트가 없으면 파일 수준의 진실만을 얻게 됩니다.
# .coveragerc (또는 pyproject.toml의 [tool.coverage.run])
# [run]
# relative_files = True
...
relative_files = True가 중요합니다: JSON 키는 git diff 경로와 일치하도록 저장소 상대(repo-relative)여야 합니다. 만약 pytest-xdist를 사용한다면, 워커별 데이터 파일을 예상해야 합니다. 컨텍스트가 비어 보인다면 coverage combine를 먼저 실행한 후 coverage json을 실행하세요. 컨텍스트 지원과 --show-contexts JSON 옵션은 버전 하한선이 있으므로, 실제로 실행하는 coverage.py 및 pytest-cov 버전을 모두 확인해야 합니다.
2단계 — 코드 블록(Hunks)을 라인에 매핑하기
diff를 컨텍스트 라인 없이 파싱합니다. 가독성이 아닌 추가된 라인 범위가 필요합니다.
git diff -U0 origin/main...HEAD
-U0은 @@ -a,b +c,d @@ 헤더를 생성하며, 여기서 c,d는 새 파일에 정확히 추가된 블록입니다. 삭제만 포함된 hunk는 +c,0으로 나타나며 건너뛰어야 합니다. 이는 소유할 라인이 없기 때문입니다.
3단계 — 모든 hunk 분류하기
아래 스크립트는 두 입력을 결합합니다. 이를 로컬 게이트로 실행하세요. 시작점일 뿐이며, 패키징된 도구는 아니므로 그 동작은 사용자의 coverage.py 버전에 따라 달라집니다.
#!/usr/bin/env python3
"""hunk_ownership.py — 패치 hunk를 실행한 테스트 컨텍스트에 매핑합니다."""
import json, re, subprocess, sys
...
예시 보고서 형태 (실제 저장소의 측정값이 아닌 합성 값):
INCIDENTAL src/retry.py:41-48 owners=tests/test_client.py::test_get_ok
ORPHAN src/retry.py:52-55 owners=-
OWNED src/retry.py:60-63 owners=tests/test_retry.py::test_backoff_caps
4단계 — OWNED를 신뢰하기 전에 fixture 도달 가능성 감사하기
많은 INCIDENTAL 판결은 거의 모든 테스트가 거의 모든 코드 경로를 실행하게 만드는 공유 fixture에서 발생합니다. 각 fixture에 의존하는 테스트 수를 계산한 다음, 그 목록을 패치가 건드린 fixture와 교차 분석하세요.
import re, subprocess
from collections import defaultdict
...
--fixtures-per-test 레이아웃은 pytest 릴리스에 걸쳐 변경되었으므로, 이를 기반으로 게이팅을 하기 전에 사용자의 버전에 맞춰 파싱을 검증하세요. 테스트 스위트의 대부분에서 도달 가능하고 패치에 의해 수정된 fixture는 해당 패치가 프로덕션 동작뿐만 아니라 테스트 인프라를 변경했다는 강력한 신호입니다. 이는 테스트 변경으로 간주하여 두 번 읽어보세요.
5단계 — 안정성을 사후 고려가 아닌 전제 조건으로 만들기
소유권은 커버하는 테스트가 매번 동일한 판결을 반환할 때만 의미가 있습니다. OWNED hunk를 승인하기 전에, 해당 hunk를 커버하는 파일들로 고정된 셔플 시드(shuffle seed)를 사용하여 반복적으로 실행해 보세요.
# requires pytest-repeat와 pytest-randomly; 버전별 플래그를 확인하세요
pytest -q tests/test_retry.py --count=5 --randomly-seed=1729
결과가 뒤집히면, 해당 테스트는 소유자 세트(owner set)에서 제거하고, 커버했던 모든 덩어리(hunk)를 수정될 때까지 ORPHAN으로 간주합니다. 가끔 통과하는 테스트는 증거가 아닙니다. 그것은 병합 버튼을 가진 노이즈입니다.
단계 6 — 고정된 표로 결정하기
| Hunk 클래스 | 변경된 라인에 대한 커버리지 | 소유자 | 게이트 액션 |
|---|---|---|---|
| OWNED | 실행됨 | 이 패치에 의해 테스트가 추가/변경됨 | 승인(accept), PR에 소유자 ID 기록 |
| ... | |||
| INCIDENTAL은 전체 작업을 수행하는 클래스입니다. 녹색 CI 실행과 자신감 있는 검토가 충돌하는 곳이 바로 이곳입니다. 그러한 덩어리(hunks)의 경우, 이 시리즈에서 앞서 다룬 게이트 설계의 차분 복원 기법(differential revert technique)을 사용하여 덩어리별 행동 프로브를 실행하고, 기록된 검토자(reviewer)가 있을 때만 해당 덩어리를 WAIVED로 승격시키세요. |
CI 분 단위 비용이 들지 않는 곳에서 매트릭스 실행하기
테스트당 컨텍스트는 커버리지 데이터의 크기와 런타임을 증가시키고, 다섯 배의 안정성 패스는 이를 다시 증폭시킵니다. 이는 측정되는 CI 계획에 부담을 주기에는 어색한 부하입니다.
공개: 이 글은 MonkeyCode의 제품 홍보의 일환으로 작성되었습니다. MonkeyCode는 운영자별로 무료 모델 액세스와 무료 서버 옵션을 제공합니다. 저는 할당량, 하드웨어 또는 기간을 확인하지 않았으므로, 둘 중 어느 것에 의존하기 전에 현재 계획 페이지를 읽어보셔야 합니다. 이 워크플로우에는 솔직히 두 가지 사용이 적합합니다. 첫째, 커버리지-컨텍스트 매트릭스와 안정성 반복 테스트를 무료 서버에서 실행하여, 비용이 많이 드는 수집 작업이 유료 러너(paid runners)에 남아있지 않도록 합니다. 둘째, coverage.json과 소유권 보고서를 무료 모델에 입력하여 PR 댓글 초안을 작성하고 ORPHAN 덩어리를 위한 후보 속성 테스트를 제안하게 한 다음 — 그 제안들을 직접 검토하세요. 왜냐하면 잘못된 패치의 관찰된 행동을 단언하는 생성된 테스트는 테스트가 없는 것보다 더 나쁘기 때문입니다.
제한 사항 및 건너뛰어야 할 사람들
• 작은 테스트 스위트. 약 100개의 테스트만으로는 diff를 읽는 것이 맵을 만드는 것보다 빠릅니다. 도구 오버헤드가 절감 효과를 초과합니다.
• 생성 및 컴파일된 코드. 라인별 커버리지는 트랜스파일링 출력, 매크로, 템플릿 인스턴스화 전반에 걸쳐 부적절한 대리 지표입니다. 소스코드의 hunk 경계가 실행 가능한 경계와 일치하는 경우는 드뭅니다.
• 컨텍스트 지원이 보편적이지 않습니다. coverage.py 컨텍스트가 이 분야에서 성숙한 방법론입니다. 만약 사용 언어의 커버리지 도구가 테스트당 컨텍스트 개념을 가지고 있지 않다면, 이 방법은 파일 수준 추측으로 저하됩니다.
• 이미 PR(Pull Request) 단위 뮤테이션 테스트를 실행하는 팀. 뮤테이션은 직접적으로 더 강력한 소유권 신호를 제공합니다. 여기에 추가하는 것은 적은 새로운 정보에 대해 비용을 중복하게 만듭니다.
• 병렬 수집. pytest-xdist의 경우, 컨텍스트 데이터는 분석 전에 결합되어야 하며, 워커별 컨텍스트 이름이 다를 수 있습니다. 이를 위한 예산을 책정하거나 무료 서버에서 순차적으로 실행해야 합니다.
주간 루프
git diff -U0을 사용하여 패치 diff를 생성합니다.pytest --cov-context=test를 실행하고coverage.json --show-contexts를 발행합니다.- 모든 hunk를 분류하고, ORPHAN 및 INCIDENTAL인 경우 게이트를 실패시킵니다.
- 패치가 건드린 모든 fixture에 대해 fixture 도달 여부를 감사(Audit)합니다.
- 고정된 시드를 사용하여 커버리지 파일들을 다섯 번 재실행하여 불안정한 소유권을 격하시킵니다.
- 다음 독자가 백분율이 아닌 증거를 볼 수 있도록, owner ID와 면제 사항을 PR 본문에 기록합니다.
커버리지는 라인이 실행되었는지를 알려줍니다. 소유권은 테스트가 불만을 제기했을 것이라는 것을 알려줍니다. 이 게시물에서 하나의 변경 사항만 채택한다면, ORPHAN에 대한 종료 코드로 만드십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기