E2E Automation 슬랙 채널에 ✅ All Pass 알림이 왔다
리포트를 열어보니 Total Test Case 개수가 평소와 달랐다
실행되지 않은 케이스가 있었다
원인: Allure 결과 디렉터리 카운트 기반 집계
결과 집계 스크립트는 Allure 결과 파일 개수로 passed/failed를 덮어쓰는 구조였다
# 의도된 동작
passed = count_allure_results(status="passed")
failed = count_allure_results(status="failed")
total = passed + failed
pytest watchdog이 hang된 pytest를 SIGTERM으로 죽이는 순간, 진행 중·미실행 케이스는 Allure 결과 파일을 남기지 않는다
집계에서 빠진다
passed 100건이 통과 후 watchdog이 죽었다면 total=100, passed=100, failed=0으로 집계된다
"실행되었어야 할 N건"은 어디에도 없다
진행 중인 테스트를 추정이 아닌 사실로 기록
기존엔 watchdog이 stdout이나 log 파일을 휴리스틱으로 파싱해 "어떤 테스트가 hung 상태였는지" 추정했다
부정확하다
stdout은 PASSED/FAILED만 찍히고, log 파일은 마지막 줄이 Page Object의 INFO 로그라 테스트 이름이 안 나오는 경우가 많다
conftest.py에 hook을 추가해 매 테스트 진입/종료 시 파일에 직접 기록한다
import os
from pathlib import Path
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_setup(item):
phase = os.environ.get("WATCHDOG_PHASE_NAME")
if not phase:
return
Path(f"reports/.watchdog_kills/.current_test_{phase}.txt").write_text(item.name)
@pytest.hookimpl(trylast=True)
def pytest_runtest_teardown(item):
phase = os.environ.get("WATCHDOG_PHASE_NAME")
if not phase:
return
flag = Path(f"reports/.watchdog_kills/.current_test_{phase}.txt")
if flag.exists():
flag.unlink()
WATCHDOG_PHASE_NAME이 export된 phase에만 동작한다
이제 watchdog이 어떤 테스트 도중에 죽였는지 추정이 아니라 사실로 안다
kill 이벤트를 진단 JSON으로 dump
watchdog이 SIGTERM을 보내기 직전에 진단 정보를 기록한다
diag = {
"phase": phase,
"reason": reason, # "log_stall" or "absolute_timeout"
"pid": target_pid,
"in_progress_test": read_current_test(phase),
"in_progress_source": source, # "hook"=fact, "heuristic"=guess
"last_log_lines": tail_log_lines(200),
"timestamp": datetime.now().isoformat(),
}
Path("reports/watchdog_diagnostic.json").write_text(json.dumps(diag))
Path(f"reports/.watchdog_kills/{phase}.flag").touch()
in_progress_source 필드로 사실(hook)/추정(heuristic)을 구분한다
이후 진단 리포트 HTML에서 배지로 시각화한다 (✅ hook / 🔍 heuristic)
{phase}.flag 파일은 집계 단계에서 "이 phase에서 watchdog kill이 있었음"을 알리는 시그널이다
집계 단계에서 run_aborted 플래그로 분기
결과 집계 스크립트가 flag 파일을 먼저 읽고, 있으면 missing 카운트를 계산한다
def collect_results():
flag_files = list(Path("reports/.watchdog_kills/").glob("*.flag"))
if flag_files:
aborted_phase = parse_phase(flag_files[0])
expected = get_expected_test_count(aborted_phase)
completed = count_allure_results(phase=aborted_phase)
missing = expected - completed
run_aborted = True
else:
missing = 0
run_aborted = False
return {
"passed": count_allure_results(status="passed"),
"failed": count_allure_results(status="failed"),
"missing": missing,
"run_aborted": run_aborted,
}
여기서 한 가지 함정이 있다
다른 phase의 stale flag를 흡수하지 않도록 timestamp staleness 가드를 추가했다
def load_run_abort_info():
diag = json.load(Path("reports/watchdog_diagnostic.json").open())
age_seconds = time.time() - parse_iso(diag["timestamp"]).timestamp()
# 5분 이상 된 진단 정보는 무시 (이전 실행의 잔재)
if age_seconds > 300:
return None
# 현재 실행 시작 시각 이후인지 추가 확인
run_start = int(os.environ.get("SLACK_RUN_START_EPOCH", "0"))
if parse_iso(diag["timestamp"]).timestamp() < run_start:
return None
return diag
smoke 실행이 이전 prod 실행의 stale 진단 정보를 흡수하던 회귀를 막는다
알림 메시지를 incomplete 분기로 전환
if run_info["run_aborted"]:
title = f"🚨 Run Incomplete (미실행 {run_info['missing']}건)"
body += format_watchdog_section(diag)
else:
title = f"✅ All Pass {passed}/{failed}/{total}"
format_watchdog_section은 phase, reason, in-progress test, 마지막 로그 200줄을 슬랙 블록으로 포맷한다
이제 슬랙 알림이 거짓말을 하지 않는다
정리
| 항목 | Before | After |
|---|---|---|
| 진행 중 테스트 식별 | stdout/log heuristic 파싱 | conftest hook으로 파일 기록 |
| watchdog kill 시 집계 | Allure 카운트로 덮어씀 | run_aborted 플래그 + missing 산출 |
| 알림 제목 | 거짓 All Pass | Run Incomplete (미실행 N건) |
| 진단 정보 | 없음 | phase / reason / in-progress test / 마지막 로그 |
Allure는 "통과한 것"만 안다
"실행되었어야 했지만 안 된 것"은 다른 출처에서 와야 한다
자동화 알림 시스템 자체도 검증 대상이다
'TIL > Claude Code' 카테고리의 다른 글
| [TIL] Claude Code 커스텀 스킬로 반복 작업 자동화하기 (0) | 2026.05.31 |
|---|---|
| [TIL][Playwright] networkidle이 React 앱에서 거짓말하는 이유 — MutationObserver로 DOM 안정화 대기 내장 (0) | 2026.04.20 |
| [TIL][E2E] UI 선택 없이 언어를 전환하는 방법 — 쿠키 + URL 주입 (4) | 2026.04.18 |
| [TIL][Shell] pytest 좀비 프로세스를 잡는 Watchdog 스크립트 (3) | 2026.04.16 |
| [TIL][Playwright] pages/ 전체 210개 wait_for_timeout을 0개로 제거한 4단계 과정 (0) | 2026.04.14 |