Files
hackerthon-vote/tests/e2e.py
th-kim0823 6e517be918 refactor: DB 제거 → 단일 hackathon.json (JSON only)
DB(sqlite + WAL) 제거. 모든 state를 단일 JSON 파일로 통합.
일회용/내부용이라 유지보수성/확장성보다 단순성 우선.

변경:
- app.py: sqlite3 import 제거. load_data/save_data + threading.RLock + atomic write
  - votes: list of dict
  - titles, tie_breaks, settings: dict
  - people: roster (assign_teams가 채움)
  - 누락 키 자동 보강
- assign_teams.py: hackathon.json 단일 출력. 기존 votes/titles 보존
- Dockerfile/compose: votes.db volume 제거. hackathon.json read-write mount
- tests/e2e.py: 12개 (12/12 통과). load/save/insert_vote/clear_votes/atomic 추가
- README: 새 데이터 구조 문서화
- roster.json/participants.json 제거 (hackathon.json으로 통합)

호스트 편집 워크플로:
- jq/vi로 hackathon.json 직접 편집
- 앱 매 요청 reload — 컨테이너 재시작 불필요

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:25:46 +09:00

220 lines
6.3 KiB
Python

"""
E2E 테스트 — JSON 단일 파일 기반.
실행: docker exec hackathon-vote python3 /tmp/e2e.py
"""
import os
import sys
import tempfile
import shutil
# 격리 데이터 파일 (실제 hackathon.json과 분리)
TEST_DATA = tempfile.mktemp(suffix=".json")
shutil.copy("/app/hackathon.json", TEST_DATA)
os.environ["DATA_PATH"] = TEST_DATA
os.environ["ADMIN_TOKEN"] = "test"
sys.path.insert(0, "/app")
from app import ( # noqa: E402
load_data,
save_data,
insert_vote,
list_votes,
clear_votes,
get_participants,
get_teams,
compute_winners,
save_tie_break,
clear_tie_break,
set_voting_open,
is_voting_open,
set_title,
get_titles,
archive_results,
)
def reset_votes():
"""투표만 초기화 (people, settings 보존)."""
data = load_data()
data["votes"] = []
data["tie_breaks"] = {}
data["titles"] = {}
data["settings"]["voting_open"] = True
save_data(data)
def add_vote(name, emp, team, fun, polish, utility):
insert_vote(name, emp, team, {
"fun_team": fun,
"polish_team": polish,
"utility_team": utility,
})
PASS = ""
FAIL = ""
results = []
def test(name, fn):
try:
fn()
results.append((PASS, name))
print(f"{PASS} {name}")
except AssertionError as e:
results.append((FAIL, f"{name}: {e}"))
print(f"{FAIL} {name}: {e}")
except Exception as e:
results.append((FAIL, f"{name}: UNEXPECTED {type(e).__name__}: {e}"))
print(f"{FAIL} {name}: UNEXPECTED {type(e).__name__}: {e}")
def t_load():
parts = get_participants()
teams = get_teams()
assert len(parts) == 34, f"기대 34명, 실제 {len(parts)}"
assert len(teams) == 7
assert "한지승" in parts
assert "김태현" not in parts
def t_voting_toggle():
reset_votes()
assert is_voting_open() is True
set_voting_open(False)
assert is_voting_open() is False
set_voting_open(True)
assert is_voting_open() is True
def t_simple_winners():
reset_votes()
for i in range(5):
add_vote(f"V{i}", f"E{i}", "팀7", "팀1", "팀2", "팀3")
winners, _ = compute_winners()
assert winners["fun_team"]["status"] == "ok"
assert winners["fun_team"]["team"] == "팀1"
assert winners["polish_team"]["team"] == "팀2"
assert winners["utility_team"]["team"] == "팀3"
def t_priority_one_team_all_first():
reset_votes()
for i in range(5):
add_vote(f"V{i}", f"E{i}", "팀7", "팀1", "팀1", "팀1")
for i in range(5, 8):
add_vote(f"V{i}", f"E{i}", "팀7", "팀2", "팀2", "팀1")
for i in range(8, 10):
add_vote(f"V{i}", f"E{i}", "팀7", "팀3", "팀3", "팀2")
winners, _ = compute_winners()
assert winners["utility_team"]["team"] == "팀1"
assert winners["polish_team"]["team"] == "팀2"
assert winners["fun_team"]["team"] == "팀3"
def t_tie_pending():
reset_votes()
for i in range(3):
add_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀1")
for i in range(3, 6):
add_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀2")
winners, _ = compute_winners()
assert winners["utility_team"]["status"] == "tie"
assert set(winners["utility_team"]["tied"]) == {"팀1", "팀2"}
def t_tie_break_random():
reset_votes()
for i in range(3):
add_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀1")
for i in range(3, 6):
add_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀2")
save_tie_break("utility_team", "팀1", "random")
winners, _ = compute_winners()
assert winners["utility_team"]["status"] == "ok"
assert winners["utility_team"]["team"] == "팀1"
assert winners["utility_team"]["method"] == "random"
clear_tie_break("utility_team")
def t_unique_voter():
reset_votes()
add_vote("홍길동", "E1", "팀1", "팀2", "팀3", "팀4")
try:
add_vote("홍길동", "E2", "팀1", "팀5", "팀6", "팀7")
raise AssertionError("중복 INSERT 통과")
except ValueError as e:
assert str(e) == "DUPLICATE_VOTER"
def t_titles_persist():
reset_votes()
set_title("팀1", "Slack 자동 분류기")
titles = get_titles()
assert titles["팀1"] == "Slack 자동 분류기"
def t_archive_writes_file():
reset_votes()
for i in range(5):
add_vote(f"V{i}", f"E{i}", "팀7", "팀1", "팀2", "팀3")
archive_dir = os.path.dirname(TEST_DATA) or "."
before = set(f for f in os.listdir(archive_dir) if f.startswith("results_"))
path = archive_results()
assert path is not None, "archive None 반환"
assert os.path.exists(path)
after = set(f for f in os.listdir(archive_dir) if f.startswith("results_"))
assert len(after) > len(before)
os.unlink(path)
def t_archive_skip_pending():
reset_votes()
for i in range(3):
add_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀1")
for i in range(3, 6):
add_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀2")
path = archive_results()
assert path is None
def t_atomic_write():
"""save_data 후 load_data가 동일 데이터 반환."""
reset_votes()
data = load_data()
data["titles"]["팀1"] = "test atomic"
save_data(data)
fresh = load_data()
assert fresh["titles"]["팀1"] == "test atomic"
def t_vote_count():
reset_votes()
assert len(list_votes()) == 0
add_vote("A", "1", "팀1", "팀2", "팀3", "팀4")
assert len(list_votes()) == 1
clear_votes()
assert len(list_votes()) == 0
if __name__ == "__main__":
print(f"# E2E (data={TEST_DATA})\n")
test("hackathon.json 로드 (34명, 7팀)", t_load)
test("투표 마감 토글", t_voting_toggle)
test("단순 winner", t_simple_winners)
test("priority 1팀 모든 분야 1위", t_priority_one_team_all_first)
test("동률 status='tie'", t_tie_pending)
test("tie_break 적용", t_tie_break_random)
test("voter_name UNIQUE 강제", t_unique_voter)
test("팀 제목 영속", t_titles_persist)
test("archive 파일 생성", t_archive_writes_file)
test("archive 동률 skip", t_archive_skip_pending)
test("atomic write", t_atomic_write)
test("clear_votes", t_vote_count)
fails = sum(1 for r, _ in results if r == FAIL)
print(f"\n# {len(results)} 중 통과 {len(results) - fails}, 실패 {fails}")
os.unlink(TEST_DATA)
sys.exit(0 if fails == 0 else 1)