"""ROUGE 평가 모듈 단위테스트 (성능지표 No.7).""" from __future__ import annotations import pytest from app.engine.rouge import rouge_n, rouge_l, evaluate_pairs, tokenize def test_identical_is_perfect(): s = rouge_n("홍길동은 활빈당을 만들었다", "홍길동은 활빈당을 만들었다", n=1) assert s.f1 == 1.0 assert s.recall == 1.0 def test_no_overlap_is_zero(): s = rouge_n("우주선이 행성을 탐사한다", "사랑은 아름다운 감정이다", n=1, mode="char") assert s.f1 == 0.0 def test_partial_overlap_between_zero_and_one(): s = rouge_n("홍길동은 활빈당을 만들어 재물을 빼앗았다", "홍길동은 활빈당을 조직했다", n=1) assert 0.0 < s.f1 < 1.0 def test_rouge2_stricter_than_rouge1(): sys = "홍길동은 활빈당을 만들어 재물을 빼앗았다" ref = "홍길동은 재물을 활빈당으로 만들어 빼앗았다" # 단어 순서 섞임 r1 = rouge_n(sys, ref, n=1).f1 r2 = rouge_n(sys, ref, n=2).f1 assert r2 <= r1 # bigram 은 순서에 민감 → 더 낮거나 같음 def test_rouge_l_rewards_sequence(): s = rouge_l("홍길동은 활빈당을 만들어 재물을 빼앗았다", "홍길동은 활빈당을 만들어 재물을 빼앗았다") assert s.f1 == 1.0 def test_evaluate_pairs_aggregates(): pairs = [ ("홍길동은 활빈당을 만들었다", "홍길동은 활빈당을 만들었다"), ("어린왕자가 여우를 만났다", "어린왕자가 여우를 만났다"), ] out = evaluate_pairs(pairs) assert set(out) == {"rouge1", "rouge2", "rougeL"} assert out["rouge1"]["f1"] == 1.0 def test_tokenize_char_fallback(): toks = tokenize("Hello 월드 123", mode="char") assert "hello" in toks and "월드" in toks and "123" in toks # --------------------------------------------------------------------------- # 다중 참조 — 계획서 p.24 수식이 Σ_S∈{Reference Summaries} 로 전제하는 형태 # --------------------------------------------------------------------------- class TestMultiReference: SYS = "홍길동은 활빈당을 만들어 백성을 도왔다" REF_A = "홍길동은 활빈당을 세워 백성을 구했다" REF_B = "홍길동이 백성을 도왔다" def test_single_string_and_single_item_list_are_identical(self): """하위 호환 — 기존 호출부의 값이 절대 바뀌면 안 된다.""" for n in (1, 2): assert (rouge_n(self.SYS, self.REF_A, n, "char").as_dict() == rouge_n(self.SYS, [self.REF_A], n, "char").as_dict()) assert (rouge_l(self.SYS, self.REF_A, "char").as_dict() == rouge_l(self.SYS, [self.REF_A], "char").as_dict()) def test_recall_follows_plan_formula(self): """분자·분모를 참조별로 합산한 값과 일치해야 한다.""" from app.engine.rouge import _ngrams, tokenize sys_g = _ngrams(tokenize(self.SYS, "char"), 1) match = ref_total = 0 for ref in (self.REF_A, self.REF_B): ref_g = _ngrams(tokenize(ref, "char"), 1) match += sum((sys_g & ref_g).values()) ref_total += sum(ref_g.values()) got = rouge_n(self.SYS, [self.REF_A, self.REF_B], 1, "char") assert got.recall == pytest.approx(match / ref_total) def test_empty_reference_list_is_zero(self): assert rouge_n(self.SYS, [], 1, "char").recall == 0.0 assert rouge_l(self.SYS, [], "char").recall == 0.0 def test_blank_references_are_dropped(self): assert (rouge_n(self.SYS, [self.REF_A, "", " "], 1, "char").as_dict() == rouge_n(self.SYS, [self.REF_A], 1, "char").as_dict()) def test_evaluate_pairs_accepts_mixed_forms(self): scores = evaluate_pairs( [(self.SYS, self.REF_A), (self.SYS, [self.REF_A, self.REF_B])], mode="char" ) assert 0.0 < scores["rouge1"]["recall"] <= 1.0