import logging import unittest from testfixtures import LogCapture from twisted.internet import defer from twisted.internet.error import ( ConnectError, ConnectionDone, ConnectionLost, DNSLookupError, TCPTimedOutError, ) from twisted.web.client import ResponseFailed from scrapy.downloadermiddlewares.retry import get_retry_request, RetryMiddleware from scrapy.exceptions import IgnoreRequest from scrapy.http import Request, Response from scrapy.spiders import Spider from scrapy.utils.test import get_crawler class RetryTest(unittest.TestCase): def setUp(self): self.crawler = get_crawler(Spider) self.spider = self.crawler._create_spider('foo') self.mw = RetryMiddleware.from_crawler(self.crawler) self.mw.max_retry_times = 2 def test_priority_adjust(self): req = Request('http://www.scrapytest.org/503') rsp = Response('http://www.scrapytest.org/503', body=b'', status=503) req2 = self.mw.process_response(req, rsp, self.spider) assert req2.priority < req.priority def test_404(self): req = Request('http://www.scrapytest.org/404') rsp = Response('http://www.scrapytest.org/404', body=b'', status=404) # dont retry 404s assert self.mw.process_response(req, rsp, self.spider) is rsp def test_dont_retry(self): req = Request('http://www.scrapytest.org/503', meta={'dont_retry': True}) rsp = Response('http://www.scrapytest.org/503', body=b'', status=503) # first retry r = self.mw.process_response(req, rsp, self.spider) assert r is rsp # Test retry when dont_retry set to False req = Request('http://www.scrapytest.org/503', meta={'dont_retry': False}) rsp = Response('http://www.scrapytest.org/503') # first retry r = self.mw.process_response(req, rsp, self.spider) assert r is rsp def test_dont_retry_exc(self): req = Request('http://www.scrapytest.org/503', meta={'dont_retry': True}) r = self.mw.process_exception(req, DNSLookupError(), self.spider) assert r is None def test_503(self): req = Request('http://www.scrapytest.org/503') rsp = Response('http://www.scrapytest.org/503', body=b'', status=503) # first retry req = self.mw.process_response(req, rsp, self.spider) assert isinstance(req, Request) self.assertEqual(req.meta['retry_times'], 1) # second retry req = self.mw.process_response(req, rsp, self.spider) assert isinstance(req, Request) self.assertEqual(req.meta['retry_times'], 2) # discard it assert self.mw.process_response(req, rsp, self.spider) is rsp assert self.crawler.stats.get_value('retry/max_reached') == 1 assert self.crawler.stats.get_value('retry/reason_count/503 Service Unavailable') == 2 assert self.crawler.stats.get_value('retry/count') == 2 def test_twistederrors(self): exceptions = [ ConnectError, ConnectionDone, ConnectionLost, ConnectionRefusedError, defer.TimeoutError, DNSLookupError, ResponseFailed, TCPTimedOutError, TimeoutError, ] for exc in exceptions: req = Request(f'http://www.scrapytest.org/{exc.__name__}') self._test_retry_exception(req, exc('foo')) stats = self.crawler.stats assert stats.get_value('retry/max_reached') == len(exceptions) assert stats.get_value('retry/count') == len(exceptions) * 2 assert stats.get_value('retry/reason_count/twisted.internet.defer.TimeoutError') == 2 def _test_retry_exception(self, req, exception): # first retry req = self.mw.process_exception(req, exception, self.spider) assert isinstance(req, Request) self.assertEqual(req.meta['retry_times'], 1) # second retry req = self.mw.process_exception(req, exception, self.spider) assert isinstance(req, Request) self.assertEqual(req.meta['retry_times'], 2) # discard it req = self.mw.process_exception(req, exception, self.spider) self.assertEqual(req, None) class MaxRetryTimesTest(unittest.TestCase): invalid_url = 'http://www.scrapytest.org/invalid_url' def get_spider_and_middleware(self, settings=None): crawler = get_crawler(Spider, settings or {}) spider = crawler._create_spider('foo') middleware = RetryMiddleware.from_crawler(crawler) return spider, middleware def test_with_settings_zero(self): max_retry_times = 0 settings = {'RETRY_TIMES': max_retry_times} spider, middleware = self.get_spider_and_middleware(settings) req = Request(self.invalid_url) self._test_retry( req, DNSLookupError('foo'), max_retry_times, spider=spider, middleware=middleware, ) def test_with_metakey_zero(self): max_retry_times = 0 spider, middleware = self.get_spider_and_middleware() meta = {'max_retry_times': max_retry_times} req = Request(self.invalid_url, meta=meta) self._test_retry( req, DNSLookupError('foo'), max_retry_times, spider=spider, middleware=middleware, ) def test_without_metakey(self): max_retry_times = 5 settings = {'RETRY_TIMES': max_retry_times} spider, middleware = self.get_spider_and_middleware(settings) req = Request(self.invalid_url) self._test_retry( req, DNSLookupError('foo'), max_retry_times, spider=spider, middleware=middleware, ) def test_with_metakey_greater(self): meta_max_retry_times = 3 middleware_max_retry_times = 2 req1 = Request(self.invalid_url, meta={'max_retry_times': meta_max_retry_times}) req2 = Request(self.invalid_url) settings = {'RETRY_TIMES': middleware_max_retry_times} spider, middleware = self.get_spider_and_middleware(settings) self._test_retry( req1, DNSLookupError('foo'), meta_max_retry_times, spider=spider, middleware=middleware, ) self._test_retry( req2, DNSLookupError('foo'), middleware_max_retry_times, spider=spider, middleware=middleware, ) def test_with_metakey_lesser(self): meta_max_retry_times = 4 middleware_max_retry_times = 5 req1 = Request(self.invalid_url, meta={'max_retry_times': meta_max_retry_times}) req2 = Request(self.invalid_url) settings = {'RETRY_TIMES': middleware_max_retry_times} spider, middleware = self.get_spider_and_middleware(settings) self._test_retry( req1, DNSLookupError('foo'), meta_max_retry_times, spider=spider, middleware=middleware, ) self._test_retry( req2, DNSLookupError('foo'), middleware_max_retry_times, spider=spider, middleware=middleware, ) def test_with_dont_retry(self): max_retry_times = 4 spider, middleware = self.get_spider_and_middleware() meta = { 'max_retry_times': max_retry_times, 'dont_retry': True, } req = Request(self.invalid_url, meta=meta) self._test_retry( req, DNSLookupError('foo'), 0, spider=spider, middleware=middleware, ) def _test_retry( self, req, exception, max_retry_times, spider=None, middleware=None, ): spider = spider or self.spider middleware = middleware or self.mw for i in range(0, max_retry_times): req = middleware.process_exception(req, exception, spider) assert isinstance(req, Request) # discard it req = middleware.process_exception(req, exception, spider) self.assertEqual(req, None) class GetRetryRequestTest(unittest.TestCase): def get_spider(self, settings=None): crawler = get_crawler(Spider, settings or {}) return crawler._create_spider('foo') def test_basic_usage(self): request = Request('https://example.com') spider = self.get_spider() with LogCapture() as log: new_request = get_retry_request( request, spider=spider, ) self.assertIsInstance(new_request, Request) self.assertNotEqual(new_request, request) self.assertEqual(new_request.dont_filter, True) expected_retry_times = 1 self.assertEqual(new_request.meta['retry_times'], expected_retry_times) self.assertEqual(new_request.priority, -1) expected_reason = "unspecified" for stat in ('retry/count', f'retry/reason_count/{expected_reason}'): self.assertEqual(spider.crawler.stats.get_value(stat), 1) log.check_present( ( "scrapy.downloadermiddlewares.retry", "DEBUG", f"Retrying {request} (failed {expected_retry_times} times): " f"{expected_reason}", ) ) def test_max_retries_reached(self): request = Request('https://example.com') spider = self.get_spider() max_retry_times = 0 with LogCapture() as log: new_request = get_retry_request( request, spider=spider, max_retry_times=max_retry_times, ) self.assertEqual(new_request, None) self.assertEqual( spider.crawler.stats.get_value('retry/max_reached'), 1 ) failure_count = max_retry_times + 1 expected_reason = "unspecified" log.check_present( ( "scrapy.downloadermiddlewares.retry", "ERROR", f"Gave up retrying {request} (failed {failure_count} times): " f"{expected_reason}", ) ) def test_one_retry(self): request = Request('https://example.com') spider = self.get_spider() with LogCapture() as log: new_request = get_retry_request( request, spider=spider, max_retry_times=1, ) self.assertIsInstance(new_request, Request) self.assertNotEqual(new_request, request) self.assertEqual(new_request.dont_filter, True) expected_retry_times = 1 self.assertEqual(new_request.meta['retry_times'], expected_retry_times) self.assertEqual(new_request.priority, -1) expected_reason = "unspecified" for stat in ('retry/count', f'retry/reason_count/{expected_reason}'): self.assertEqual(spider.crawler.stats.get_value(stat), 1) log.check_present( ( "scrapy.downloadermiddlewares.retry", "DEBUG", f"Retrying {request} (failed {expected_retry_times} times): " f"{expected_reason}", ) ) def test_two_retries(self): spider = self.get_spider() request = Request('https://example.com') new_request = request max_retry_times = 2 for index in range(max_retry_times): with LogCapture() as log: new_request = get_retry_request( new_request, spider=spider, max_retry_times=max_retry_times, ) self.assertIsInstance(new_request, Request) self.assertNotEqual(new_request, request) self.assertEqual(new_request.dont_filter, True) expected_retry_times = index + 1 self.assertEqual(new_request.meta['retry_times'], expected_retry_times) self.assertEqual(new_request.priority, -expected_retry_times) expected_reason = "unspecified" for stat in ('retry/count', f'retry/reason_count/{expected_reason}'): value = spider.crawler.stats.get_value(stat) self.assertEqual(value, expected_retry_times) log.check_present( ( "scrapy.downloadermiddlewares.retry", "DEBUG", f"Retrying {request} (failed {expected_retry_times} times): " f"{expected_reason}", ) ) with LogCapture() as log: new_request = get_retry_request( new_request, spider=spider, max_retry_times=max_retry_times, ) self.assertEqual(new_request, None) self.assertEqual( spider.crawler.stats.get_value('retry/max_reached'), 1 ) failure_count = max_retry_times + 1 expected_reason = "unspecified" log.check_present( ( "scrapy.downloadermiddlewares.retry", "ERROR", f"Gave up retrying {request} (failed {failure_count} times): " f"{expected_reason}", ) ) def test_no_spider(self): request = Request('https://example.com') with self.assertRaises(TypeError): get_retry_request(request) # pylint: disable=missing-kwoa def test_max_retry_times_setting(self): max_retry_times = 0 spider = self.get_spider({'RETRY_TIMES': max_retry_times}) request = Request('https://example.com') new_request = get_retry_request( request, spider=spider, ) self.assertEqual(new_request, None) def test_max_retry_times_meta(self): max_retry_times = 0 spider = self.get_spider({'RETRY_TIMES': max_retry_times + 1}) meta = {'max_retry_times': max_retry_times} request = Request('https://example.com', meta=meta) new_request = get_retry_request( request, spider=spider, ) self.assertEqual(new_request, None) def test_max_retry_times_argument(self): max_retry_times = 0 spider = self.get_spider({'RETRY_TIMES': max_retry_times + 1}) meta = {'max_retry_times': max_retry_times + 1} request = Request('https://example.com', meta=meta) new_request = get_retry_request( request, spider=spider, max_retry_times=max_retry_times, ) self.assertEqual(new_request, None) def test_priority_adjust_setting(self): priority_adjust = 1 spider = self.get_spider({'RETRY_PRIORITY_ADJUST': priority_adjust}) request = Request('https://example.com') new_request = get_retry_request( request, spider=spider, ) self.assertEqual(new_request.priority, priority_adjust) def test_priority_adjust_argument(self): priority_adjust = 1 spider = self.get_spider({'RETRY_PRIORITY_ADJUST': priority_adjust + 1}) request = Request('https://example.com') new_request = get_retry_request( request, spider=spider, priority_adjust=priority_adjust, ) self.assertEqual(new_request.priority, priority_adjust) def test_log_extra_retry_success(self): request = Request('https://example.com') spider = self.get_spider() with LogCapture(attributes=('spider',)) as log: get_retry_request( request, spider=spider, ) log.check_present(spider) def test_log_extra_retries_exceeded(self): request = Request('https://example.com') spider = self.get_spider() with LogCapture(attributes=('spider',)) as log: get_retry_request( request, spider=spider, max_retry_times=0, ) log.check_present(spider) def test_reason_string(self): request = Request('https://example.com') spider = self.get_spider() expected_reason = 'because' with LogCapture() as log: get_retry_request( request, spider=spider, reason=expected_reason, ) expected_retry_times = 1 for stat in ('retry/count', f'retry/reason_count/{expected_reason}'): self.assertEqual(spider.crawler.stats.get_value(stat), 1) log.check_present( ( "scrapy.downloadermiddlewares.retry", "DEBUG", f"Retrying {request} (failed {expected_retry_times} times): " f"{expected_reason}", ) ) def test_reason_builtin_exception(self): request = Request('https://example.com') spider = self.get_spider() expected_reason = NotImplementedError() expected_reason_string = 'builtins.NotImplementedError' with LogCapture() as log: get_retry_request( request, spider=spider, reason=expected_reason, ) expected_retry_times = 1 stat = spider.crawler.stats.get_value( f'retry/reason_count/{expected_reason_string}' ) self.assertEqual(stat, 1) log.check_present( ( "scrapy.downloadermiddlewares.retry", "DEBUG", f"Retrying {request} (failed {expected_retry_times} times): " f"{expected_reason}", ) ) def test_reason_builtin_exception_class(self): request = Request('https://example.com') spider = self.get_spider() expected_reason = NotImplementedError expected_reason_string = 'builtins.NotImplementedError' with LogCapture() as log: get_retry_request( request, spider=spider, reason=expected_reason, ) expected_retry_times = 1 stat = spider.crawler.stats.get_value( f'retry/reason_count/{expected_reason_string}' ) self.assertEqual(stat, 1) log.check_present( ( "scrapy.downloadermiddlewares.retry", "DEBUG", f"Retrying {request} (failed {expected_retry_times} times): " f"{expected_reason}", ) ) def test_reason_custom_exception(self): request = Request('https://example.com') spider = self.get_spider() expected_reason = IgnoreRequest() expected_reason_string = 'scrapy.exceptions.IgnoreRequest' with LogCapture() as log: get_retry_request( request, spider=spider, reason=expected_reason, ) expected_retry_times = 1 stat = spider.crawler.stats.get_value( f'retry/reason_count/{expected_reason_string}' ) self.assertEqual(stat, 1) log.check_present( ( "scrapy.downloadermiddlewares.retry", "DEBUG", f"Retrying {request} (failed {expected_retry_times} times): " f"{expected_reason}", ) ) def test_reason_custom_exception_class(self): request = Request('https://example.com') spider = self.get_spider() expected_reason = IgnoreRequest expected_reason_string = 'scrapy.exceptions.IgnoreRequest' with LogCapture() as log: get_retry_request( request, spider=spider, reason=expected_reason, ) expected_retry_times = 1 stat = spider.crawler.stats.get_value( f'retry/reason_count/{expected_reason_string}' ) self.assertEqual(stat, 1) log.check_present( ( "scrapy.downloadermiddlewares.retry", "DEBUG", f"Retrying {request} (failed {expected_retry_times} times): " f"{expected_reason}", ) ) def test_custom_logger(self): logger = logging.getLogger("custom-logger") request = Request("https://example.com") spider = self.get_spider() expected_reason = "because" with LogCapture() as log: get_retry_request( request, spider=spider, reason=expected_reason, logger=logger, ) log.check_present( ( "custom-logger", "DEBUG", f"Retrying {request} (failed 1 times): {expected_reason}", ) ) def test_custom_stats_key(self): request = Request("https://example.com") spider = self.get_spider() expected_reason = "because" stats_key = "custom_retry" get_retry_request( request, spider=spider, reason=expected_reason, stats_base_key=stats_key, ) for stat in (f"{stats_key}/count", f"{stats_key}/reason_count/{expected_reason}"): self.assertEqual(spider.crawler.stats.get_value(stat), 1) if __name__ == "__main__": unittest.main()