186 lines
5.2 KiB
Python
186 lines
5.2 KiB
Python
from __future__ import annotations
|
||
|
||
from decimal import Decimal
|
||
from pathlib import Path
|
||
from typing import Callable
|
||
|
||
import pytest
|
||
|
||
from app.catalog import CatalogItem, RawCatalogItem, load_raw_catalog
|
||
from app.matcher import CatalogMatcher
|
||
|
||
|
||
CATALOG_ITEMS = load_raw_catalog(
|
||
Path(__file__).resolve().parents[1] / "catalog_excel.csv"
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize("item", CATALOG_ITEMS, ids=lambda item: item.sku)
|
||
def test_full_catalog_name_retrieves_its_own_sku(
|
||
matcher: CatalogMatcher, item: RawCatalogItem
|
||
) -> None:
|
||
result = matcher.match(item.name)
|
||
|
||
assert item.sku in {candidate.sku for candidate in result.candidates}
|
||
|
||
|
||
def test_every_canonical_product_type_returns_only_its_own_items(
|
||
matcher: CatalogMatcher,
|
||
) -> None:
|
||
items_by_sku = {item.sku: item for item in matcher.items}
|
||
product_types = {item.product_type for item in matcher.items}
|
||
|
||
for product_type in product_types:
|
||
result = matcher.match(product_type)
|
||
|
||
assert result.candidates, product_type
|
||
assert all(
|
||
items_by_sku[candidate.sku].product_type == product_type
|
||
for candidate in result.candidates
|
||
), product_type
|
||
|
||
|
||
def test_every_catalog_model_code_returns_only_matching_items(
|
||
matcher: CatalogMatcher,
|
||
) -> None:
|
||
items_by_sku = {item.sku: item for item in matcher.items}
|
||
model_codes = {
|
||
model
|
||
for item in matcher.items
|
||
for model in item.attributes.model_codes
|
||
}
|
||
|
||
for model_code in model_codes:
|
||
result = matcher.match(model_code)
|
||
|
||
assert result.candidates, model_code
|
||
assert all(
|
||
model_code in items_by_sku[candidate.sku].attributes.model_codes
|
||
for candidate in result.candidates
|
||
), model_code
|
||
|
||
|
||
def test_every_catalog_brand_returns_only_its_own_items(
|
||
matcher: CatalogMatcher,
|
||
) -> None:
|
||
items_by_sku = {item.sku: item for item in matcher.items}
|
||
brands = {
|
||
item.attributes.brand
|
||
for item in matcher.items
|
||
if item.attributes.brand is not None
|
||
}
|
||
|
||
for brand in brands:
|
||
result = matcher.match(brand)
|
||
|
||
assert result.candidates, brand
|
||
assert all(
|
||
items_by_sku[candidate.sku].attributes.brand == brand
|
||
for candidate in result.candidates
|
||
), brand
|
||
|
||
|
||
def _number_text(value: Decimal) -> str:
|
||
return format(value, "f")
|
||
|
||
|
||
def _assert_query_returns_only(
|
||
matcher: CatalogMatcher,
|
||
query: str,
|
||
predicate: Callable[[CatalogItem], bool],
|
||
) -> None:
|
||
items_by_sku = {item.sku: item for item in matcher.items}
|
||
result = matcher.match(query)
|
||
|
||
assert result.candidates, query
|
||
assert all(
|
||
predicate(items_by_sku[candidate.sku]) for candidate in result.candidates
|
||
), query
|
||
|
||
|
||
def test_every_catalog_millimetre_value_is_searchable_without_product_name(
|
||
matcher: CatalogMatcher,
|
||
) -> None:
|
||
values = {
|
||
value
|
||
for item in matcher.items
|
||
for value in (
|
||
*item.attributes.mm_values,
|
||
*((item.attributes.dimensions[0],) if item.attributes.dimensions else ()),
|
||
*((item.attributes.thread_length_mm,) if item.attributes.thread_length_mm else ()),
|
||
)
|
||
}
|
||
|
||
for value in values:
|
||
_assert_query_returns_only(
|
||
matcher,
|
||
f"{_number_text(value)} мм",
|
||
lambda item, expected=value: (
|
||
expected in item.attributes.mm_values
|
||
or bool(
|
||
item.attributes.dimensions
|
||
and item.attributes.dimensions[0] == expected
|
||
)
|
||
or item.attributes.thread_length_mm == expected
|
||
),
|
||
)
|
||
|
||
|
||
def test_every_catalog_dimension_is_searchable_without_product_name(
|
||
matcher: CatalogMatcher,
|
||
) -> None:
|
||
dimensions = {
|
||
item.attributes.dimensions
|
||
for item in matcher.items
|
||
if item.attributes.dimensions
|
||
}
|
||
|
||
for dimension in dimensions:
|
||
query = "x".join(_number_text(value) for value in dimension)
|
||
_assert_query_returns_only(
|
||
matcher,
|
||
query,
|
||
lambda item, expected=dimension: (
|
||
len(item.attributes.dimensions) >= len(expected)
|
||
and item.attributes.dimensions[: len(expected)] == expected
|
||
),
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("field", "suffix"),
|
||
[
|
||
("voltage_v", " В"),
|
||
("power_w", " Вт"),
|
||
("thread_size", ""),
|
||
("grit", ""),
|
||
("profile", ""),
|
||
("sds_type", ""),
|
||
("teeth", " зубьев"),
|
||
],
|
||
)
|
||
def test_every_scalar_catalog_attribute_is_searchable_without_product_name(
|
||
matcher: CatalogMatcher,
|
||
field: str,
|
||
suffix: str,
|
||
) -> None:
|
||
values = {
|
||
getattr(item.attributes, field)
|
||
for item in matcher.items
|
||
if getattr(item.attributes, field) is not None
|
||
}
|
||
|
||
for value in values:
|
||
if field == "thread_size":
|
||
query = f"M{_number_text(value)}"
|
||
elif field == "grit":
|
||
query = f"P{value}"
|
||
else:
|
||
value_text = _number_text(value) if isinstance(value, Decimal) else str(value)
|
||
query = f"{value_text}{suffix}"
|
||
_assert_query_returns_only(
|
||
matcher,
|
||
query,
|
||
lambda item, expected=value: getattr(item.attributes, field) == expected,
|
||
)
|