110 lines
4.3 KiB
Python
110 lines
4.3 KiB
Python
"""군집화 기반 표절 판별 단위테스트 (계획서 2단계 고도화).
|
|
|
|
핵심 시나리오: "본문(lemma)·키워드는 그대로 두고 인물만 바꾼 표절" 을
|
|
element_swap_plagiarism 으로 분해해서 잡는다.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from app.api.schemas import ExtractedElements
|
|
from app.engine.clustering import (
|
|
ClusterIndex,
|
|
signature_similarity,
|
|
_element_signature,
|
|
)
|
|
|
|
|
|
def _elem(characters=None, motifs=None, keywords=None, genre=None) -> ExtractedElements:
|
|
return ExtractedElements(
|
|
characters=characters or [],
|
|
motifs=motifs or [],
|
|
keywords=keywords or [],
|
|
genre=genre,
|
|
)
|
|
|
|
|
|
def test_signature_similarity_identical():
|
|
a = _element_signature(_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리"], genre="역사"))
|
|
assert signature_similarity(a, a) == 1.0
|
|
|
|
|
|
def test_clusters_group_similar_docs():
|
|
# 두 홍길동 변형은 한 군집, 어린왕자는 별도 군집
|
|
ids = ["hong-a", "hong-b", "prince"]
|
|
elems = [
|
|
_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리", "재물"], genre="역사"),
|
|
_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리", "재물"], genre="역사"),
|
|
_elem(characters=["어린왕자", "여우"], keywords=["사막", "장미", "별"], genre="동화"),
|
|
]
|
|
idx = ClusterIndex(ids, elems, link_threshold=0.35)
|
|
assert idx.cluster_of("hong-a") == idx.cluster_of("hong-b")
|
|
assert idx.cluster_of("prince") != idx.cluster_of("hong-a")
|
|
|
|
|
|
def test_route_to_nearest_cluster():
|
|
ids = ["hong", "prince"]
|
|
elems = [
|
|
_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리", "재물"], genre="역사"),
|
|
_elem(characters=["어린왕자"], keywords=["사막", "장미", "별"], genre="동화"),
|
|
]
|
|
idx = ClusterIndex(ids, elems, link_threshold=0.9) # 분리 유지
|
|
q = _elem(characters=["홍길동"], keywords=["활빈당", "재물"], genre="역사")
|
|
routed = idx.route(q)
|
|
assert routed is not None
|
|
assert "hong" in routed.members
|
|
|
|
|
|
def test_element_swap_plagiarism_detected():
|
|
"""본문 lemma·키워드 유지 + 인물만 교체 → element_swap_plagiarism."""
|
|
ids = ["original"]
|
|
elems = [_elem(characters=["홍길동"], motifs=["복수"], keywords=["활빈당", "탐관오리", "재물", "의적"])]
|
|
lemmas = [["만들다", "빼앗다", "재물", "탐관오리", "활빈당", "나누다"]]
|
|
idx = ClusterIndex(ids, elems, doc_lemmas=lemmas, link_threshold=0.35)
|
|
|
|
# 인물(홍길동→김민수)·모티프만 바꾸고 본문 lemma/키워드는 그대로
|
|
q_elem = _elem(characters=["김민수"], motifs=["정의"], keywords=["활빈당", "탐관오리", "재물", "의적"])
|
|
q_lemmas = ["만들다", "빼앗다", "재물", "탐관오리", "활빈당", "나누다"]
|
|
|
|
sig = idx.partial_signal("original", q_elem, q_lemmas)
|
|
assert sig is not None
|
|
assert sig.verdict == "element_swap_plagiarism", sig.per_element
|
|
assert "characters" in sig.changed_elements
|
|
assert "lemmas" in sig.retained_elements
|
|
|
|
|
|
def test_near_duplicate_verdict():
|
|
ids = ["original"]
|
|
elems = [_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리", "재물"])]
|
|
lemmas = [["만들다", "빼앗다", "재물"]]
|
|
idx = ClusterIndex(ids, elems, doc_lemmas=lemmas)
|
|
|
|
sig = idx.partial_signal(
|
|
"original",
|
|
_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리", "재물"]),
|
|
["만들다", "빼앗다", "재물"],
|
|
)
|
|
assert sig is not None
|
|
assert sig.verdict == "near_duplicate"
|
|
|
|
|
|
def test_unrelated_gets_none_or_weak():
|
|
ids = ["original"]
|
|
elems = [_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리"])]
|
|
lemmas = [["만들다", "빼앗다"]]
|
|
idx = ClusterIndex(ids, elems, doc_lemmas=lemmas)
|
|
|
|
sig = idx.partial_signal(
|
|
"original",
|
|
_elem(characters=["우주비행사"], keywords=["로켓", "행성"]),
|
|
["날다", "탐사하다"],
|
|
)
|
|
assert sig is not None
|
|
assert sig.verdict in ("none", "weak")
|
|
assert sig.verdict != "element_swap_plagiarism"
|
|
|
|
|
|
def test_empty_index():
|
|
idx = ClusterIndex([], [])
|
|
assert idx.num_clusters == 0
|
|
assert idx.route(_elem(characters=["x"])) is None
|
|
assert idx.candidate_ids(_elem(characters=["x"])) == set()
|