test(scrapers): поднять caplog-фильтры под error->warning штатных исходов
All checks were successful
CI Trade-In / backend-tests (pull_request) Successful in 8m2s
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 12s
CI Trade-In / browser-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
All checks were successful
CI Trade-In / backend-tests (pull_request) Successful in 8m2s
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 12s
CI Trade-In / browser-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
Ветка fix/3471-scraper-log-levels понизила error на warning для штатных исходов скрапинга (пустой/исчерпанный пул прокси, серия подтверждённых блоков площадки) -- 7 тестов фильтровали caplog по ERROR и падали на пустом списке. Поправлен только уровень фильтра/set_level, содержательные assert'ы (streak vs ratio, отсутствие qrator/ip_rate_limited литералов, различимость текстов "исчерпан" и "пуст") не менялись. В test_exhausted_and_empty_pool_log_texts_are_distinct оба сценария (пустой пул и fail-closed) теперь на одном уровне (warning) -- тест адаптирован проверять различимость по тексту, а не по уровню. Refs #3471 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY6iWDnGDthdvsMWgK1BMG
This commit is contained in:
parent
70b1419cde
commit
6608fd5c70
4 changed files with 27 additions and 14 deletions
|
|
@ -420,8 +420,10 @@ def test_all_candidates_banned_raises_pool_exhausted_not_env_fallback(
|
||||||
assert exc_info.value.pool_total == 2
|
assert exc_info.value.pool_total == 2
|
||||||
assert exc_info.value.banned_for_source == 2
|
assert exc_info.value.banned_for_source == 2
|
||||||
assert exc_info.value.unhealthy_or_disabled == 0
|
assert exc_info.value.unhealthy_or_disabled == 0
|
||||||
errors = [rec for rec in caplog.records if rec.levelname == "ERROR"]
|
# fail-closed без здорового узла — штатный исход скрапинга, а не инцидент;
|
||||||
assert any("fail-closed" in rec.message.lower() for rec in errors)
|
# понижено до warning, чтобы не шуметь в GlitchTip (было logger.error).
|
||||||
|
warnings = [rec for rec in caplog.records if rec.levelname == "WARNING"]
|
||||||
|
assert any("fail-closed" in rec.message.lower() for rec in warnings)
|
||||||
# НЕ должно быть "обход пула" / "static-fallback" в логах — env не тронут.
|
# НЕ должно быть "обход пула" / "static-fallback" в логах — env не тронут.
|
||||||
assert not any("static-fallback" in rec.message for rec in caplog.records)
|
assert not any("static-fallback" in rec.message for rec in caplog.records)
|
||||||
|
|
||||||
|
|
@ -429,8 +431,10 @@ def test_all_candidates_banned_raises_pool_exhausted_not_env_fallback(
|
||||||
def test_exhausted_and_empty_pool_log_texts_are_distinct(
|
def test_exhausted_and_empty_pool_log_texts_are_distinct(
|
||||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Регрессия на замечание ревью: "пуст" и "все отсеяны" — РАЗНЫЕ формулировки И
|
"""Регрессия на замечание ревью: "пуст" и "все отсеяны" — РАЗНЫЕ формулировки,
|
||||||
разные уровни (WARNING vs ERROR), иначе их нельзя различить в логах/алертах."""
|
иначе их нельзя различить в логах/алертах. Оба сценария — штатный исход
|
||||||
|
скрапинга, поэтому оба теперь warning (было WARNING vs ERROR), различимость
|
||||||
|
держится на тексте, не на уровне."""
|
||||||
now = datetime.now(UTC)
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
with caplog.at_level("WARNING"):
|
with caplog.at_level("WARNING"):
|
||||||
|
|
@ -452,8 +456,8 @@ def test_exhausted_and_empty_pool_log_texts_are_distinct(
|
||||||
exhausted_messages = {rec.levelname: rec.message for rec in caplog.records}
|
exhausted_messages = {rec.levelname: rec.message for rec in caplog.records}
|
||||||
|
|
||||||
assert "ERROR" not in empty_pool_messages
|
assert "ERROR" not in empty_pool_messages
|
||||||
assert "ERROR" in exhausted_messages
|
assert "ERROR" not in exhausted_messages
|
||||||
assert empty_pool_messages.get("WARNING") != exhausted_messages.get("ERROR")
|
assert empty_pool_messages.get("WARNING") != exhausted_messages.get("WARNING")
|
||||||
|
|
||||||
|
|
||||||
def test_disabled_proxy_raises_pool_exhausted() -> None:
|
def test_disabled_proxy_raises_pool_exhausted() -> None:
|
||||||
|
|
|
||||||
|
|
@ -348,7 +348,9 @@ async def test_backfill_abort_log_has_no_ip_rate_limited_literal(caplog: Any) ->
|
||||||
patch(_RESOLVE_PROXY_URL, MagicMock(return_value="http://test-proxy.local:8080")),
|
patch(_RESOLVE_PROXY_URL, MagicMock(return_value="http://test-proxy.local:8080")),
|
||||||
patch(_FETCH, mock_fetch),
|
patch(_FETCH, mock_fetch),
|
||||||
patch(_SLEEP, new_callable=AsyncMock),
|
patch(_SLEEP, new_callable=AsyncMock),
|
||||||
caplog.at_level("ERROR"),
|
# серия подтверждённых блоков площадки -- штатный исход скрапинга, не
|
||||||
|
# инцидент; понижено до warning, чтобы не шуметь в GlitchTip.
|
||||||
|
caplog.at_level("WARNING"),
|
||||||
):
|
):
|
||||||
await run_avito_detail_backfill(
|
await run_avito_detail_backfill(
|
||||||
db, run_id=3, params={"batch_size": 10, "budget_sec": 3600, "max_consecutive_blocks": 5}
|
db, run_id=3, params={"batch_size": 10, "budget_sec": 3600, "max_consecutive_blocks": 5}
|
||||||
|
|
@ -1354,7 +1356,9 @@ async def test_backfill_ratio_abort_log_names_the_ratio_not_the_streak(caplog: A
|
||||||
patch(_FETCH, mock_fetch),
|
patch(_FETCH, mock_fetch),
|
||||||
patch(_SAVE, return_value=True),
|
patch(_SAVE, return_value=True),
|
||||||
patch(_SLEEP, new_callable=AsyncMock),
|
patch(_SLEEP, new_callable=AsyncMock),
|
||||||
caplog.at_level("ERROR"),
|
# ABORT по доле блоков -- штатный исход скрапинга, не инцидент; понижено
|
||||||
|
# до warning, чтобы не шуметь в GlitchTip.
|
||||||
|
caplog.at_level("WARNING"),
|
||||||
):
|
):
|
||||||
result = await run_avito_detail_backfill(
|
result = await run_avito_detail_backfill(
|
||||||
db, run_id=110, params={"batch_size": total, "budget_sec": 3600}
|
db, run_id=110, params={"batch_size": total, "budget_sec": 3600}
|
||||||
|
|
@ -1396,7 +1400,9 @@ async def test_backfill_safety_net_abort_log_names_the_streak(caplog: Any) -> No
|
||||||
patch(_FETCH, mock_fetch),
|
patch(_FETCH, mock_fetch),
|
||||||
patch(_SAVE, return_value=True),
|
patch(_SAVE, return_value=True),
|
||||||
patch(_SLEEP, new_callable=AsyncMock),
|
patch(_SLEEP, new_callable=AsyncMock),
|
||||||
caplog.at_level("ERROR"),
|
# ABORT по safety-net серии -- штатный исход скрапинга, не инцидент;
|
||||||
|
# понижено до warning, чтобы не шуметь в GlitchTip.
|
||||||
|
caplog.at_level("WARNING"),
|
||||||
):
|
):
|
||||||
result = await run_avito_detail_backfill(
|
result = await run_avito_detail_backfill(
|
||||||
db, run_id=111, params={"batch_size": total, "budget_sec": 3600}
|
db, run_id=111, params={"batch_size": total, "budget_sec": 3600}
|
||||||
|
|
|
||||||
|
|
@ -287,7 +287,9 @@ async def test_backfill_abort_log_has_no_qrator_literal(caplog: pytest.LogCaptur
|
||||||
patch(_BROWSER_FETCHER, mock_bf_cls),
|
patch(_BROWSER_FETCHER, mock_bf_cls),
|
||||||
patch(_FETCH, mock_fetch),
|
patch(_FETCH, mock_fetch),
|
||||||
patch(_SLEEP, new_callable=AsyncMock),
|
patch(_SLEEP, new_callable=AsyncMock),
|
||||||
caplog.at_level("ERROR"),
|
# серия подтверждённых блоков площадки -- штатный исход скрапинга, не
|
||||||
|
# инцидент; понижено до warning, чтобы не шуметь в GlitchTip.
|
||||||
|
caplog.at_level("WARNING"),
|
||||||
):
|
):
|
||||||
await run_domclick_detail_backfill(
|
await run_domclick_detail_backfill(
|
||||||
db,
|
db,
|
||||||
|
|
|
||||||
|
|
@ -446,8 +446,9 @@ async def test_verify_session_pool_exhausted_returns_source_unavailable_with_err
|
||||||
) -> None:
|
) -> None:
|
||||||
"""#2825 fail-closed (#2616): пул scrape_proxies исчерпан для cian (все узлы
|
"""#2825 fail-closed (#2616): пул scrape_proxies исчерпан для cian (все узлы
|
||||||
забанены/нездоровы) — session.get НЕ вызывается (никуда не ходим без egress),
|
забанены/нездоровы) — session.get НЕ вызывается (никуда не ходим без egress),
|
||||||
возвращается VERIFY_SOURCE_UNAVAILABLE_SENTINEL, но с ERROR-логом (не warning,
|
возвращается VERIFY_SOURCE_UNAVAILABLE_SENTINEL, с явным логом (не тихое
|
||||||
отдельным от обычного network-error пути) — явная деградация, а не проглатывание."""
|
проглатывание). Штатный исход скрапинга -- warning, не error (было error;
|
||||||
|
понижено, чтобы не шуметь в GlitchTip)."""
|
||||||
from app.services.proxy_egress import ProxyPoolExhaustedError
|
from app.services.proxy_egress import ProxyPoolExhaustedError
|
||||||
|
|
||||||
def _raise(source: str) -> str | None:
|
def _raise(source: str) -> str | None:
|
||||||
|
|
@ -470,8 +471,8 @@ async def test_verify_session_pool_exhausted_returns_source_unavailable_with_err
|
||||||
|
|
||||||
assert result is VERIFY_SOURCE_UNAVAILABLE_SENTINEL
|
assert result is VERIFY_SOURCE_UNAVAILABLE_SENTINEL
|
||||||
mock_session.get.assert_not_called()
|
mock_session.get.assert_not_called()
|
||||||
errors = [rec for rec in caplog.records if rec.levelname == "ERROR"]
|
warnings = [rec for rec in caplog.records if rec.levelname == "WARNING"]
|
||||||
assert any("пул прокси исчерпан" in rec.message.lower() for rec in errors)
|
assert any("пул прокси исчерпан" in rec.message.lower() for rec in warnings)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue