동결에는 증인이 필요하다: 간헐적인 테스트를 격리하기 전에 시드-매트릭스 검토가 필요합니다
요약
CI 환경에서 발생하는 간헐적 테스트 실패(flakiness)는 원인 파악을 어렵게 합니다. 이 글은 '시드-매트릭스' 검토 방법론을 제시하며, 명명된 시드를 사용하여 속성 검사의 결정론적 증거를 확보하는 방법을 설명합니다. 이를 통해 실제 회귀와 노이즈를 정확히 분리하여 테스트 트리아지(triage)의 신뢰도를 높일 수 있습니다.
핵심 포인트
- 간헐적 실패는 불변성 파괴, 비결정성 속성 검사 등 복합적인 원인이 있다.
- 명시적인 시드(named seed)를 사용하여 테스트 실행을 결정론적으로 만듭니다.
- 기준 커밋과 패치된 버전을 동일한 시드로 반복 실행하여 비교합니다.
- JSON 원장(ledger)은 시드별 실패 횟수를 기록하며 노이즈와 회귀를 분리합니다.
에이전트 패치 후 CI에서 빨간 줄이 나타나는 것은 단 하나의 사건이 아닙니다. 적어도 세 가지입니다. 첫째, 패치가 실제 불변성(invariant)을 깨뜨렸거나, 둘째, 속성 검사(property check) 자체가 비결정적(non-deterministic)이거나, 또는 셋째, 테스트가 이미 기준 커밋(baseline commit)에서 실패하고 있었던 경우입니다. 테스트를 동결하면 이 세 가지 모두 로그상에서 동일하게 보이게 되며, 가장 포착해야 할 실제 회귀(regression)는 격리 라벨 뒤로 사라집니다.
제가 만든 규칙은 다음과 같습니다: 어떤 테스트도 기준 커밋에서 증인(witness)을 확보하고, 명명된 시드(named seed)로 재현 가능할 때까지 동결되지 않습니다. 이 글에 담긴 것은 바로 그 증인을 생성하는 데 제가 사용하는 장치(harness)와 의사 결정표입니다. 지극히 평범합니다: 표준 라이브러리만 사용하며, 프레임워크는 없고, 트리당 하나의 JSON 원장(ledger)을 만듭니다.
이 방법론을 강요한 실패 사례
어떤 에이전트 패치가 수동으로 작성된 중복 제거 루프를 set으로 대체했습니다. 디프(diff)는 마치 정리 작업처럼 보였습니다. 순서 보존 테스트가 빨간 줄로 표시되었고, 누군가는 이 테스트가 '이전에 아무 이유 없이 빨간 줄을 띄었기 때문에' 간헐적이라고 라벨링했고, 그 결과 두 주 동안 격리되었습니다.
하지만 이 테스트는 간헐적이 아니었습니다. 오히려 이 스위트(suite)에서 순서 보장(ordering guarantee)이 사라졌다는 것을 감지한 유일한 것이었습니다. 다른 것은 아무것도 실패하지 않았는데, 그것들은 순서를 주장하는 검증 자체가 없었기 때문입니다.
만약 제가 그 속성을 몇 개의 시드 입력값에 걸쳐 실행하고 두 트리 모두의 결과를 저장했다면, 이 분류 작업은 단 하나의 명령으로 끝났을 것입니다. 여기 그 명령어입니다.
1단계 — 시드를 명시적인 입력값으로 만들기
첫 번째 실수는 러너(runner)가 입력값을 임의로 선택하도록 내버려 두는 것입니다. 암묵적인 무작위성을 가진 속성 검사는 빨간 줄만 보여줄 뿐, 그 원인을 알려주지 않습니다. 시드(Seeds)는 실패를 인용할 수 있는 어떤 것으로 만듭니다.
# seed_matrix.py — stdlib only
import argparse, importlib, json, random, sys
from pathlib import Path
...
동일한 속성(property)과 동일한 시드 개수로 각 트리에 대해 한 번 실행합니다:
python seed_matrix.py --impl ./trees/baseline::dedupe --prop seed_matrix::order_preserved \
--seeds 200 --replays 5 --out baseline.json
python seed_matrix.py --impl ./trees/patched::dedupe --prop seed_matrix::order_preserved \
...
한 오후 시간을 낭비하게 만든 하나의 주의사항: 재실행(replays)은 구현체가 프로세스 레벨 상태를 가지지 않는 경우에만 독립적입니다. 만약 메모이제이션(memoizes)하거나 캐싱(caches)한다면, --replays 5는 같은 답변을 다섯 번 읽는 것일 뿐입니다. 각 재실행은 서브프로세스에서 실행하거나, 실행 사이에 모듈을 초기화해야 합니다.
2단계 — 의사결정표를 이용한 트리아지(Triage)
원장(ledger)은 시드당 재실패(fails) 수를 기록합니다. 이 단 하나의 숫자가 통계적 장치 없이 결정론(determinism)과 노이즈를 분리해 줍니다.
| baseline seed | patched seed | reading | action |
|---|---|---|---|
| 0/5 | 5/5 | deterministic counterexample, patch-only | merge 차단; 시드 명시 |
| ... |
마지막 행은 첫 번째 행만큼 중요합니다. 실패할 수 없는 속성은 테스트가 아니라, 실행 시간 비용이 드는 주석(comment)입니다.
# triage.py — 두 개의 원장 입력, 하나의 판결 출력
import json, sys
from pathlib import Path
...
순서가 중요합니다. PATCH_BUG 브랜치는 플래키(flaky) 브랜치보다 먼저 실행되어야 합니다. 왜냐하면 베이스라인에서 통과하는 트리에 대한 결정론적 실패는 나머지 매트릭스가 아무리 노이즈가 많아 보여도 카운터예제이기 때문입니다.
3단계 — 동결(Freeze) 계약
트리아지(triage)가 PRE_EXISTING 또는 FLAKY_ORACLE이라고 판단하면, 격리 항목은 합법적이지만 증인(witness)이 첨부되어야만 합니다. 만료 메커니즘은 별도의 논의 사항이며, 여기서의 관문은 '입장'입니다.
{
"test": "tests/test_dedupe.py::test_order_preserved",
"witness_seed": 137,
...
}
witness_seed 또는 baseline_sha를 누락한 격리 항목은 동결(freeze)이 아니라 추가 단계를 거치는 삭제입니다. CI에서는 두 필드 중 하나라도 누락된 항목을 거부해야 합니다. 원장 파일은 항목 옆에 보관하세요. JSON이 증거이고, 항목 자체가 포인터입니다.
원격 무료 박스가 실제로 도움이 되는 경우
매트릭스는 배치 작업의 정의입니다: 200개의 시드(seeds) × 5번의 리플레이(replays) × 3개의 트리(trees)는 상호작용 지연 시간 요구 사항이 없는 독립적인 속성 평가 3,000개를 의미합니다. 이러한 형태는 무료 서버 인스턴스에 적합하며, MonkeyCode의 운영자는 무료 모델 접근 및 무료 서버 옵션이 제공된다고 명시하고 있습니다. 저는 할당량(quotas), 하드웨어 또는 실행 시간을 측정하지 않았으므로, 이를 귀하의 워크로드에 대해 미검증된 것으로 간주하십시오.
공개 고지: 이 기사는 MonkeyCode의 제품 홍보 활동의 일환으로 작성되었습니다.
유효한 두 가지 사용 사례:
- 변경 사항(diff)에서 속성 후보를 얻기. 모델에게 변경된 함수가 보존해야 하는 불변량(invariants)을 제안하도록 요청한 다음, 그중 대부분을 삭제합니다. 후보는 해당 속성이 거부해야 하는 알려진 잘못된 입력인 부정적인 제어 입력(negative control)을 작성한 후에야 매트릭스에 자리를 얻습니다. 만약 속성이 자신의 부정적인 제어 입력을 수용한다면, 어떤 것도 잡아낼 수 없습니다.
- 노트북에서 매트릭스를 실행하기. 기준선(baseline)과 패치된 원장(patched ledgers)은 동일한 환경이 필요합니다. 표준 컨테이너를 사용하면 이를 얻을 수 있으며, 3,000개의 평가가 타이핑하는 장치와 분리됩니다.
단계 1~3 중 어느 것도 둘 중 하나(환경 또는 서버)를 요구하지 않습니다. 안정적인 시드(seeds)를 가진 러너(runner)가 이미 있다면, 제품 사용을 완전히 건너뛰고 표만 유지하십시오.
한계점 및 이 작업을 수행해서는 안 되는 사람
- 시드는 커버리지를 의미하지 않습니다. 12개 요소의 생성기(generator)에 대한 200개의 시드만으로는 아주 작은 부분만을 탐색할 뿐입니다. 여기서 구속 제약 조건은 시드 개수가 아니라 생성기 자체입니다. 만약 생성기가 작은 리스트만 생성한다면, 매트릭스는 작은 리스트의 정확성만 증명하고 그 외에는 아무것도 증명하지 못합니다.
- 런타임을 배수적으로 증가시킵니다. N개의 시드 × K개의 재실행(replays)은 속성당 N×K번의 평가를 의미합니다. 이미 20분이 걸리는 스위트에서, 이는 모든 테스트에 대한 정책이라기보다는 의심스러운 단일 속성에 초점을 맞춘 도구입니다.
- 벽시계 시간이나 네트워크 종속성을 해결할 수 없습니다. 실패가 실제 시계(real clock), 실제 소켓(real socket) 또는 공유 데이터베이스에 의존하는 경우, 시드 제어는 단순히 간헐적 실패를 잘 문서화할 뿐입니다. 이 경우에는 픽스처(fixtures)가 필요합니다.
- 상태 저장 코드에는 서브프로세스 격리가 필요합니다. 위에서 언급했듯이, 캐싱이 있는 어떤 것에 대해서도 프로세스 내 재실행은 독립성을 속입니다.
- 작고 빠르며 결정론적인 스위트에서는 건너뜁니다. 만약 어떤 속성이 완전히 열거된 입력 공간(fully enumerated input space)에서 밀리초 단위로 실행된다면, 이 표는 오버헤드일 뿐입니다.
- 규제되거나 SLA에 묶인 파이프라인을 무료 공유 인프라에서 실행하지 마십시오. 로컬 또는 사용자가 제어하는 인프라에서 실행하십시오.
Hypothesis를 stdlib 하네스 대신 사용하는 경우, 동등한 기능을 하는 레버(levers)들이 존재합니다. 바로 derandomize 설정과 pytest 플러그인의 시드 플래그입니다. 하지만 버전별로 플래그 이름과 동작 방식이 변경되었으므로, 사후 분석(postmortem)에서 플래그를 인용하기 전에 설치된 버전의 문서를 확인하십시오.
얻어 가야 할 점 (What to take away)
간헐적 실패 라벨은 과거에 대한 주장이며, 과거에 대한 주장은 증인이 필요합니다. 기준선 트리(baseline tree)에서 5번 중 5번 실패하는 시드 하나를 생성하십시오. 그것이 무엇인지 이름 붙일 수 있을 때까지는, 당신은 간헐적 테스트를 보고 있는 것이 아닙니다. 당신은 검토되지 않은 빨간 선을 보고 있는 것이며, 가장 저렴한 설명(cheapest explanation)이 보통 진실입니다: 패치가 동작 방식을 변경했고 단지 하나의 테스트만이 그것을 알아차렸다는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기