diff --git a/scrapy/commands/check.py b/scrapy/commands/check.py index 579bbac51..eaf16e031 100644 --- a/scrapy/commands/check.py +++ b/scrapy/commands/check.py @@ -1,11 +1,14 @@ from collections import defaultdict from functools import wraps +from unittest import TextTestRunner +from scrapy import signals from scrapy.command import ScrapyCommand from scrapy.contracts import ContractsManager from scrapy.utils.misc import load_object from scrapy.utils.spider import iterate_spider_output from scrapy.utils.conf import build_component_list +from scrapy.xlib.pydispatch import dispatcher def _generate(cb): @@ -31,6 +34,8 @@ class Command(ScrapyCommand): ScrapyCommand.add_options(self, parser) parser.add_option("-l", "--list", dest="list", action="store_true", help="only list contracts, without checking them") + parser.add_option("-v", "--verbose", dest="verbose", default=1, action="count", + help="print all contract hooks") def run(self, args, opts): # load contracts @@ -39,6 +44,7 @@ class Command(ScrapyCommand): self.settings['SPIDER_CONTRACTS'], ) self.conman = ContractsManager([load_object(c) for c in contracts]) + self.results = TextTestRunner(verbosity=opts.verbose)._makeResult() # contract requests contract_reqs = defaultdict(list) @@ -61,6 +67,8 @@ class Command(ScrapyCommand): for method in sorted(methods): print ' * %s' % method else: + dispatcher.connect(self.results.printErrors, + signals.engine_stopped) self.crawler.start() def get_requests(self, spider): @@ -69,7 +77,7 @@ class Command(ScrapyCommand): for key, value in vars(type(spider)).items(): if callable(value) and value.__doc__: bound_method = value.__get__(spider, type(spider)) - request = self.conman.from_method(bound_method) + request = self.conman.from_method(bound_method, self.results) if request: request.callback = _generate(request.callback) diff --git a/scrapy/contracts/__init__.py b/scrapy/contracts/__init__.py index fac226822..cfccfe0c5 100644 --- a/scrapy/contracts/__init__.py +++ b/scrapy/contracts/__init__.py @@ -1,10 +1,11 @@ +import sys import re from functools import wraps +from unittest import TestCase from scrapy.http import Request from scrapy.utils.spider import iterate_spider_output from scrapy.utils.python import get_spec -from scrapy.exceptions import ContractFail class ContractsManager(object): @@ -27,7 +28,7 @@ class ContractsManager(object): return contracts - def from_method(self, method, fail=False): + def from_method(self, method, results): contracts = self.extract_contracts(method) if contracts: # calculate request args @@ -43,9 +44,9 @@ class ContractsManager(object): # execute pre and post hooks in order for contract in reversed(contracts): - request = contract.add_pre_hook(request, fail) + request = contract.add_pre_hook(request, results) for contract in contracts: - request = contract.add_post_hook(request, fail) + request = contract.add_post_hook(request, results) return request @@ -54,49 +55,67 @@ class Contract(object): """ Abstract class for contracts """ def __init__(self, method, *args): - self.method = method + self.testcase_pre = self.create_testcase(method, 'pre-hook') + self.testcase_post = self.create_testcase(method, 'post-hook') self.args = args - def add_pre_hook(self, request, fail=False): - cb = request.callback + def create_testcase(self, method, hook): + spider = method.__self__.name - @wraps(cb) - def wrapper(response): - try: - self.pre_process(response) - except ContractFail as e: - if fail: - raise + class ContractTestCase(TestCase): + def __str__(_self): + return "[%s] %s (@%s %s)" % (spider, method.__name__, self.name, hook) + + name = '%s_%s' % (spider, method.__name__) + setattr(ContractTestCase, name, lambda x: x) + return ContractTestCase(name) + + def add_pre_hook(self, request, results): + if hasattr(self, 'pre_process'): + cb = request.callback + + @wraps(cb) + def wrapper(response): + try: + results.startTest(self.testcase_pre) + self.pre_process(response) + results.stopTest(self.testcase_pre) + except AssertionError: + results.addFailure(self.testcase_pre, sys.exc_info()) + except Exception: + results.addError(self.testcase_pre, sys.exc_info()) else: - print e.format(self.method) - return list(iterate_spider_output(cb(response))) + results.addSuccess(self.testcase_pre) + finally: + return list(iterate_spider_output(cb(response))) + + request.callback = wrapper - request.callback = wrapper return request - def add_post_hook(self, request, fail=False): - cb = request.callback + def add_post_hook(self, request, results): + if hasattr(self, 'post_process'): + cb = request.callback - @wraps(cb) - def wrapper(response): - output = list(iterate_spider_output(cb(response))) - try: - self.post_process(output) - except ContractFail as e: - if fail: - raise + @wraps(cb) + def wrapper(response): + try: + output = list(iterate_spider_output(cb(response))) + results.startTest(self.testcase_post) + self.post_process(output) + results.stopTest(self.testcase_post) + except AssertionError: + results.addFailure(self.testcase_post, sys.exc_info()) + except Exception: + results.addError(self.testcase_post, sys.exc_info()) else: - print e.format(self.method) - return output + results.addSuccess(self.testcase_post) + finally: + return output + + request.callback = wrapper - request.callback = wrapper return request def adjust_request_args(self, args): return args - - def pre_process(self, response): - pass - - def post_process(self, output): - pass diff --git a/scrapy/exceptions.py b/scrapy/exceptions.py index f41403bb4..c22ba0204 100644 --- a/scrapy/exceptions.py +++ b/scrapy/exceptions.py @@ -52,7 +52,4 @@ class ScrapyDeprecationWarning(Warning): class ContractFail(AssertionError): """Error raised in case of a failing contract""" - - def format(self, method): - return '[FAILED] %s:%s\n>>> %s\n' % \ - (method.im_class.name, method.__name__, self) + pass diff --git a/scrapy/tests/test_contracts.py b/scrapy/tests/test_contracts.py index da4eaf3a4..7e89de25f 100644 --- a/scrapy/tests/test_contracts.py +++ b/scrapy/tests/test_contracts.py @@ -1,9 +1,10 @@ +from unittest import TextTestRunner + from twisted.trial import unittest from scrapy.spider import BaseSpider from scrapy.http import Request from scrapy.item import Item, Field -from scrapy.exceptions import ContractFail from scrapy.contracts import ContractsManager from scrapy.contracts.default import ( UrlContract, @@ -71,54 +72,69 @@ class TestSpider(BaseSpider): class ContractsManagerTest(unittest.TestCase): contracts = [UrlContract, ReturnsContract, ScrapesContract] + def setUp(self): + self.conman = ContractsManager(self.contracts) + self.results = TextTestRunner()._makeResult() + self.results.stream = None + + def should_succeed(self): + self.assertFalse(self.results.failures) + self.assertFalse(self.results.errors) + + def should_fail(self): + self.assertTrue(self.results.failures) + self.assertFalse(self.results.errors) + def test_contracts(self): - conman = ContractsManager(self.contracts) + spider = TestSpider() # extract contracts correctly - contracts = conman.extract_contracts(TestSpider.returns_request) + contracts = self.conman.extract_contracts(spider.returns_request) self.assertEqual(len(contracts), 2) self.assertEqual(frozenset(map(type, contracts)), frozenset([UrlContract, ReturnsContract])) # returns request for valid method - request = conman.from_method(TestSpider.returns_request) + request = self.conman.from_method(spider.returns_request, self.results) self.assertNotEqual(request, None) # no request for missing url - request = conman.from_method(TestSpider.parse_no_url) + request = self.conman.from_method(spider.parse_no_url, self.results) self.assertEqual(request, None) def test_returns(self): - conman = ContractsManager(self.contracts) - spider = TestSpider() response = ResponseMock() # returns_item - request = conman.from_method(spider.returns_item, fail=True) + request = self.conman.from_method(spider.returns_item, self.results) output = request.callback(response) self.assertEqual(map(type, output), [TestItem]) + self.should_succeed() # returns_request - request = conman.from_method(spider.returns_request, fail=True) + request = self.conman.from_method(spider.returns_request, self.results) output = request.callback(response) self.assertEqual(map(type, output), [Request]) + self.should_succeed() # returns_fail - request = conman.from_method(spider.returns_fail, fail=True) - self.assertRaises(ContractFail, request.callback, response) + request = self.conman.from_method(spider.returns_fail, self.results) + request.callback(response) + self.should_fail() def test_scrapes(self): - conman = ContractsManager(self.contracts) - spider = TestSpider() response = ResponseMock() # scrapes_item_ok - request = conman.from_method(spider.scrapes_item_ok, fail=True) + request = self.conman.from_method(spider.scrapes_item_ok, self.results) output = request.callback(response) self.assertEqual(map(type, output), [TestItem]) + self.should_succeed() # scrapes_item_fail - request = conman.from_method(spider.scrapes_item_fail, fail=True) - self.assertRaises(ContractFail, request.callback, response) + request = self.conman.from_method(spider.scrapes_item_fail, + self.results) + request.callback(response) + self.should_fail()