"""Юнит-тесты алгоритма Winnowing (уровень 1 — точные/частичные совпадения).""" from app.algorithms.winnowing import ( compute_similarity, get_ngrams, hash_ngram, jaccard_similarity, sample_evenly, winnow, winnow_ordered, ) # Достаточно длинный текст, чтобы окно Winnowing реально отработало LONG = ( "машинное обучение позволяет извлекать закономерности из больших объёмов " "данных без явного программирования каждого правила вручную аналитиком" ) def test_get_ngrams_basic(): assert get_ngrams(["a", "b", "c", "d"], k=2) == ["a b", "b c", "c d"] def test_get_ngrams_too_short_is_empty(): assert get_ngrams(["a", "b"], k=5) == [] def test_hash_ngram_is_deterministic(): assert hash_ngram("одна и та же строка") == hash_ngram("одна и та же строка") def test_hash_ngram_fits_signed_int64(): # Хэш обязан умещаться в PostgreSQL BIGINT (signed 64-bit) for s in ("a", "тест", "one two three four five", "𝔘𝔫𝔦𝔠𝔬𝔡𝔢"): h = hash_ngram(s) assert -(2**63) <= h <= 2**63 - 1 def test_winnow_short_text_is_empty(): assert winnow("три слова тут", k=5) == set() def test_winnow_identical_text_identical_fingerprint(): assert winnow(LONG) == winnow(LONG) def test_winnow_is_case_insensitive(): assert winnow(LONG) == winnow(LONG.upper()) def test_jaccard_identical_is_one(): fp = winnow(LONG) assert fp # непустой отпечаток assert jaccard_similarity(fp, fp) == 1.0 def test_jaccard_disjoint_is_zero(): assert jaccard_similarity({1, 2, 3}, {4, 5, 6}) == 0.0 def test_jaccard_empty_is_zero(): assert jaccard_similarity(set(), {1, 2}) == 0.0 def test_compute_similarity_identical_documents_is_one(): # Ключевая гарантия L1: копия документа детектится как 100% совпадение assert compute_similarity(LONG, LONG) == 1.0 def test_compute_similarity_unrelated_documents_is_low(): other = ( "рецепт домашнего хлеба на закваске требует терпения муки воды соли " "и тёплого места для медленного подъёма теста в течение ночи" ) assert compute_similarity(LONG, other) < 0.1 def test_compute_similarity_partial_overlap_is_between(): modified = LONG + " добавлен ещё один совершенно новый хвост предложения здесь" sim = compute_similarity(LONG, modified) assert 0.0 < sim < 1.0 # ─── Обрезка отпечатков по лимиту ──────────────────────────────────────────── # Регрессия: winnow() возвращает set, и list(fp)[:limit] брал произвольное # подмножество — у длинного документа целые куски оставались без покрытия, # и списывание из них не находилось. def test_winnow_ordered_matches_winnow_by_content(): """Тот же набор отпечатков, что и у winnow, только с порядком.""" assert set(winnow_ordered(LONG)) == winnow(LONG) def test_winnow_ordered_has_no_duplicates(): ordered = winnow_ordered(LONG) assert len(ordered) == len(set(ordered)) def test_winnow_ordered_follows_text_order(): """Отпечатки начала текста идут раньше отпечатков продолжения.""" tail = " совершенно другой хвост про выпечку хлеба и закваску в тёплой печи" ordered = winnow_ordered(LONG + tail) head_prints = set(winnow_ordered(LONG)) positions = [i for i, h in enumerate(ordered) if h in head_prints] # Отпечатки первой половины сосредоточены в начале списка, а не разбросаны assert max(positions) < len(ordered) assert positions[0] == 0 def test_sample_evenly_keeps_everything_under_limit(): items = [1, 2, 3] assert sample_evenly(items, 10) == items assert sample_evenly(items, 0) == items # 0 = без ограничения def test_sample_evenly_respects_limit(): items = list(range(1000)) assert len(sample_evenly(items, 100)) == 100 def test_sample_evenly_covers_whole_document(): """Главное свойство: выборка растянута по всей длине, а не обрезана с начала.""" items = list(range(1000)) sampled = sample_evenly(items, 10) assert sampled[0] == 0 assert sampled[-1] >= 900 # хвост документа тоже покрыт assert sampled == sorted(sampled) # порядок сохранён def test_sample_evenly_spreads_uniformly(): items = list(range(100)) sampled = sample_evenly(items, 10) gaps = [b - a for a, b in zip(sampled, sampled[1:], strict=False)] assert max(gaps) - min(gaps) <= 1 # шаг ровный