"""Контракт progress_cb у парсеров: отчёт по страницам и остановка по запросу. Сеть не трогаем — подменяем HTTP-клиент парсера заглушкой. Проверяем то, на что опирается заливка: воркер видит рост выборки и может остановить парсер, не дожидаясь конца (отмена из админки, исчерпанный бюджет времени таска). """ from typing import Any from cyberleninka import CyberLeninkaParser class FakeResponse: def __init__(self, payload: dict[str, Any]) -> None: self._payload = payload def raise_for_status(self) -> None: pass def json(self) -> dict[str, Any]: return self._payload class FakeClient: """Отдаёт бесконечные полные страницы по 10 статей — как большая выдача.""" def __init__(self) -> None: self.calls = 0 def post(self, url: str, json: dict[str, Any]) -> FakeResponse: self.calls += 1 start = json["from"] return FakeResponse({ "articles": [{"id": start + i, "name": f"статья {start + i}"} for i in range(10)] }) def _parser(monkeypatch) -> CyberLeninkaParser: p = CyberLeninkaParser() p.client = FakeClient() # type: ignore[assignment] monkeypatch.setattr("cyberleninka.RATE_LIMIT_DELAY", 0) return p def test_progress_cb_reports_growing_count(monkeypatch): p = _parser(monkeypatch) seen: list[int] = [] docs = p.fetch(query="x", limit=30, progress_cb=lambda n: seen.append(n) or True) assert len(docs) == 30 assert seen == [10, 20, 30] def test_progress_cb_false_stops_fetch_early(monkeypatch): p = _parser(monkeypatch) # Останавливаем после второй страницы — как отмена прогона из админки docs = p.fetch(query="x", limit=1000, progress_cb=lambda n: n < 20) assert len(docs) == 20 assert p.client.calls == 2 # type: ignore[attr-defined] def test_fetch_works_without_callback(monkeypatch): """Скрипты заливки зовут парсеры без progress_cb — поведение прежнее.""" p = _parser(monkeypatch) assert len(p.fetch(query="x", limit=20)) == 20