scrapy/tests/test_spidermiddleware.py

345 lines
12 KiB
Python

import logging
from testfixtures import LogCapture
from twisted.trial.unittest import TestCase
from twisted.internet import defer
from scrapy.spiders import Spider
from scrapy.item import Item, Field
from scrapy.http import Request
from scrapy.utils.test import get_crawler
from tests.mockserver import MockServer
class TestItem(Item):
value = Field()
class LocalhostSpider(Spider):
start_urls = ['http://localhost:8998'] # tests.mockserver.MockServer
# ================================================================================
# exceptions from a spider's parse method
class BaseExceptionFromParseMethodSpider(LocalhostSpider):
custom_settings = {
'SPIDER_MIDDLEWARES': {'tests.test_spidermiddleware.CatchExceptionMiddleware': 540}
}
class NotAGeneratorSpider(BaseExceptionFromParseMethodSpider):
""" return value is NOT a generator """
name = 'not_a_generator'
def parse(self, response):
raise AssertionError
class GeneratorErrorBeforeItemsSpider(BaseExceptionFromParseMethodSpider):
""" return value is a generator; the exception is raised
before the items are yielded: no items should be scraped """
name = 'generator_error_before_items'
def parse(self, response):
raise ValueError
for i in range(3):
yield {'value': i}
class GeneratorErrorAfterItemsSpider(BaseExceptionFromParseMethodSpider):
""" return value is a generator; the exception is raised
after the items are yielded: 3 items should be scraped """
name = 'generator_error_after_items'
def parse(self, response):
for i in range(3):
yield {'value': i}
raise FloatingPointError
class CatchExceptionMiddleware(object):
def process_spider_exception(self, response, exception, spider):
""" catch an exception and log it """
logging.warn('{} exception caught'.format(exception.__class__.__name__))
return None
# ================================================================================
# exception from a previous middleware's process_spider_input method
# process_spider_input is not expected to return an iterable, so there are no
# separate tests for generator/non-generator implementations
class FromPreviousMiddlewareInputSpider(LocalhostSpider):
name = 'not_a_generator_from_previous_middleware_input'
custom_settings = {
'SPIDER_MIDDLEWARES': {
# engine side
'tests.test_spidermiddleware.CatchExceptionMiddleware': 540,
'tests.test_spidermiddleware.RaiseExceptionOnInputMiddleware': 545,
# spider side
}
}
def parse(self, response):
return None
class RaiseExceptionOnInputMiddleware(object):
def process_spider_input(self, response, spider):
raise LookupError
# ================================================================================
# exception from a previous middleware's process_spider_output method (not a generator)
class NotAGeneratorFromPreviousMiddlewareOutputSpider(LocalhostSpider):
name = 'not_a_generator_from_previous_middleware_output'
custom_settings = {
'SPIDER_MIDDLEWARES': {
# engine side
'tests.test_spidermiddleware.CatchExceptionMiddleware': 540,
'tests.test_spidermiddleware.RaiseExceptionOnOutputNotAGeneratorMiddleware': 545,
# spider side
}
}
def parse(self, response):
return [{'value': i} for i in range(3)]
class RaiseExceptionOnOutputNotAGeneratorMiddleware(object):
def process_spider_output(self, response, result, spider):
raise UnicodeError
# ================================================================================
# exception from a previous middleware's process_spider_output method (generator)
class GeneratorFromPreviousMiddlewareOutputSpider(LocalhostSpider):
name = 'generator_from_previous_middleware_output'
custom_settings = {
'SPIDER_MIDDLEWARES': {
# engine side
'tests.test_spidermiddleware.CatchExceptionMiddleware': 540,
'tests.test_spidermiddleware.RaiseExceptionOnOutputGeneratorMiddleware': 545,
# spider side
}
}
def parse(self, response):
return [{'value': i} for i in range(10, 13)]
class RaiseExceptionOnOutputGeneratorMiddleware(object):
def process_spider_output(self, response, result, spider):
for r in result:
yield r
raise NameError
# ================================================================================
# do something useful from the exception handler
class DoSomethingSpider(LocalhostSpider):
name = 'do_something'
custom_settings = {
'SPIDER_MIDDLEWARES': {
# engine side
'tests.test_spidermiddleware.DoSomethingMiddleware': 540,
'tests.test_spidermiddleware.CatchExceptionMiddleware': 545,
# spider side
}
}
def parse(self, response):
yield {'value': response.url}
raise ImportError
class DoSomethingMiddleware(object):
def process_spider_exception(self, response, exception, spider):
return [Request('http://localhost:8998?processed=true'), {'value': 10}, TestItem(value='asdf')]
# ================================================================================
# don't catch _InvalidOutput from scrapy's spider middleware manager
class InvalidReturnValueFromPreviousMiddlewareInputSpider(LocalhostSpider):
name = 'invalid_return_value_from_previous_middleware_input'
custom_settings = {
'SPIDER_MIDDLEWARES': {
# engine side
'tests.test_spidermiddleware.InvalidReturnValueInputMiddleware': 540,
'tests.test_spidermiddleware.CatchExceptionMiddleware': 545,
# spider side
}
}
def parse(self, response):
return None
class InvalidReturnValueInputMiddleware(object):
def process_spider_input(self, response, spider):
return 1.0 # <type 'float'>, not None
class InvalidReturnValueFromPreviousMiddlewareOutputSpider(LocalhostSpider):
name = 'invalid_return_value_from_previous_middleware_output'
custom_settings = {
'SPIDER_MIDDLEWARES': {
# engine side
'tests.test_spidermiddleware.CatchExceptionMiddleware': 540,
'tests.test_spidermiddleware.InvalidReturnValueOutputMiddleware': 545,
# spider side
}
}
def parse(self, response):
return None
class InvalidReturnValueOutputMiddleware(object):
def process_spider_output(self, response, result, spider):
return 1 # <type 'int'>, not an iterable
# ================================================================================
# make sure only non already called process_spider_output methods
# are called if process_spider_exception returns an iterable
class ExecutionChainSpider(LocalhostSpider):
name = 'execution_chain'
custom_settings = {
'SPIDER_MIDDLEWARES': {
# engine side
'tests.test_spidermiddleware.ThirdMiddleware': 540,
'tests.test_spidermiddleware.SecondMiddleware': 541,
'tests.test_spidermiddleware.FirstMiddleware': 542
# spider side
},
}
def parse(self, response):
return None
class FirstMiddleware(object):
def process_spider_output(self, response, result, spider):
for r in result:
if isinstance(r, dict):
r['handled_by_first_middleware'] = True
yield r
def process_spider_exception(self, response, exception, spider):
# log exception, handle control to the next middleware's process_spider_exception
logging.warn('{} exception caught'.format(exception.__class__.__name__))
return None
class SecondMiddleware(object):
def process_spider_output(self, response, result, spider):
for r in result:
if isinstance(r, dict):
r['handled_by_second_middleware'] = True
yield r
raise MemoryError
class ThirdMiddleware(object):
def process_spider_output(self, response, result, spider):
for r in result:
if isinstance(r, dict):
r['handled_by_third_middleware'] = True
yield r
def process_spider_exception(self, response, exception, spider):
# handle control to the next middleware's process_spider_output
return [{'item': i} for i in range(3)]
class TestSpiderMiddleware(TestCase):
def setUp(self):
self.mockserver = MockServer()
self.mockserver.__enter__()
def tearDown(self):
self.mockserver.__exit__(None, None, None)
@defer.inlineCallbacks
def test_process_spider_exception_from_parse_method(self):
# non-generator return value
crawler = get_crawler(NotAGeneratorSpider)
with LogCapture() as log:
yield crawler.crawl()
self.assertIn("AssertionError exception caught", str(log))
self.assertIn("spider_exceptions/AssertionError", str(log))
# generator return value, no items before the error
crawler = get_crawler(GeneratorErrorBeforeItemsSpider)
with LogCapture() as log:
yield crawler.crawl()
self.assertIn("ValueError exception caught", str(log))
self.assertIn("spider_exceptions/ValueError", str(log))
# generator return value, 3 items before the error
crawler = get_crawler(GeneratorErrorAfterItemsSpider)
with LogCapture() as log:
yield crawler.crawl()
self.assertIn("'item_scraped_count': 3", str(log))
self.assertIn("FloatingPointError exception caught", str(log))
self.assertIn("spider_exceptions/FloatingPointError", str(log))
@defer.inlineCallbacks
def test_process_spider_exception_from_previous_middleware_input(self):
crawler = get_crawler(FromPreviousMiddlewareInputSpider)
with LogCapture() as log:
yield crawler.crawl()
self.assertIn("LookupError exception caught", str(log))
@defer.inlineCallbacks
def test_process_spider_exception_from_previous_middleware_output(self):
# non-generator output value
crawler = get_crawler(NotAGeneratorFromPreviousMiddlewareOutputSpider)
with LogCapture() as log:
yield crawler.crawl()
self.assertNotIn("UnicodeError exception caught", str(log))
# generator output value
crawler = get_crawler(GeneratorFromPreviousMiddlewareOutputSpider)
with LogCapture() as log:
yield crawler.crawl()
self.assertIn("'item_scraped_count': 3", str(log))
self.assertIn("NameError exception caught", str(log))
@defer.inlineCallbacks
def test_process_spider_exception_do_something(self):
crawler = get_crawler(DoSomethingSpider)
with LogCapture() as log:
yield crawler.crawl()
self.assertIn("ImportError exception caught", str(log))
self.assertIn("{'value': 10}", str(log))
self.assertIn("{'value': 'asdf'}", str(log))
self.assertIn("{'value': 'http://localhost:8998'}", str(log))
self.assertIn("{'value': 'http://localhost:8998?processed=true'}", str(log))
@defer.inlineCallbacks
def test_process_spider_exception_invalid_return_value_previous_middleware(self):
""" don't catch _InvalidOutput from middleware """
# on middleware's input
crawler1 = get_crawler(InvalidReturnValueFromPreviousMiddlewareInputSpider)
with LogCapture() as log1:
yield crawler1.crawl()
self.assertNotIn("_InvalidOutput exception caught", str(log1))
self.assertIn("'spider_exceptions/_InvalidOutput'", str(log1))
# on middleware's output
crawler2 = get_crawler(InvalidReturnValueFromPreviousMiddlewareOutputSpider)
with LogCapture() as log2:
yield crawler2.crawl()
self.assertNotIn("_InvalidOutput exception caught", str(log2))
self.assertIn("'spider_exceptions/_InvalidOutput'", str(log2))
@defer.inlineCallbacks
def test_process_spider_exception_execution_chain(self):
# on middleware's input
crawler1 = get_crawler(ExecutionChainSpider)
with LogCapture() as log1:
yield crawler1.crawl()
self.assertNotIn("handled_by_first_middleware", str(log1))
self.assertNotIn("handled_by_second_middleware", str(log1))
self.assertIn("MemoryError exception caught", str(log1))
self.assertIn("handled_by_third_middleware", str(log1))