All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 1m5s
Замер на проде 2026-08-29. Три независимых запуска camoufox через прод-прокси, идём по карточкам до первого отказа — каждый раз одно и то же: карточка #1 даёт 200 и ~1 МБ с SSR-стейтом, карточка #2 даёт 401 и страницу отказа на 26 625 байт. A/B на восьми карточках: свой браузер на каждую — 4 из 4 успеха, один браузер на четыре — 1 из 4. Прямой выход с сервера и выход через прокси неразличимы (1 из 8 в обоих условиях), то есть дело не в IP. Свежего КОНТЕКСТА не хватает: в контрольном замере каждая карточка бралась через browser.new_page(), в новом изолированном контексте, — и всё равно отказ со второй. Признак живёт на уровне процесса, camoufox генерирует отпечаток при запуске, а не при создании контекста. Отсюда: reset_context (#3118) эту задачу не решает в принципе. Цена перезапуска — 0.6 с (3.5 с только первый, холодный). - PROVIDERS: добавлен "domclick" (+ host-detect). Раньше он проваливался в generic и делил браузер со счётчиком страниц с прочим трафиком — при пороге перезапуска 1 это было бы неверно. - BROWSER_RECYCLE_PAGES стал поставщик-зависимым (_resolve_recycle_pages + BROWSER_RECYCLE_PAGES_{PROVIDER}), по образцу BROWSER_BLOCK_IMAGES_{PROVIDER} из #3185. Код-дефолт domclick=1, остальным прежние 15 — у Авито и Циана узор другой и своего замера под него нет. Отдельно починены ~24 холостых охранника в тестах. Они делали monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000), чтобы запретить перезапуск браузера; после перехода на словарь этот патч перестал на что-либо влиять, и набор оставался зелёным лишь потому, что ни один тест не делает 15 страниц подряд. Теперь патчится _RECYCLE_PAGES_BY_PROVIDER, а сама глобальная константа убрана, чтобы её не патчили снова. Проверено мутацией: при пороге 1 для всех провайдеров падают ровно три теста, которые этот дефолт и проверяют, остальные 155 удерживаются — значит охранники работают.
74 lines
3.7 KiB
Python
74 lines
3.7 KiB
Python
"""test_server_recycle.py — юниты для per-provider recycle_pages threshold (#3205).
|
||
|
||
Проверяет ``_resolve_recycle_pages``:
|
||
1. per-provider env перебивает глобальный env;
|
||
2. глобальный env перебивает код-дефолт per-provider;
|
||
3. невалидное (не int) значение → фолбэк, без исключения;
|
||
4. значение <1 → приводится к 1;
|
||
5. domclick без env → код-дефолт 1 (живой замер #3205: одна карточка на процесс);
|
||
6. avito/cian/yandex без env → общий фолбэк 15 (их узор бана не менялся).
|
||
|
||
camoufox/Playwright НЕ поднимается: тестируется чистая функция резолвера.
|
||
|
||
Запуск (из tradein-mvp/browser/)::
|
||
|
||
python -m pytest test_server_recycle.py -q
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import importlib.util
|
||
from pathlib import Path
|
||
|
||
# server.py — не пакет (отдельный сервис без __init__/pyproject). Грузим по пути.
|
||
_SERVER_PATH = Path(__file__).resolve().parent / "server.py"
|
||
_spec = importlib.util.spec_from_file_location("tradein_browser_server", _SERVER_PATH)
|
||
assert _spec is not None and _spec.loader is not None
|
||
server = importlib.util.module_from_spec(_spec)
|
||
_spec.loader.exec_module(server)
|
||
|
||
|
||
def test_per_provider_env_overrides_global() -> None:
|
||
"""BROWSER_RECYCLE_PAGES_DOMCLICK=3 + глобал=10 → domclick=3."""
|
||
fake_env = {"BROWSER_RECYCLE_PAGES": "10", "BROWSER_RECYCLE_PAGES_DOMCLICK": "3"}
|
||
assert server._resolve_recycle_pages("domclick", fake_env) == 3
|
||
|
||
|
||
def test_global_env_overrides_code_default() -> None:
|
||
"""Нет per-provider env, но задан глобальный → перебивает код-дефолт domclick=1."""
|
||
fake_env = {"BROWSER_RECYCLE_PAGES": "7"}
|
||
assert server._resolve_recycle_pages("domclick", fake_env) == 7
|
||
|
||
|
||
def test_invalid_per_provider_env_falls_back() -> None:
|
||
"""Невалидный (не int) per-provider env → фолбэк на глобальный уровень."""
|
||
fake_env = {"BROWSER_RECYCLE_PAGES": "9", "BROWSER_RECYCLE_PAGES_CIAN": "abc"}
|
||
assert server._resolve_recycle_pages("cian", fake_env) == 9
|
||
|
||
|
||
def test_invalid_global_env_falls_back_to_code_default() -> None:
|
||
"""Невалидный глобальный env → фолбэк на код-дефолт per-provider (без исключения)."""
|
||
fake_env = {"BROWSER_RECYCLE_PAGES": "not-a-number"}
|
||
assert server._resolve_recycle_pages("domclick", fake_env) == 1
|
||
assert server._resolve_recycle_pages("cian", fake_env) == 15
|
||
|
||
|
||
def test_value_below_one_clamped_to_one() -> None:
|
||
"""Явный env <1 недопустим → приводится к 1, а не проваливается дальше по цепочке."""
|
||
assert server._resolve_recycle_pages("avito", {"BROWSER_RECYCLE_PAGES_AVITO": "0"}) == 1
|
||
assert server._resolve_recycle_pages("cian", {"BROWSER_RECYCLE_PAGES": "-5"}) == 1
|
||
|
||
|
||
def test_domclick_without_env_defaults_to_one() -> None:
|
||
"""#3205: без единого env — domclick=1 (одна карточка на процесс camoufox)."""
|
||
assert server._resolve_recycle_pages("domclick", {}) == 1
|
||
|
||
|
||
def test_other_providers_without_env_default_to_fifteen() -> None:
|
||
"""avito/cian/yandex без env — общий фолбэк 15, узор бана у них не менялся."""
|
||
for provider in ("avito", "cian", "yandex"):
|
||
assert server._resolve_recycle_pages(provider, {}) == 15
|
||
|
||
|
||
def test_generic_without_env_defaults_to_fifteen() -> None:
|
||
assert server._resolve_recycle_pages("generic", {}) == 15
|