gendesign/tradein-mvp/backend/tests/test_1781_secondary_only_param.py
bot-backend cdf493f345
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m56s
Deploy / build-worker (push) Successful in 4m16s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m49s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / deploy (push) Successful in 2m25s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
chore(format): нормализация под ruff 0.15.20 — 161 файл, только формат (#2864) (#3022)
2026-08-21 12:01:52 +00:00

123 lines
7.1 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.

"""`secondary_only` — параметр расписания, а не хардкод; выброшенное считается (#1781).
`run_cian_full_load` передавал `secondary_only=True` жёстко (`pipeline.py:3328`), поэтому
включить новостройки в полный обход можно было только деплоем. При этом новостройки
НЕ пропускаются при запросе — они скачиваются, разбираются и выбрасываются последним
шагом (`cian/serp.py:671`), потому что SERP-параметр `object_type=1` у Cian ненадёжен
(~5 % выдачи) и фильтруют по authoritative `listing_segment` уже после парсинга.
Следствие, проверенное по коду: проба бакета берёт `totalOffers` из Redux-состояния
SERP и считает `pages_needed = ceil(totalOffers / offers_per_page)`; `totalOffers`
включает обе категории. То есть страницы с новостройками уже скачаны, лимит страниц и
антибан-бюджет за них уже заплачены — включение стоит НОЛЬ дополнительных запросов.
Дефолт НЕ меняется: `True`, поведение прода прежнее. Меняется только то, что решение
становится правкой одной ячейки `scrape_schedules.default_params`, а не деплоем.
Замер прода 21.08.2026, ради которого это и делается: месячный охват свипа
cian/novostroyki — 11.7 % против 100 % у cian/vtorichka и avito/novostroyki;
11 993 активные строки, медианный возраст 81 сутки.
"""
from __future__ import annotations
import os
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
import inspect
def test_full_load_no_longer_hardcodes_secondary_only() -> None:
"""Головной: значение берётся из параметра, а не из литерала.
На origin/main в теле стоит `secondary_only=True` — включить новостройки
можно только правкой кода.
"""
from scraper_kit.orchestration import pipeline
src = inspect.getsource(pipeline.run_cian_full_load)
assert "secondary_only=secondary_only" in src, "значение не пробрасывается из параметра"
assert "secondary_only=True," not in src, (
"в теле остался хардкод — расписание на него повлиять не сможет"
)
def test_default_is_unchanged() -> None:
"""Контроль: дефолт остаётся True — поведение прода не меняется этим PR.
Без этого контроля правка «сделать параметром» могла бы тихо включить сбор
новостроек на проде, а это отдельное решение с замером (#1781, #2994).
"""
from scraper_kit.orchestration.pipeline import run_cian_full_load
params = inspect.signature(run_cian_full_load).parameters
assert "secondary_only" in params, (
"параметра нет в сигнатуре — значение задано в теле, расписание на него "
f"повлиять не может; параметры: {list(params)}"
)
default = params["secondary_only"].default
assert default is True, f"дефолт изменён на {default!r} — это не входило в правку"
def test_admin_request_exposes_the_flag_with_same_default() -> None:
"""Параметр доезжает до расписания: у эндпоинта есть поле с тем же дефолтом."""
from app.api.v1.admin import CianFullLoadRequest
поля = CianFullLoadRequest.model_fields
assert "secondary_only" in поля, (
"поля нет в запросе — параметр не доедет из scrape_schedules.default_params; "
f"поля: {sorted(поля)}"
)
assert поля["secondary_only"].default is True
req = CianFullLoadRequest()
assert req.secondary_only is True
assert CianFullLoadRequest(secondary_only=False).secondary_only is False
def test_counters_persist_dropped_novostroyki() -> None:
"""Выброшенное обязано сохраняться, а не только логироваться.
Счётчик `dropped_nb` печатался в лог, но в `scrape_runs.counters` не попадал, и
ответить «сколько инвентаря выбрасывает полный обход» задним числом было нечем:
логи за нужную дату уже ротировались. Тот же довод, по которому рядом заведён
`partial_buckets`.
"""
from scraper_kit.orchestration.pipeline import CianFullLoadCounters
c = CianFullLoadCounters()
assert "dropped_novostroyki" in c.to_dict(), c.to_dict()
c.dropped_novostroyki = 7
assert c.to_dict()["dropped_novostroyki"] == 7
def test_scraper_resets_the_counter_per_run() -> None:
"""Контроль: счётчик сбрасывается на каждый прогон.
Инстанс скрапера переиспользуется; без сброса второй прогон унаследовал бы
число первого и записал бы в counters завышенное значение.
"""
from scraper_kit.providers.cian.serp import CianScraper
src = inspect.getsource(CianScraper.fetch_all_secondary)
assert "self.last_dropped_nb = 0" in src, "нет сброса в начале прогона"
assert "self.last_dropped_nb += " in inspect.getsource(CianScraper._paginate_leaf_bucket), (
"накопление не там, где считается dropped_nb"
)
assert getattr(CianScraper, "last_dropped_nb", None) == 0, (
"нет класс-дефолта: атрибут не прочитается, если прогон упал до первого бакета"
)
def test_filter_still_drops_when_flag_is_on() -> None:
"""Контроль от переусердствования: при secondary_only=True фильтр остаётся.
Правка делает флаг управляемым, а не отменяет его.
"""
from scraper_kit.providers.cian.serp import CianScraper
# Фильтр лежит в _paginate_leaf_bucket — том методе, что собирает страницы
# бакета; fetch_all_secondary только раздаёт флаг вниз.
src = inspect.getsource(CianScraper._paginate_leaf_bucket)
assert 'lot.listing_segment != "novostroyki"' in src, "фильтр пропал"
assert "if secondary_only:" in src, "фильтр перестал зависеть от флага"