From 94629a1f08eaf34d61d9bca068192b616c56a728 Mon Sep 17 00:00:00 2001 From: Alex Cepoi Date: Sat, 27 Oct 2012 23:26:40 +0200 Subject: [PATCH 1/3] proper unittest-like output for scrapy contracts --- scrapy/commands/check.py | 8 ++++- scrapy/contracts/__init__.py | 56 ++++++++++++++++++++++------------ scrapy/exceptions.py | 5 +-- scrapy/tests/test_contracts.py | 50 +++++++++++++++++++----------- 4 files changed, 77 insertions(+), 42 deletions(-) diff --git a/scrapy/commands/check.py b/scrapy/commands/check.py index 13190e025..95fc78a36 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.runner 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): @@ -39,6 +42,7 @@ class Command(ScrapyCommand): self.settings['SPIDER_CONTRACTS'], ) self.conman = ContractsManager([load_object(c) for c in contracts]) + self.results = TextTestRunner()._makeResult() # contract requests contract_reqs = defaultdict(list) @@ -61,6 +65,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 +75,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..a9c2c2f83 100644 --- a/scrapy/contracts/__init__.py +++ b/scrapy/contracts/__init__.py @@ -1,10 +1,22 @@ +import sys import re from functools import wraps +from unittest.case 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 + + +def create_testcase(method): + name = '%s.%s' % (method.__self__.__class__.__name__, method.__name__) + + class ContractTestCase(TestCase): + def __str__(self): + return "%s (%s)" % (name, method.__self__.name) + + setattr(ContractTestCase, name, lambda x: x) + return ContractTestCase(name) class ContractsManager(object): @@ -27,7 +39,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 +55,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,40 +66,44 @@ class Contract(object): """ Abstract class for contracts """ def __init__(self, method, *args): - self.method = method + self.testcase = create_testcase(method) self.args = args - def add_pre_hook(self, request, fail=False): + def add_pre_hook(self, request, results): cb = request.callback @wraps(cb) def wrapper(response): try: self.pre_process(response) - except ContractFail as e: - if fail: - raise - else: - print e.format(self.method) - return list(iterate_spider_output(cb(response))) + except AssertionError: + results.addFailure(self.testcase, sys.exc_info()) + except Exception: + results.addError(self.testcase, sys.exc_info()) + else: + results.addSuccess(self.testcase) + finally: + return list(iterate_spider_output(cb(response))) request.callback = wrapper return request - def add_post_hook(self, request, fail=False): + def add_post_hook(self, request, results): cb = request.callback @wraps(cb) def wrapper(response): - output = list(iterate_spider_output(cb(response))) try: + output = list(iterate_spider_output(cb(response))) self.post_process(output) - except ContractFail as e: - if fail: - raise - else: - print e.format(self.method) - return output + except AssertionError: + results.addFailure(self.testcase, sys.exc_info()) + except Exception: + results.addError(self.testcase, sys.exc_info()) + else: + results.addSuccess(self.testcase) + finally: + return output request.callback = wrapper return request 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..07210d0f3 100644 --- a/scrapy/tests/test_contracts.py +++ b/scrapy/tests/test_contracts.py @@ -1,9 +1,10 @@ +from unittest.runner 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 test_contracts(self): - conman = ContractsManager(self.contracts) + 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): # extract contracts correctly - contracts = conman.extract_contracts(TestSpider.returns_request) + contracts = self.conman.extract_contracts(TestSpider.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(TestSpider.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(TestSpider.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() From c6fad057568e9ba4178e2b161d8676afcef6e27d Mon Sep 17 00:00:00 2001 From: Alex Cepoi Date: Sun, 28 Oct 2012 17:53:38 +0100 Subject: [PATCH 2/3] scrapy contracts verbose option --- scrapy/commands/check.py | 4 +- scrapy/contracts/__init__.py | 97 ++++++++++++++++++---------------- scrapy/tests/test_contracts.py | 10 ++-- 3 files changed, 58 insertions(+), 53 deletions(-) diff --git a/scrapy/commands/check.py b/scrapy/commands/check.py index 95fc78a36..56af93548 100644 --- a/scrapy/commands/check.py +++ b/scrapy/commands/check.py @@ -34,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 @@ -42,7 +44,7 @@ class Command(ScrapyCommand): self.settings['SPIDER_CONTRACTS'], ) self.conman = ContractsManager([load_object(c) for c in contracts]) - self.results = TextTestRunner()._makeResult() + self.results = TextTestRunner(verbosity=opts.verbose)._makeResult() # contract requests contract_reqs = defaultdict(list) diff --git a/scrapy/contracts/__init__.py b/scrapy/contracts/__init__.py index a9c2c2f83..55f2cab52 100644 --- a/scrapy/contracts/__init__.py +++ b/scrapy/contracts/__init__.py @@ -8,17 +8,6 @@ from scrapy.utils.spider import iterate_spider_output from scrapy.utils.python import get_spec -def create_testcase(method): - name = '%s.%s' % (method.__self__.__class__.__name__, method.__name__) - - class ContractTestCase(TestCase): - def __str__(self): - return "%s (%s)" % (name, method.__self__.name) - - setattr(ContractTestCase, name, lambda x: x) - return ContractTestCase(name) - - class ContractsManager(object): contracts = {} @@ -66,53 +55,67 @@ class Contract(object): """ Abstract class for contracts """ def __init__(self, method, *args): - self.testcase = create_testcase(method) + self.testcase_pre = self.create_testcase(method, 'pre-hook') + self.testcase_post = self.create_testcase(method, 'post-hook') self.args = args + def create_testcase(self, method, hook): + spider = method.__self__.name + + 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): - cb = request.callback + if hasattr(self, 'pre_process'): + cb = request.callback - @wraps(cb) - def wrapper(response): - try: - self.pre_process(response) - except AssertionError: - results.addFailure(self.testcase, sys.exc_info()) - except Exception: - results.addError(self.testcase, sys.exc_info()) - else: - results.addSuccess(self.testcase) - finally: - return list(iterate_spider_output(cb(response))) + @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: + 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, results): - cb = request.callback + if hasattr(self, 'post_process'): + cb = request.callback - @wraps(cb) - def wrapper(response): - try: - output = list(iterate_spider_output(cb(response))) - self.post_process(output) - except AssertionError: - results.addFailure(self.testcase, sys.exc_info()) - except Exception: - results.addError(self.testcase, sys.exc_info()) - else: - results.addSuccess(self.testcase) - finally: - return output + @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: + 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/tests/test_contracts.py b/scrapy/tests/test_contracts.py index 07210d0f3..38d2e56fc 100644 --- a/scrapy/tests/test_contracts.py +++ b/scrapy/tests/test_contracts.py @@ -86,20 +86,20 @@ class ContractsManagerTest(unittest.TestCase): self.assertFalse(self.results.errors) def test_contracts(self): + spider = TestSpider() + # extract contracts correctly - contracts = self.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 = self.conman.from_method(TestSpider.returns_request, - self.results) + request = self.conman.from_method(spider.returns_request, self.results) self.assertNotEqual(request, None) # no request for missing url - request = self.conman.from_method(TestSpider.parse_no_url, - self.results) + request = self.conman.from_method(spider.parse_no_url, self.results) self.assertEqual(request, None) def test_returns(self): From 0350920f1dd3764183ec0a95f9fee3483b489a12 Mon Sep 17 00:00:00 2001 From: Alex Cepoi Date: Sun, 28 Oct 2012 18:18:35 +0100 Subject: [PATCH 3/3] scrapy contracts: python2.6 compat --- scrapy/commands/check.py | 2 +- scrapy/contracts/__init__.py | 2 +- scrapy/tests/test_contracts.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scrapy/commands/check.py b/scrapy/commands/check.py index 56af93548..c8dd951a7 100644 --- a/scrapy/commands/check.py +++ b/scrapy/commands/check.py @@ -1,6 +1,6 @@ from collections import defaultdict from functools import wraps -from unittest.runner import TextTestRunner +from unittest import TextTestRunner from scrapy import signals from scrapy.command import ScrapyCommand diff --git a/scrapy/contracts/__init__.py b/scrapy/contracts/__init__.py index 55f2cab52..cfccfe0c5 100644 --- a/scrapy/contracts/__init__.py +++ b/scrapy/contracts/__init__.py @@ -1,7 +1,7 @@ import sys import re from functools import wraps -from unittest.case import TestCase +from unittest import TestCase from scrapy.http import Request from scrapy.utils.spider import iterate_spider_output diff --git a/scrapy/tests/test_contracts.py b/scrapy/tests/test_contracts.py index 38d2e56fc..7e89de25f 100644 --- a/scrapy/tests/test_contracts.py +++ b/scrapy/tests/test_contracts.py @@ -1,4 +1,4 @@ -from unittest.runner import TextTestRunner +from unittest import TextTestRunner from twisted.trial import unittest