"""ocr_normalize 단위테스트. 외부 의존 없음.""" from __future__ import annotations import pytest from app.engine.ocr_normalize import normalize_ocr, ocr_artifact_stats class TestNumberUnitSpace: """규칙 2 — 없는 어절을 만들어내는 가장 흔한 아티팩트(35.9%).""" def test_year_month(self): assert normalize_ocr("1880 년 11 월에 쓴 편지") == "1880년 11월에 쓴 편지" def test_various_units(self): assert normalize_ocr("3 개월 뒤 5 명이 2 권을 샀다") == "3개월 뒤 5명이 2권을 샀다" def test_latin_unit(self): assert normalize_ocr("12 km 를 걸었다") == "12km 를 걸었다" def test_eojeol_count_restored(self): """이 규칙의 목적 자체 — 어절 수가 정상으로 돌아와야 한다.""" assert len(normalize_ocr("1880 년 11 월").split()) == 2 def test_does_not_eat_real_words(self): """숫자 뒤 공백이라고 다 붙이면 안 된다. 단위 목록에 없으면 그대로.""" assert normalize_ocr("5 사람이 왔다") == "5 사람이 왔다" assert normalize_ocr("3 번지 근처") == "3 번지 근처" class TestHanjaGloss: """규칙 3 — 생성문에 없는 조판 관습(24.1%).""" def test_removes_gloss(self): assert normalize_ocr("대사(大使)의 임무를 띠고") == "대사의 임무를 띠고" def test_keeps_latin_paren(self): """원어 병기는 본문일 수 있으므로 건드리지 않는다.""" text = '"탄식의 노래"(Das klagende Lied)' assert "Das klagende Lied" in normalize_ocr(text) def test_keeps_non_gloss_hanja(self): """앞말이 한글이 아니면 병기가 아니다 — 보수적으로 남긴다.""" assert "(大使)" in normalize_ocr("A (大使)") def test_opt_out(self): assert "(大使)" in normalize_ocr("대사(大使)", strip_hanja_gloss=False) class TestQuotes: """규칙 4 — 낫표는 조판 관습이지 문체가 아니다(18.0%).""" def test_bracket_quotes_unified(self): assert normalize_ocr("「탄식의 노래」") == '"탄식의 노래"' def test_curly_quotes_unified(self): assert normalize_ocr("“인용” ‘강조’") == "\"인용\" '강조'" def test_opt_out(self): assert "「" in normalize_ocr("「제목」", unify_quotes=False) class TestPunctSpacing: def test_space_before_punct(self): assert normalize_ocr("기본적 . 0루") == "기본적. 0루" def test_inside_paren(self): assert normalize_ocr("( 안쪽 )") == "(안쪽)" def test_ellipsis(self): assert normalize_ocr("그리고...") == "그리고…" class TestWhitespace: def test_zero_width_removed(self): assert normalize_ocr("말러​는") == "말러는" def test_nbsp_to_space(self): assert normalize_ocr("말러 는") == "말러 는" def test_paragraphs_preserved(self): assert normalize_ocr("문단1\n\n문단2") == "문단1\n\n문단2" class TestInvariants: """이 두 성질이 깨지면 학습·캘리브레이션 결과를 신뢰할 수 없다.""" SAMPLES = [ "1880 년 11 월에 쓴 어떤 편지에는 대사(大使)의 「임무」가 적혀 있다...", "말러는 한때 바그너에 심취한 채식주의자였다. 그때 그는 도덕적 작용을 경험하였다.", "( 안쪽 ) 공백과 문장부호 . 그리고 3 개월", "", "짧은글", ] @pytest.mark.parametrize("text", SAMPLES) def test_idempotent(self, text): once = normalize_ocr(text) assert normalize_ocr(once) == once def test_noop_on_clean_text(self): """운영에서 들어올 깨끗한 원고는 바뀌지 않아야 한다.""" clean = ( "말러는 한때 바그너에 심취한 채식주의자였다. 1880년 11월에 쓴 편지에는 " '"동화극이 끝났다"고 적혀 있다.\n\n그것은 1년 이상 걸린 고심작이었다.' ) assert normalize_ocr(clean) == clean def test_empty(self): assert normalize_ocr("") == "" class TestStats: def test_counts_before_and_after(self): raw = "1880 년 대사(大使)의 「임무」... 그리고 3 개월" before = ocr_artifact_stats(raw) after = ocr_artifact_stats(normalize_ocr(raw)) assert before["num_unit_space"] > 0 assert before["hanja_gloss"] > 0 assert before["bracket_quote"] > 0 assert sum(after.values()) == 0