from __future__ import annotations import sys from unittest.mock import Mock, call import pytest from twisted.internet import defer from scrapy.core.engine import ExecutionEngine from scrapy.http import Request, Response from scrapy.utils.defer import maybe_deferred_to_future from scrapy.utils.spider import DefaultSpider from scrapy.utils.test import get_crawler from tests.utils.decorators import coroutine_test @pytest.fixture def engine() -> ExecutionEngine: crawler = get_crawler(DefaultSpider) engine = ExecutionEngine(crawler, lambda _: None) engine.downloader.close() engine.downloader = Mock() engine._slot = Mock() engine._slot.inprogress = set() return engine class TestEngineDownloadAsync: """Test cases for ExecutionEngine.download_async().""" @staticmethod async def _download(engine: ExecutionEngine, request: Request) -> Response: return await engine.download_async(request) @coroutine_test async def test_download_async_success(self, engine): """Test basic successful async download of a request.""" request = Request("http://example.com") response = Response("http://example.com", body=b"test body") engine.spider = Mock() engine.downloader.fetch.return_value = defer.succeed(response) engine._slot.add_request = Mock() engine._slot.remove_request = Mock() result = await self._download(engine, request) assert result == response engine._slot.add_request.assert_called_once_with(request) engine._slot.remove_request.assert_called_once_with(request) engine.downloader.fetch.assert_called_once_with(request) @coroutine_test async def test_download_async_redirect(self, engine): """Test async download with a redirect request.""" original_request = Request("http://example.com") redirect_request = Request("http://example.com/redirect") final_response = Response("http://example.com/redirect", body=b"redirected") # First call returns redirect request, second call returns final response engine.downloader.fetch.side_effect = [ defer.succeed(redirect_request), defer.succeed(final_response), ] engine.spider = Mock() engine._slot.add_request = Mock() engine._slot.remove_request = Mock() result = await self._download(engine, original_request) assert result == final_response assert engine.downloader.fetch.call_count == 2 engine._slot.add_request.assert_has_calls( [call(original_request), call(redirect_request)] ) engine._slot.remove_request.assert_has_calls( [call(original_request), call(redirect_request)] ) @coroutine_test async def test_download_async_many_redirects(self, engine): """A long chain of requests being replaced by new ones is handled iteratively, without hitting the recursion limit.""" count = sys.getrecursionlimit() * 2 requests = [Request(f"http://example.com/{i}") for i in range(count)] final_response = Response("http://example.com/final", body=b"done") engine.downloader.fetch.side_effect = [ *(defer.succeed(request) for request in requests[1:]), defer.succeed(final_response), ] engine.spider = Mock() engine._slot.add_request = Mock() engine._slot.remove_request = Mock() result = await self._download(engine, requests[0]) assert result == final_response assert engine.downloader.fetch.call_count == count @coroutine_test async def test_download_async_no_spider(self, engine): """Test async download attempt when no spider is available.""" request = Request("http://example.com") engine.spider = None with pytest.raises(RuntimeError, match="No open spider to crawl:"): await self._download(engine, request) @coroutine_test async def test_download_async_failure(self, engine): """Test async download when the downloader raises an exception.""" request = Request("http://example.com") error = RuntimeError("Download failed") engine.spider = Mock() engine.downloader.fetch.return_value = defer.fail(error) engine._slot.add_request = Mock() engine._slot.remove_request = Mock() with pytest.raises(RuntimeError, match="Download failed"): await self._download(engine, request) engine._slot.add_request.assert_called_once_with(request) engine._slot.remove_request.assert_called_once_with(request) @coroutine_test async def test_download_async_fetch_needs_spider(self, engine): engine._downloader_fetch_needs_spider = True request = Request("http://example.com") response = Response("http://example.com", body=b"test body") engine.spider = Mock() engine.downloader.fetch.return_value = defer.succeed(response) result = await self._download(engine, request) assert result == response engine.downloader.fetch.assert_called_once_with(request, engine.spider) @pytest.mark.filterwarnings("ignore::scrapy.exceptions.ScrapyDeprecationWarning") class TestEngineDownload(TestEngineDownloadAsync): """Test cases for ExecutionEngine.download().""" @staticmethod async def _download(engine: ExecutionEngine, request: Request) -> Response: return await maybe_deferred_to_future(engine.download(request))