Files
test-search-engine/tests/test_message_corpus.py
T

291 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
import pytest
from app.matcher import CatalogMatcher
ROOT = Path(__file__).resolve().parents[1]
MESSAGE_PATHS = (ROOT / "messages.txt", ROOT / "more_messages.txt")
MatchStatus = Literal["matched", "ambiguous", "not_found"]
ExpectedResult = tuple[MatchStatus, tuple[str, ...]]
@dataclass(frozen=True, slots=True)
class MessageCase:
id: str
source: str
line_number: int
message: str
def _load_message_cases() -> tuple[MessageCase, ...]:
cases: list[MessageCase] = []
for path in MESSAGE_PATHS:
for line_number, raw_line in enumerate(
path.read_text(encoding="utf-8").splitlines(), start=1
):
message = raw_line.strip()
if not message:
continue
cases.append(
MessageCase(
id=f"{path.name}:{line_number}",
source=path.name,
line_number=line_number,
message=message,
)
)
return tuple(cases)
MESSAGE_CASES = _load_message_cases()
VALIDATED_MESSAGE_CASES = tuple(
case for case in MESSAGE_CASES if case.source == "messages.txt"
)
MORE_MESSAGE_CASES = tuple(
case for case in MESSAGE_CASES if case.source == "more_messages.txt"
)
# Hand-reviewed acceptance results for every required messages.txt query.
# Do not regenerate this table from CatalogMatcher output: it is the independent
# oracle that should catch semantic regressions in the implementation.
VALIDATED_MESSAGE_RESULTS: dict[str, ExpectedResult] = {
"здравствуйте, есть саморезы гкл 3.5х25?": ("ambiguous", ("SAM-0063", "SAM-0061", "SAM-0062")),
"дрель ударная prowerk pw-750 в наличии?": ("matched", ("INS-0008",)),
"кабель шввп 2х0.5 сколько за метр": ("matched", ("KAB-0017",)),
"диск пильный 190 на 48 зубьев": ("matched", ("DSK-0034",)),
"уровень 1000 мм есть?": ("matched", ("RIN-0024",)),
"гайки м10 нужны": ("matched", ("KRP-0020",)),
"лента фум 12 мм": ("matched", ("RAS-0060",)),
"перчатки нитриловые есть?": ("matched", ("PER-0002",)),
"проф труба 20х20 стенка полтора": ("matched", ("TRB-0002",)),
"шурик на 12в недорогой": ("ambiguous", ("INS-0017", "INS-0010", "INS-0003")),
"болгарка на 230 какая есть": ("ambiguous", ("INS-0014", "INS-0028", "INS-0007")),
"гкл 9.5 сколько лист": ("matched", ("GKL-0001",)),
"хомуты пластиковые 4.8х400": ("matched", ("RAS-0014",)),
"сдс бур 6 на 110": ("matched", ("BIT-0074",)),
"наждачка р120 листами": ("matched", ("RAS-0050",)),
"круг зачистной на 125": ("matched", ("DSK-0018",)),
"бита ph2 50 мм": ("matched", ("BIT-0005",)),
"саморезы по дереву 3.5х45 пачка 200": ("matched", ("SAM-0010",)),
"труба профильная 20х20х2": ("matched", ("TRB-0003",)),
"ввгнг лс 3х1.5": ("matched", ("KAB-0010",)),
"диск отрезной по металлу 115х1.2": ("matched", ("DSK-0002",)),
"бур sds 10х210": ("matched", ("BIT-0080",)),
"дюбель-гвоздь 6х60": ("matched", ("KRP-0048",)),
"саморезы по металлу 4.2х70": ("not_found", ()),
"труба профильная 35х35": ("not_found", ()),
"кабель ввгнг 4х2.5": ("not_found", ()),
"ушм на 150": ("not_found", ()),
"бита t50": ("not_found", ()),
"шпилька м16 метровая": ("not_found", ()),
"нужен кабель": ("ambiguous", ("KAB-0004", "KAB-0005", "KAB-0008")),
"дайте дюбелей": ("ambiguous", ("KRP-0050", "KRP-0047", "KRP-0048")),
"сверло нужно": ("ambiguous", ("BIT-0060", "BIT-0061", "BIT-0062")),
"какие есть диски": ("ambiguous", ("DSK-0011", "DSK-0012", "DSK-0013")),
"перфоратор посоветуйте": ("ambiguous", ("INS-0012", "INS-0026", "INS-0005")),
"шуруповерт как у макиты, только дешевле": ("ambiguous", ("INS-0017", "INS-0010", "INS-0003")),
"здравствуйте, вы до скольки работаете?": ("not_found", ()),
"можно оплатить картой при получении?": ("not_found", ()),
"где находится ваш магазин": ("not_found", ()),
"статус заказа 4512 подскажите": ("not_found", ()),
"спасибо, заказ получил, все отлично": ("not_found", ()),
}
# The larger exploratory corpus is useful as a regression snapshot, but it is
# intentionally kept separate from the hand-reviewed acceptance set above.
MORE_MESSAGE_BASELINE_RESULTS: dict[str, ExpectedResult] = {
"добрый день, саморезы по дереву 3.5х25 пачка 200 штук": ("matched", ("SAM-0001",)),
"саморез гкл 3,5 на 25 кг": ("matched", ("SAM-0063",)),
"нужны черные саморезы 4.2x75 упаковка 1000": ("matched", ("SAM-0023",)),
"самарезы па дереву 3.5х45 200шт": ("ambiguous", ("SAM-0010", "SAM-0012", "SAM-0011")),
"саморезы 99х999": ("not_found", ()),
"дюбель гвоздь 6x40": ("matched", ("KRP-0047",)),
"дюбеля 8 на 80 есть?": ("matched", ("KRP-0049",)),
"анкерный болт м10": ("ambiguous", ("KRP-0009", "KRP-0010", "KRP-0011")),
"гайка м8 оцинкованная": ("matched", ("KRP-0019",)),
"шайбы м12 сколько стоят": ("ambiguous", ("KRP-0032", "KRP-0031")),
"болт м6х30 50 штук": ("ambiguous", ("KRP-0002", "KRP-0001", "KRP-0003")),
"шпилька м10 один метр": ("matched", ("KRP-0034",)),
"крепеж какой-нибудь": ("not_found", ()),
"кабель ввг нг ls 3*2,5": ("matched", ("KAB-0011",)),
"провод пвс 2х1.5 нужен": ("not_found", ()),
"ШВВП 2 X 0,75, цена за метр?": ("matched", ("KAB-0018",)),
"кабель кг 3х2,5": ("not_found", ()),
"кабель для чайника": ("ambiguous", ("KAB-0004", "KAB-0005", "KAB-0008")),
"3х1.5": ("ambiguous", ("KAB-0022", "KAB-0002", "KAB-0010")),
"ввгнг 20х20": ("not_found", ()),
"труба профильная 40x20x2": ("not_found", ()),
"профтруба 60 40 3": ("ambiguous", ("TRB-0009", "TRB-0010", "TRB-0015")),
"труба квадратная двадцать на двадцать": ("ambiguous", ("TRB-0003", "TRB-0007", "TRB-0008")),
"труба 35х35х2": ("not_found", ()),
"нужна труба длиной до луны": ("ambiguous", ("TRB-0003", "TRB-0007", "TRB-0008")),
"гипсокартон 12,5 влагостойкий": ("matched", ("GKL-0003",)),
"гкл обычный 9.5 мм": ("matched", ("GKL-0001",)),
"лист гипса зеленый": ("ambiguous", ("GKL-0001", "GKL-0002", "GKL-0004")),
"гипсокортан 12.5": ("ambiguous", ("GKL-0002", "GKL-0004", "GKL-0003")),
"фанера 18 мм": ("not_found", ()),
"дрель prowerk pw-750": ("matched", ("INS-0008",)),
"аккумуляторная дрель 18в": ("not_found", ()),
"шуруповёрт 12 вольт самый дешёвый": ("ambiguous", ("INS-0017", "INS-0010", "INS-0003")),
"перфоратор sds plus 800 вт": ("not_found", ()),
"болгарка 125": ("ambiguous", ("INS-0013", "INS-0027", "INS-0006")),
"ушм 230 мм 2200 вт": ("not_found", ()),
"лобзик посоветуйте": ("ambiguous", ("INS-0031", "INS-0029")),
"makita df333 есть?": ("not_found", ()),
"инструмент чтобы сверлить стену": ("ambiguous", ("BIT-0067", "BIT-0068", "BIT-0069")),
"дрэл 9999 ват": ("not_found", ()),
"бур sds-plus 8x160": ("matched", ("BIT-0077",)),
"бур 10 на 210 sds+": ("matched", ("BIT-0080",)),
"бур SDS MAX 20х400": ("not_found", ()),
"сверло по металлу 6 мм": ("matched", ("BIT-0045",)),
"сверло по дереву 10x120": ("not_found", ()),
"бита ph2 25": ("matched", ("BIT-0004",)),
"набор бит torx": ("not_found", ()),
"бита t30 50 мм": ("matched", ("BIT-0029",)),
"бита t99": ("not_found", ()),
"насадка на шуруповерт": ("ambiguous", ("INS-0010", "INS-0011", "INS-0024")),
"диск отрезной 125х1,0 металл": ("matched", ("DSK-0003",)),
"круг по металлу 230 на 2.5": ("not_found", ()),
"пильный диск 190x48t": ("matched", ("DSK-0034",)),
"алмазный диск 125 бетон": ("ambiguous", ("DSK-0025", "DSK-0026", "DSK-0027")),
"диск на болгарку": ("ambiguous", ("DSK-0011", "DSK-0012", "DSK-0013")),
"круг квадратный 12х34": ("not_found", ()),
"наждачная бумага p80": ("matched", ("RAS-0049",)),
"шкурка нулёвка": ("ambiguous", ("RAS-0049", "RAS-0048", "RAS-0052")),
"круг лепестковый 125 р40": ("matched", ("RAS-0054",)),
"лента фум 19 мм": ("not_found", ()),
"изолента синяя 15 мм": ("not_found", ()),
"хомут 4,8x300 черный": ("not_found", ()),
"стяжки пластиковые": ("ambiguous", ("RAS-0009", "RAS-0011", "RAS-0012")),
"перчатки нитриловые размер l": ("matched", ("PER-0002",)),
"перчатки хб с пвх": ("matched", ("PER-0001",)),
"мешки для мусора 120 литров": ("ambiguous", ("RAS-0035", "RAS-0034")),
"пена монтажная зимняя": ("ambiguous", ("RAS-0015", "RAS-0016")),
"герметик силиконовый прозрачный": ("ambiguous", ("RAS-0018", "RAS-0017")),
"клей жидкие гвозди": ("ambiguous", ("KRP-0043", "KRP-0044", "KRP-0045")),
"WD-40 400 мл": ("not_found", ()),
"здравствуйте": ("not_found", ()),
"вы сегодня открыты?": ("not_found", ()),
"какой адрес": ("not_found", ()),
"есть доставка в химки?": ("not_found", ()),
"можно вернуть товар без чека": ("not_found", ()),
"примите заказ 12345": ("not_found", ()),
"где мой заказ 000001": ("not_found", ()),
"оплатил, но денег нет": ("not_found", ()),
"спасибо большое!": ("not_found", ()),
"позовите человека": ("not_found", ()),
"1234567890": ("not_found", ()),
"000000000000000000": ("not_found", ()),
"42": ("not_found", ()),
"qwerty asdf zxcv": ("not_found", ()),
"йцукен фывапр олдж": ("not_found", ()),
"абракадабра крокодил вертолёт": ("not_found", ()),
"!@#$%^&*()": ("not_found", ()),
"..........": ("not_found", ()),
"купить купить купить": ("not_found", ()),
"null": ("not_found", ()),
"None": ("not_found", ()),
"SELECT * FROM catalog;": ("not_found", ()),
"<script>alert(1)</script>": ("not_found", ()),
"../../etc/passwd": ("not_found", ()),
"🤖🔩🔨": ("not_found", ()),
"САМОБРЕЗЫ ГКЛ 3.5Х25???": ("not_found", ()),
"ну эээ диск этот круглый короче": ("ambiguous", ("DSK-0011", "DSK-0012", "DSK-0013")),
"мне два метра того красного": ("not_found", ()),
"хочу всё и сразу за сто рублей": ("not_found", ()),
"статус заказа 4512 и бур 8х160": ("matched", ("BIT-0077",)),
"диск": ("ambiguous", ("DSK-0011", "DSK-0012", "DSK-0013")),
"24 зуба": ("ambiguous", ("DSK-0029", "DSK-0032", "DSK-0035")),
"210 мм 24 зуба": ("matched", ("DSK-0035",)),
"125 мм": ("ambiguous", ("INS-0013", "DSK-0025", "RAS-0054")),
"12 в": ("ambiguous", ("INS-0003", "INS-0010", "INS-0024")),
"900 вт": ("ambiguous", ("INS-0009", "INS-0013")),
"M10": ("ambiguous", ("KRP-0030", "KRP-0020", "KRP-0009")),
"P120": ("ambiguous", ("DSK-0024", "RAS-0050", "RAS-0057")),
"SDS-plus": ("ambiguous", ("BIT-0073", "INS-0012")),
"PH2": ("ambiguous", ("BIT-0004", "RIN-0010")),
"20x20x2": ("matched", ("TRB-0003",)),
"ToolKraft 125 мм": ("matched", ("INS-0027",)),
"99999 мм": ("not_found", ()),
"аквариум 125 мм": ("not_found", ()),
"удар": ("ambiguous", ("INS-0008", "INS-0009", "INS-0022")),
"ударная": ("ambiguous", ("INS-0008", "INS-0009", "INS-0022")),
"удар мощный": ("ambiguous", ("INS-0008", "INS-0009", "INS-0022")),
"удар мощно 900 хромированый анус долбить": ("ambiguous", ("INS-0009", "INS-0016", "INS-0023")),
"дрель удар мощно 900 хромированый анус долбить": ("ambiguous", ("INS-0009", "INS-0023", "INS-0002")),
"анус": ("not_found", ()),
"долбить": ("not_found", ()),
"Арс-12": ("matched", ("INS-0017",)),
}
EXPECTED_RESULTS = VALIDATED_MESSAGE_RESULTS | MORE_MESSAGE_BASELINE_RESULTS
def test_message_files_are_present_and_have_expected_size() -> None:
counts = {
path.name: sum(
bool(line.strip())
for line in path.read_text(encoding="utf-8").splitlines()
)
for path in MESSAGE_PATHS
}
assert counts["messages.txt"] > 0
assert counts["more_messages.txt"] >= 100
messages = [case.message for case in MESSAGE_CASES]
assert len(messages) == len(set(messages)), "message texts must be unique"
assert set(EXPECTED_RESULTS) == set(messages)
assert set(VALIDATED_MESSAGE_RESULTS) == {
case.message for case in MESSAGE_CASES if case.source == "messages.txt"
}
assert set(MORE_MESSAGE_BASELINE_RESULTS) == {
case.message for case in MESSAGE_CASES if case.source == "more_messages.txt"
}
def _assert_expected_result(
matcher: CatalogMatcher, case: MessageCase
) -> None:
result = matcher.match(case.message)
assert result.message == case.message
assert result.status in {"matched", "ambiguous", "not_found"}
assert len(result.candidates) <= 3
assert len({candidate.sku for candidate in result.candidates}) == len(
result.candidates
)
assert all(0.0 <= candidate.confidence <= 1.0 for candidate in result.candidates)
if result.status == "matched":
assert len(result.candidates) == 1
elif result.status == "ambiguous":
assert 2 <= len(result.candidates) <= 3
else:
assert result.candidates == []
expected_status, expected_skus = EXPECTED_RESULTS[case.message]
assert result.status == expected_status
assert tuple(candidate.sku for candidate in result.candidates) == expected_skus
@pytest.mark.parametrize(
"case", VALIDATED_MESSAGE_CASES, ids=lambda case: case.id
)
def test_validated_messages_txt_result(
matcher: CatalogMatcher, case: MessageCase
) -> None:
_assert_expected_result(matcher, case)
@pytest.mark.parametrize("case", MORE_MESSAGE_CASES, ids=lambda case: case.id)
def test_more_messages_regression(
matcher: CatalogMatcher, case: MessageCase
) -> None:
_assert_expected_result(matcher, case)