diff options
Diffstat (limited to 'lib/testtools/testtools')
22 files changed, 2143 insertions, 202 deletions
diff --git a/lib/testtools/testtools/__init__.py b/lib/testtools/testtools/__init__.py index 0504d661d4..b1c9b661ed 100644 --- a/lib/testtools/testtools/__init__.py +++ b/lib/testtools/testtools/__init__.py @@ -1,13 +1,15 @@ -# Copyright (c) 2008, 2009 Jonathan M. Lange. See LICENSE for details. +# Copyright (c) 2008, 2009, 2010 Jonathan M. Lange. See LICENSE for details. """Extensions to the standard Python unittest library.""" __all__ = [ 'clone_test_with_new_id', 'ConcurrentTestSuite', + 'ErrorHolder', 'ExtendedToOriginalDecorator', 'iterate_tests', 'MultiTestResult', + 'PlaceHolder', 'TestCase', 'TestResult', 'TextTestResult', @@ -25,6 +27,8 @@ from testtools.runtest import ( RunTest, ) from testtools.testcase import ( + ErrorHolder, + PlaceHolder, TestCase, clone_test_with_new_id, skip, @@ -40,8 +44,8 @@ from testtools.testresult import ( ) from testtools.testsuite import ( ConcurrentTestSuite, + iterate_tests, ) -from testtools.utils import iterate_tests # same format as sys.version_info: "A tuple containing the five components of # the version number: major, minor, micro, releaselevel, and serial. All @@ -55,4 +59,4 @@ from testtools.utils import iterate_tests # If the releaselevel is 'final', then the tarball will be major.minor.micro. # Otherwise it is major.minor.micro~$(revno). -__version__ = (0, 9, 2, 'final', 0) +__version__ = (0, 9, 7, 'dev', 0) diff --git a/lib/testtools/testtools/compat.py b/lib/testtools/testtools/compat.py new file mode 100644 index 0000000000..0dd2fe8bf9 --- /dev/null +++ b/lib/testtools/testtools/compat.py @@ -0,0 +1,246 @@ +# Copyright (c) 2008-2010 testtools developers. See LICENSE for details. + +"""Compatibility support for python 2 and 3.""" + + +import codecs +import linecache +import locale +import os +import re +import sys +import traceback + +__metaclass__ = type +__all__ = [ + '_b', + '_u', + 'advance_iterator', + 'str_is_unicode', + 'unicode_output_stream', + ] + + +__u_doc = """A function version of the 'u' prefix. + +This is needed becayse the u prefix is not usable in Python 3 but is required +in Python 2 to get a unicode object. + +To migrate code that was written as u'\u1234' in Python 2 to 2+3 change +it to be _u('\u1234'). The Python 3 interpreter will decode it +appropriately and the no-op _u for Python 3 lets it through, in Python +2 we then call unicode-escape in the _u function. +""" + +if sys.version_info > (3, 0): + def _u(s): + return s + _r = ascii + def _b(s): + """A byte literal.""" + return s.encode("latin-1") + advance_iterator = next + def istext(x): + return isinstance(x, str) + def classtypes(): + return (type,) + str_is_unicode = True +else: + def _u(s): + # The double replace mangling going on prepares the string for + # unicode-escape - \foo is preserved, \u and \U are decoded. + return (s.replace("\\", "\\\\").replace("\\\\u", "\\u") + .replace("\\\\U", "\\U").decode("unicode-escape")) + _r = repr + def _b(s): + return s + advance_iterator = lambda it: it.next() + def istext(x): + return isinstance(x, basestring) + def classtypes(): + import types + return (type, types.ClassType) + str_is_unicode = sys.platform == "cli" + +_u.__doc__ = __u_doc + + +def unicode_output_stream(stream): + """Get wrapper for given stream that writes any unicode without exception + + Characters that can't be coerced to the encoding of the stream, or 'ascii' + if valid encoding is not found, will be replaced. The original stream may + be returned in situations where a wrapper is determined unneeded. + + The wrapper only allows unicode to be written, not non-ascii bytestrings, + which is a good thing to ensure sanity and sanitation. + """ + if sys.platform == "cli": + # Best to never encode before writing in IronPython + return stream + try: + writer = codecs.getwriter(stream.encoding or "") + except (AttributeError, LookupError): + # GZ 2010-06-16: Python 3 StringIO ends up here, but probably needs + # different handling as it doesn't want bytestrings + return codecs.getwriter("ascii")(stream, "replace") + if writer.__module__.rsplit(".", 1)[1].startswith("utf"): + # The current stream has a unicode encoding so no error handler is needed + return stream + if sys.version_info > (3, 0): + # Python 3 doesn't seem to make this easy, handle a common case + try: + return stream.__class__(stream.buffer, stream.encoding, "replace", + stream.newlines, stream.line_buffering) + except AttributeError: + pass + return writer(stream, "replace") + + +# The default source encoding is actually "iso-8859-1" until Python 2.5 but +# using non-ascii causes a deprecation warning in 2.4 and it's cleaner to +# treat all versions the same way +_default_source_encoding = "ascii" + +# Pattern specified in <http://www.python.org/dev/peps/pep-0263/> +_cookie_search=re.compile("coding[:=]\s*([-\w.]+)").search + +def _detect_encoding(lines): + """Get the encoding of a Python source file from a list of lines as bytes + + This function does less than tokenize.detect_encoding added in Python 3 as + it does not attempt to raise a SyntaxError when the interpreter would, it + just wants the encoding of a source file Python has already compiled and + determined is valid. + """ + if not lines: + return _default_source_encoding + if lines[0].startswith("\xef\xbb\xbf"): + # Source starting with UTF-8 BOM is either UTF-8 or a SyntaxError + return "utf-8" + # Only the first two lines of the source file are examined + magic = _cookie_search("".join(lines[:2])) + if magic is None: + return _default_source_encoding + encoding = magic.group(1) + try: + codecs.lookup(encoding) + except LookupError: + # Some codecs raise something other than LookupError if they don't + # support the given error handler, but not the text ones that could + # actually be used for Python source code + return _default_source_encoding + return encoding + + +class _EncodingTuple(tuple): + """A tuple type that can have an encoding attribute smuggled on""" + + +def _get_source_encoding(filename): + """Detect, cache and return the encoding of Python source at filename""" + try: + return linecache.cache[filename].encoding + except (AttributeError, KeyError): + encoding = _detect_encoding(linecache.getlines(filename)) + if filename in linecache.cache: + newtuple = _EncodingTuple(linecache.cache[filename]) + newtuple.encoding = encoding + linecache.cache[filename] = newtuple + return encoding + + +def _get_exception_encoding(): + """Return the encoding we expect messages from the OS to be encoded in""" + if os.name == "nt": + # GZ 2010-05-24: Really want the codepage number instead, the error + # handling of standard codecs is more deterministic + return "mbcs" + # GZ 2010-05-23: We need this call to be after initialisation, but there's + # no benefit in asking more than once as it's a global + # setting that can change after the message is formatted. + return locale.getlocale(locale.LC_MESSAGES)[1] or "ascii" + + +def _exception_to_text(evalue): + """Try hard to get a sensible text value out of an exception instance""" + try: + return unicode(evalue) + except KeyboardInterrupt: + raise + except: + # Apparently this is what traceback._some_str does. Sigh - RBC 20100623 + pass + try: + return str(evalue).decode(_get_exception_encoding(), "replace") + except KeyboardInterrupt: + raise + except: + # Apparently this is what traceback._some_str does. Sigh - RBC 20100623 + pass + # Okay, out of ideas, let higher level handle it + return None + + +# GZ 2010-05-23: This function is huge and horrible and I welcome suggestions +# on the best way to break it up +_TB_HEADER = _u('Traceback (most recent call last):\n') +def _format_exc_info(eclass, evalue, tb, limit=None): + """Format a stack trace and the exception information as unicode + + Compatibility function for Python 2 which ensures each component of a + traceback is correctly decoded according to its origins. + + Based on traceback.format_exception and related functions. + """ + fs_enc = sys.getfilesystemencoding() + if tb: + list = [_TB_HEADER] + extracted_list = [] + for filename, lineno, name, line in traceback.extract_tb(tb, limit): + extracted_list.append(( + filename.decode(fs_enc, "replace"), + lineno, + name.decode("ascii", "replace"), + line and line.decode( + _get_source_encoding(filename), "replace"))) + list.extend(traceback.format_list(extracted_list)) + else: + list = [] + if evalue is None: + # Is a (deprecated) string exception + list.append(eclass.decode("ascii", "replace")) + elif isinstance(evalue, SyntaxError) and len(evalue.args) > 1: + # Avoid duplicating the special formatting for SyntaxError here, + # instead create a new instance with unicode filename and line + # Potentially gives duff spacing, but that's a pre-existing issue + filename, lineno, offset, line = evalue.args[1] + if line: + # Errors during parsing give the line from buffer encoded as + # latin-1 or utf-8 or the encoding of the file depending on the + # coding and whether the patch for issue #1031213 is applied, so + # give up on trying to decode it and just read the file again + bytestr = linecache.getline(filename, lineno) + if bytestr: + if lineno == 1 and bytestr.startswith("\xef\xbb\xbf"): + bytestr = bytestr[3:] + line = bytestr.decode(_get_source_encoding(filename), "replace") + del linecache.cache[filename] + else: + line = line.decode("ascii", "replace") + if filename: + filename = filename.decode(fs_enc, "replace") + evalue = eclass(evalue.args[0], (filename, lineno, offset, line)) + list.extend(traceback.format_exception_only(eclass, evalue)) + else: + sclass = eclass.__name__ + svalue = _exception_to_text(evalue) + if svalue: + list.append("%s: %s\n" % (sclass, svalue)) + elif svalue is None: + # GZ 2010-05-24: Not a great fallback message, but keep for the + # the same for compatibility for the moment + list.append("%s: <unprintable %s object>\n" % (sclass, sclass)) + else: + list.append("%s\n" % sclass) + return list diff --git a/lib/testtools/testtools/content.py b/lib/testtools/testtools/content.py index 353e3f0f46..843133aa1a 100644 --- a/lib/testtools/testtools/content.py +++ b/lib/testtools/testtools/content.py @@ -3,10 +3,10 @@ """Content - a MIME-like Content object.""" import codecs -from unittest import TestResult +from testtools.compat import _b from testtools.content_type import ContentType -from testtools.utils import _b +from testtools.testresult import TestResult class Content(object): @@ -86,6 +86,6 @@ class TracebackContent(Content): content_type = ContentType('text', 'x-traceback', {"language": "python", "charset": "utf8"}) self._result = TestResult() - value = self._result._exc_info_to_string(err, test) + value = self._result._exc_info_to_unicode(err, test) super(TracebackContent, self).__init__( content_type, lambda: [value.encode("utf8")]) diff --git a/lib/testtools/testtools/content_type.py b/lib/testtools/testtools/content_type.py index aded81b732..a936506e48 100644 --- a/lib/testtools/testtools/content_type.py +++ b/lib/testtools/testtools/content_type.py @@ -28,3 +28,6 @@ class ContentType(object): def __repr__(self): return "%s/%s params=%s" % (self.type, self.subtype, self.parameters) + + +UTF8_TEXT = ContentType('text', 'plain', {'charset': 'utf8'}) diff --git a/lib/testtools/testtools/matchers.py b/lib/testtools/testtools/matchers.py index 039c84b7c7..6a4c82a2fe 100644 --- a/lib/testtools/testtools/matchers.py +++ b/lib/testtools/testtools/matchers.py @@ -15,6 +15,8 @@ __all__ = [ 'Annotate', 'DocTestMatches', 'Equals', + 'Is', + 'LessThan', 'MatchesAll', 'MatchesAny', 'NotEquals', @@ -22,9 +24,10 @@ __all__ = [ ] import doctest +import operator -class Matcher: +class Matcher(object): """A pattern matcher. A Matcher must implement match and __str__ to be used by @@ -52,18 +55,53 @@ class Matcher: raise NotImplementedError(self.__str__) -class Mismatch: +class Mismatch(object): """An object describing a mismatch detected by a Matcher.""" + def __init__(self, description=None, details=None): + """Construct a `Mismatch`. + + :param description: A description to use. If not provided, + `Mismatch.describe` must be implemented. + :param details: Extra details about the mismatch. Defaults + to the empty dict. + """ + if description: + self._description = description + if details is None: + details = {} + self._details = details + def describe(self): """Describe the mismatch. This should be either a human-readable string or castable to a string. """ - raise NotImplementedError(self.describe_difference) + try: + return self._description + except AttributeError: + raise NotImplementedError(self.describe) + + def get_details(self): + """Get extra details about the mismatch. + + This allows the mismatch to provide extra information beyond the basic + description, including large text or binary files, or debugging internals + without having to force it to fit in the output of 'describe'. + + The testtools assertion assertThat will query get_details and attach + all its values to the test, permitting them to be reported in whatever + manner the test environment chooses. + + :return: a dict mapping names to Content objects. name is a string to + name the detail, and the Content object is the detail to add + to the result. For more information see the API to which items from + this dict are passed testtools.TestCase.addDetail. + """ + return getattr(self, '_details', {}) -class DocTestMatches: +class DocTestMatches(object): """See if a string matches a doctest example.""" def __init__(self, example, flags=0): @@ -102,7 +140,7 @@ class DocTestMatches: return self._checker.output_difference(self, with_nl, self.flags) -class DocTestMismatch: +class DocTestMismatch(Mismatch): """Mismatch object for DocTestMatches.""" def __init__(self, matcher, with_nl): @@ -113,63 +151,69 @@ class DocTestMismatch: return self.matcher._describe_difference(self.with_nl) -class Equals: - """Matches if the items are equal.""" +class _BinaryComparison(object): + """Matcher that compares an object to another object.""" def __init__(self, expected): self.expected = expected + def __str__(self): + return "%s(%r)" % (self.__class__.__name__, self.expected) + def match(self, other): - if self.expected == other: + if self.comparator(other, self.expected): return None - return EqualsMismatch(self.expected, other) + return _BinaryMismatch(self.expected, self.mismatch_string, other) - def __str__(self): - return "Equals(%r)" % self.expected + def comparator(self, expected, other): + raise NotImplementedError(self.comparator) -class EqualsMismatch: - """Two things differed.""" +class _BinaryMismatch(Mismatch): + """Two things did not match.""" - def __init__(self, expected, other): + def __init__(self, expected, mismatch_string, other): self.expected = expected + self._mismatch_string = mismatch_string self.other = other def describe(self): - return "%r != %r" % (self.expected, self.other) + return "%r %s %r" % (self.expected, self._mismatch_string, self.other) + + +class Equals(_BinaryComparison): + """Matches if the items are equal.""" + + comparator = operator.eq + mismatch_string = '!=' -class NotEquals: +class NotEquals(_BinaryComparison): """Matches if the items are not equal. In most cases, this is equivalent to `Not(Equals(foo))`. The difference only matters when testing `__ne__` implementations. """ - def __init__(self, expected): - self.expected = expected + comparator = operator.ne + mismatch_string = '==' - def __str__(self): - return 'NotEquals(%r)' % (self.expected,) - def match(self, other): - if self.expected != other: - return None - return NotEqualsMismatch(self.expected, other) +class Is(_BinaryComparison): + """Matches if the items are identical.""" + comparator = operator.is_ + mismatch_string = 'is not' -class NotEqualsMismatch: - """Two things are the same.""" - def __init__(self, expected, other): - self.expected = expected - self.other = other +class LessThan(_BinaryComparison): + """Matches if the item is less than the matchers reference object.""" - def describe(self): - return '%r == %r' % (self.expected, self.other) + comparator = operator.__lt__ + mismatch_string = 'is >=' -class MatchesAny: +class MatchesAny(object): """Matches if any of the matchers it is created with match.""" def __init__(self, *matchers): @@ -189,7 +233,7 @@ class MatchesAny: str(matcher) for matcher in self.matchers]) -class MatchesAll: +class MatchesAll(object): """Matches if all of the matchers it is created with match.""" def __init__(self, *matchers): @@ -210,7 +254,7 @@ class MatchesAll: return None -class MismatchesAll: +class MismatchesAll(Mismatch): """A mismatch with many child mismatches.""" def __init__(self, mismatches): @@ -224,7 +268,7 @@ class MismatchesAll: return '\n'.join(descriptions) -class Not: +class Not(object): """Inverts a matcher.""" def __init__(self, matcher): @@ -241,7 +285,7 @@ class Not: return None -class MatchedUnexpectedly: +class MatchedUnexpectedly(Mismatch): """A thing matched when it wasn't supposed to.""" def __init__(self, matcher, other): @@ -252,7 +296,7 @@ class MatchedUnexpectedly: return "%r matches %s" % (self.other, self.matcher) -class Annotate: +class Annotate(object): """Annotates a matcher with a descriptive string. Mismatches are then described as '<mismatch>: <annotation>'. @@ -271,7 +315,7 @@ class Annotate: return AnnotatedMismatch(self.annotation, mismatch) -class AnnotatedMismatch: +class AnnotatedMismatch(Mismatch): """A mismatch annotated with a descriptive string.""" def __init__(self, annotation, mismatch): diff --git a/lib/testtools/testtools/monkey.py b/lib/testtools/testtools/monkey.py new file mode 100644 index 0000000000..bb24764cb7 --- /dev/null +++ b/lib/testtools/testtools/monkey.py @@ -0,0 +1,97 @@ +# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details. + +"""Helpers for monkey-patching Python code.""" + +__all__ = [ + 'MonkeyPatcher', + 'patch', + ] + + +class MonkeyPatcher(object): + """A set of monkey-patches that can be applied and removed all together. + + Use this to cover up attributes with new objects. Particularly useful for + testing difficult code. + """ + + # Marker used to indicate that the patched attribute did not exist on the + # object before we patched it. + _NO_SUCH_ATTRIBUTE = object() + + def __init__(self, *patches): + """Construct a `MonkeyPatcher`. + + :param *patches: The patches to apply, each should be (obj, name, + new_value). Providing patches here is equivalent to calling + `add_patch`. + """ + # List of patches to apply in (obj, name, value). + self._patches_to_apply = [] + # List of the original values for things that have been patched. + # (obj, name, value) format. + self._originals = [] + for patch in patches: + self.add_patch(*patch) + + def add_patch(self, obj, name, value): + """Add a patch to overwrite 'name' on 'obj' with 'value'. + + The attribute C{name} on C{obj} will be assigned to C{value} when + C{patch} is called or during C{run_with_patches}. + + You can restore the original values with a call to restore(). + """ + self._patches_to_apply.append((obj, name, value)) + + def patch(self): + """Apply all of the patches that have been specified with `add_patch`. + + Reverse this operation using L{restore}. + """ + for obj, name, value in self._patches_to_apply: + original_value = getattr(obj, name, self._NO_SUCH_ATTRIBUTE) + self._originals.append((obj, name, original_value)) + setattr(obj, name, value) + + def restore(self): + """Restore all original values to any patched objects. + + If the patched attribute did not exist on an object before it was + patched, `restore` will delete the attribute so as to return the + object to its original state. + """ + while self._originals: + obj, name, value = self._originals.pop() + if value is self._NO_SUCH_ATTRIBUTE: + delattr(obj, name) + else: + setattr(obj, name, value) + + def run_with_patches(self, f, *args, **kw): + """Run 'f' with the given args and kwargs with all patches applied. + + Restores all objects to their original state when finished. + """ + self.patch() + try: + return f(*args, **kw) + finally: + self.restore() + + +def patch(obj, attribute, value): + """Set 'obj.attribute' to 'value' and return a callable to restore 'obj'. + + If 'attribute' is not set on 'obj' already, then the returned callable + will delete the attribute when called. + + :param obj: An object to monkey-patch. + :param attribute: The name of the attribute to patch. + :param value: The value to set 'obj.attribute' to. + :return: A nullary callable that, when run, will restore 'obj' to its + original state. + """ + patcher = MonkeyPatcher((obj, attribute, value)) + patcher.patch() + return patcher.restore diff --git a/lib/testtools/testtools/run.py b/lib/testtools/testtools/run.py index c4f461ecfb..b6f2c491cd 100755 --- a/lib/testtools/testtools/run.py +++ b/lib/testtools/testtools/run.py @@ -8,10 +8,27 @@ For instance, to run the testtools test suite. $ python -m testtools.run testtools.tests.test_suite """ +import os +import unittest import sys -from testtools.tests import test_suite from testtools import TextTestResult +from testtools.compat import classtypes, istext, unicode_output_stream + + +defaultTestLoader = unittest.defaultTestLoader +defaultTestLoaderCls = unittest.TestLoader + +if getattr(defaultTestLoader, 'discover', None) is None: + try: + import discover + defaultTestLoader = discover.DiscoveringTestLoader() + defaultTestLoaderCls = discover.DiscoveringTestLoader + have_discover = True + except ImportError: + have_discover = False +else: + have_discover = True class TestToolsTestRunner(object): @@ -19,7 +36,7 @@ class TestToolsTestRunner(object): def run(self, test): "Run the given test case or test suite." - result = TextTestResult(sys.stdout) + result = TextTestResult(unicode_output_stream(sys.stdout)) result.startTestRun() try: return test.run(result) @@ -27,13 +44,239 @@ class TestToolsTestRunner(object): result.stopTestRun() +#################### +# Taken from python 2.7 and slightly modified for compatibility with +# older versions. Delete when 2.7 is the oldest supported version. +# Modifications: +# - Use have_discover to raise an error if the user tries to use +# discovery on an old version and doesn't have discover installed. +# - If --catch is given check that installHandler is available, as +# it won't be on old python versions. +# - print calls have been been made single-source python3 compatibile. +# - exception handling likewise. +# - The default help has been changed to USAGE_AS_MAIN and USAGE_FROM_MODULE +# removed. +# - A tweak has been added to detect 'python -m *.run' and use a +# better progName in that case. + +FAILFAST = " -f, --failfast Stop on first failure\n" +CATCHBREAK = " -c, --catch Catch control-C and display results\n" +BUFFEROUTPUT = " -b, --buffer Buffer stdout and stderr during test runs\n" + +USAGE_AS_MAIN = """\ +Usage: %(progName)s [options] [tests] + +Options: + -h, --help Show this message + -v, --verbose Verbose output + -q, --quiet Minimal output +%(failfast)s%(catchbreak)s%(buffer)s +Examples: + %(progName)s test_module - run tests from test_module + %(progName)s module.TestClass - run tests from module.TestClass + %(progName)s module.Class.test_method - run specified test method + +[tests] can be a list of any number of test modules, classes and test +methods. + +Alternative Usage: %(progName)s discover [options] + +Options: + -v, --verbose Verbose output +%(failfast)s%(catchbreak)s%(buffer)s -s directory Directory to start discovery ('.' default) + -p pattern Pattern to match test files ('test*.py' default) + -t directory Top level directory of project (default to + start directory) + +For test discovery all test modules must be importable from the top +level directory of the project. +""" + + +class TestProgram(object): + """A command-line program that runs a set of tests; this is primarily + for making test modules conveniently executable. + """ + USAGE = USAGE_AS_MAIN + + # defaults for testing + failfast = catchbreak = buffer = progName = None + + def __init__(self, module='__main__', defaultTest=None, argv=None, + testRunner=None, testLoader=defaultTestLoader, + exit=True, verbosity=1, failfast=None, catchbreak=None, + buffer=None): + if istext(module): + self.module = __import__(module) + for part in module.split('.')[1:]: + self.module = getattr(self.module, part) + else: + self.module = module + if argv is None: + argv = sys.argv + + self.exit = exit + self.failfast = failfast + self.catchbreak = catchbreak + self.verbosity = verbosity + self.buffer = buffer + self.defaultTest = defaultTest + self.testRunner = testRunner + self.testLoader = testLoader + progName = argv[0] + if progName.endswith('%srun.py' % os.path.sep): + elements = progName.split(os.path.sep) + progName = '%s.run' % elements[-2] + else: + progName = os.path.basename(argv[0]) + self.progName = progName + self.parseArgs(argv) + self.runTests() + + def usageExit(self, msg=None): + if msg: + print(msg) + usage = {'progName': self.progName, 'catchbreak': '', 'failfast': '', + 'buffer': ''} + if self.failfast != False: + usage['failfast'] = FAILFAST + if self.catchbreak != False: + usage['catchbreak'] = CATCHBREAK + if self.buffer != False: + usage['buffer'] = BUFFEROUTPUT + print(self.USAGE % usage) + sys.exit(2) + + def parseArgs(self, argv): + if len(argv) > 1 and argv[1].lower() == 'discover': + self._do_discovery(argv[2:]) + return + + import getopt + long_opts = ['help', 'verbose', 'quiet', 'failfast', 'catch', 'buffer'] + try: + options, args = getopt.getopt(argv[1:], 'hHvqfcb', long_opts) + for opt, value in options: + if opt in ('-h','-H','--help'): + self.usageExit() + if opt in ('-q','--quiet'): + self.verbosity = 0 + if opt in ('-v','--verbose'): + self.verbosity = 2 + if opt in ('-f','--failfast'): + if self.failfast is None: + self.failfast = True + # Should this raise an exception if -f is not valid? + if opt in ('-c','--catch'): + if self.catchbreak is None: + self.catchbreak = True + # Should this raise an exception if -c is not valid? + if opt in ('-b','--buffer'): + if self.buffer is None: + self.buffer = True + # Should this raise an exception if -b is not valid? + if len(args) == 0 and self.defaultTest is None: + # createTests will load tests from self.module + self.testNames = None + elif len(args) > 0: + self.testNames = args + if __name__ == '__main__': + # to support python -m unittest ... + self.module = None + else: + self.testNames = (self.defaultTest,) + self.createTests() + except getopt.error: + exc_info = sys.exc_info() + msg = exc_info[1] + self.usageExit(msg) + + def createTests(self): + if self.testNames is None: + self.test = self.testLoader.loadTestsFromModule(self.module) + else: + self.test = self.testLoader.loadTestsFromNames(self.testNames, + self.module) + + def _do_discovery(self, argv, Loader=defaultTestLoaderCls): + # handle command line args for test discovery + if not have_discover: + raise AssertionError("Unable to use discovery, must use python 2.7 " + "or greater, or install the discover package.") + self.progName = '%s discover' % self.progName + import optparse + parser = optparse.OptionParser() + parser.prog = self.progName + parser.add_option('-v', '--verbose', dest='verbose', default=False, + help='Verbose output', action='store_true') + if self.failfast != False: + parser.add_option('-f', '--failfast', dest='failfast', default=False, + help='Stop on first fail or error', + action='store_true') + if self.catchbreak != False: + parser.add_option('-c', '--catch', dest='catchbreak', default=False, + help='Catch ctrl-C and display results so far', + action='store_true') + if self.buffer != False: + parser.add_option('-b', '--buffer', dest='buffer', default=False, + help='Buffer stdout and stderr during tests', + action='store_true') + parser.add_option('-s', '--start-directory', dest='start', default='.', + help="Directory to start discovery ('.' default)") + parser.add_option('-p', '--pattern', dest='pattern', default='test*.py', + help="Pattern to match tests ('test*.py' default)") + parser.add_option('-t', '--top-level-directory', dest='top', default=None, + help='Top level directory of project (defaults to start directory)') + + options, args = parser.parse_args(argv) + if len(args) > 3: + self.usageExit() + + for name, value in zip(('start', 'pattern', 'top'), args): + setattr(options, name, value) + + # only set options from the parsing here + # if they weren't set explicitly in the constructor + if self.failfast is None: + self.failfast = options.failfast + if self.catchbreak is None: + self.catchbreak = options.catchbreak + if self.buffer is None: + self.buffer = options.buffer + + if options.verbose: + self.verbosity = 2 + + start_dir = options.start + pattern = options.pattern + top_level_dir = options.top + + loader = Loader() + self.test = loader.discover(start_dir, pattern, top_level_dir) + + def runTests(self): + if (self.catchbreak + and getattr(unittest, 'installHandler', None) is not None): + unittest.installHandler() + if self.testRunner is None: + self.testRunner = runner.TextTestRunner + if isinstance(self.testRunner, classtypes()): + try: + testRunner = self.testRunner(verbosity=self.verbosity, + failfast=self.failfast, + buffer=self.buffer) + except TypeError: + # didn't accept the verbosity, buffer or failfast arguments + testRunner = self.testRunner() + else: + # it is assumed to be a TestRunner instance + testRunner = self.testRunner + self.result = testRunner.run(self.test) + if self.exit: + sys.exit(not self.result.wasSuccessful()) +################ + + if __name__ == '__main__': - import optparse - from unittest import TestProgram - parser = optparse.OptionParser(__doc__) - args = parser.parse_args()[1] - if not args: - parser.error("No testspecs given.") runner = TestToolsTestRunner() - program = TestProgram(module=None, argv=[sys.argv[0]] + args, - testRunner=runner) + program = TestProgram(argv=sys.argv, testRunner=runner) diff --git a/lib/testtools/testtools/runtest.py b/lib/testtools/testtools/runtest.py index 053e2205a7..34954935ac 100644 --- a/lib/testtools/testtools/runtest.py +++ b/lib/testtools/testtools/runtest.py @@ -12,7 +12,7 @@ import sys from testtools.testresult import ExtendedToOriginalDecorator -class RunTest: +class RunTest(object): """An object to run a test. RunTest objects are used to implement the internal logic involved in @@ -36,6 +36,8 @@ class RunTest: classes in the list). :ivar exception_caught: An object returned when _run_user catches an exception. + :ivar _exceptions: A list of caught exceptions, used to do the single + reporting of error/failure/skip etc. """ def __init__(self, case, handlers=None): @@ -48,6 +50,7 @@ class RunTest: self.case = case self.handlers = handlers or [] self.exception_caught = object() + self._exceptions = [] def run(self, result=None): """Run self.case reporting activity to result. @@ -86,7 +89,16 @@ class RunTest: result.startTest(self.case) self.result = result try: + self._exceptions = [] self._run_core() + if self._exceptions: + # One or more caught exceptions, now trigger the test's + # reporting method for just one. + e = self._exceptions.pop() + for exc_class, handler in self.handlers: + if isinstance(e, exc_class): + handler(self.case, self.result, e) + break finally: result.stopTest(self.case) return result @@ -96,7 +108,9 @@ class RunTest: if self.exception_caught == self._run_user(self.case._run_setup, self.result): # Don't run the test method if we failed getting here. - self.case._runCleanups(self.result) + e = self.case._runCleanups(self.result) + if e is not None: + self._exceptions.append(e) return # Run everything from here on in. If any of the methods raise an # exception we'll have failed. @@ -112,8 +126,9 @@ class RunTest: failed = True finally: try: - if not self._run_user( - self.case._runCleanups, self.result): + e = self._run_user(self.case._runCleanups, self.result) + if e is not None: + self._exceptions.append(e) failed = True finally: if not failed: @@ -129,14 +144,12 @@ class RunTest: return fn(*args) except KeyboardInterrupt: raise - except Exception: - # Note that bare exceptions are not caught, so raised strings will - # escape: but they are deprecated anyway. + except: exc_info = sys.exc_info() e = exc_info[1] + self.case.onException(exc_info) for exc_class, handler in self.handlers: - self.case.onException(exc_info) if isinstance(e, exc_class): - handler(self.case, self.result, e) + self._exceptions.append(e) return self.exception_caught raise e diff --git a/lib/testtools/testtools/testcase.py b/lib/testtools/testtools/testcase.py index fd70141e6d..48eec71d41 100644 --- a/lib/testtools/testtools/testcase.py +++ b/lib/testtools/testtools/testcase.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008, 2009 Jonathan M. Lange. See LICENSE for details. +# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details. """Test case related stuff.""" @@ -17,14 +17,16 @@ try: except ImportError: wraps = None import itertools +from pprint import pformat import sys import types import unittest from testtools import content +from testtools.compat import advance_iterator +from testtools.monkey import patch from testtools.runtest import RunTest from testtools.testresult import TestResult -from testtools.utils import advance_iterator try: @@ -76,6 +78,7 @@ class TestCase(unittest.TestCase): unittest.TestCase.__init__(self, *args, **kwargs) self._cleanups = [] self._unique_id_gen = itertools.count(1) + self._traceback_id_gen = itertools.count(0) self.__setup_called = False self.__teardown_called = False self.__details = {} @@ -88,6 +91,9 @@ class TestCase(unittest.TestCase): (_UnexpectedSuccess, self._report_unexpected_success), (Exception, self._report_error), ] + if sys.version_info < (2, 6): + # Catch old-style string exceptions with None as the instance + self.exception_handlers.append((type(None), self._report_error)) def __eq__(self, other): eq = getattr(unittest.TestCase, '__eq__', None) @@ -117,10 +123,23 @@ class TestCase(unittest.TestCase): """ return self.__details + def patch(self, obj, attribute, value): + """Monkey-patch 'obj.attribute' to 'value' while the test is running. + + If 'obj' has no attribute, then the monkey-patch will still go ahead, + and the attribute will be deleted instead of restored to its original + value. + + :param obj: The object to patch. Can be anything. + :param attribute: The attribute on 'obj' to patch. + :param value: The value to set 'obj.attribute' to. + """ + self.addCleanup(patch(obj, attribute, value)) + def shortDescription(self): return self.id() - def skip(self, reason): + def skipTest(self, reason): """Cause this test to be skipped. This raises self.skipException(reason). skipException is raised @@ -133,6 +152,10 @@ class TestCase(unittest.TestCase): """ raise self.skipException(reason) + # skipTest is how python2.7 spells this. Sometime in the future + # This should be given a deprecation decorator - RBC 20100611. + skip = skipTest + def _formatTypes(self, classOrIterable): """Format a class or a bunch of classes for display in an error.""" className = getattr(classOrIterable, '__name__', None) @@ -145,9 +168,10 @@ class TestCase(unittest.TestCase): See the docstring for addCleanup for more information. - Returns True if all cleanups ran without error, False otherwise. + :return: None if all cleanups ran without error, the most recently + raised exception from the cleanups otherwise. """ - ok = True + last_exception = None while self._cleanups: function, arguments, keywordArguments = self._cleanups.pop() try: @@ -155,15 +179,16 @@ class TestCase(unittest.TestCase): except KeyboardInterrupt: raise except: - self._report_error(self, result, None) - ok = False - return ok + exc_info = sys.exc_info() + self._report_traceback(exc_info) + last_exception = exc_info[1] + return last_exception def addCleanup(self, function, *arguments, **keywordArguments): """Add a cleanup function to be called after tearDown. Functions added with addCleanup will be called in reverse order of - adding after the test method and before tearDown. + adding after tearDown, or after setUp if setUp raises an exception. If a function added with addCleanup raises an exception, the error will be recorded as a test error, and the next cleanup will then be @@ -198,6 +223,28 @@ class TestCase(unittest.TestCase): content.ContentType('text', 'plain'), lambda: [reason.encode('utf8')])) + def assertEqual(self, expected, observed, message=''): + """Assert that 'expected' is equal to 'observed'. + + :param expected: The expected value. + :param observed: The observed value. + :param message: An optional message to include in the error. + """ + try: + return super(TestCase, self).assertEqual(expected, observed) + except self.failureException: + lines = [] + if message: + lines.append(message) + lines.extend( + ["not equal:", + "a = %s" % pformat(expected), + "b = %s" % pformat(observed), + '']) + self.fail('\n'.join(lines)) + + failUnlessEqual = assertEquals = assertEqual + def assertIn(self, needle, haystack): """Assert that needle is in haystack.""" self.assertTrue( @@ -261,6 +308,14 @@ class TestCase(unittest.TestCase): mismatch = matcher.match(matchee) if not mismatch: return + existing_details = self.getDetails() + for (name, content) in mismatch.get_details().items(): + full_name = name + suffix = 1 + while full_name in existing_details: + full_name = "%s-%d" % (name, suffix) + suffix += 1 + self.addDetail(full_name, content) self.fail('Match failed. Matchee: "%s"\nMatcher: %s\nDifference: %s\n' % (matchee, matcher, mismatch.describe())) @@ -288,8 +343,7 @@ class TestCase(unittest.TestCase): predicate(*args, **kwargs) except self.failureException: exc_info = sys.exc_info() - self.addDetail('traceback', - content.TracebackContent(exc_info, self)) + self._report_traceback(exc_info) raise _ExpectedFailure(exc_info) else: raise _UnexpectedSuccess(reason) @@ -323,12 +377,14 @@ class TestCase(unittest.TestCase): :seealso addOnException: """ + if exc_info[0] not in [ + TestSkipped, _UnexpectedSuccess, _ExpectedFailure]: + self._report_traceback(exc_info) for handler in self.__exception_handlers: handler(exc_info) @staticmethod def _report_error(self, result, err): - self._report_traceback() result.addError(self, details=self.getDetails()) @staticmethod @@ -337,7 +393,6 @@ class TestCase(unittest.TestCase): @staticmethod def _report_failure(self, result, err): - self._report_traceback() result.addFailure(self, details=self.getDetails()) @staticmethod @@ -349,9 +404,13 @@ class TestCase(unittest.TestCase): self._add_reason(reason) result.addSkip(self, details=self.getDetails()) - def _report_traceback(self): - self.addDetail('traceback', - content.TracebackContent(sys.exc_info(), self)) + def _report_traceback(self, exc_info): + tb_id = advance_iterator(self._traceback_id_gen) + if tb_id: + tb_label = 'traceback-%d' % tb_id + else: + tb_label = 'traceback' + self.addDetail(tb_label, content.TracebackContent(exc_info, self)) @staticmethod def _report_unexpected_success(self, result, err): @@ -414,15 +473,102 @@ class TestCase(unittest.TestCase): self.__teardown_called = True +class PlaceHolder(object): + """A placeholder test. + + `PlaceHolder` implements much of the same interface as `TestCase` and is + particularly suitable for being added to `TestResult`s. + """ + + def __init__(self, test_id, short_description=None): + """Construct a `PlaceHolder`. + + :param test_id: The id of the placeholder test. + :param short_description: The short description of the place holder + test. If not provided, the id will be used instead. + """ + self._test_id = test_id + self._short_description = short_description + + def __call__(self, result=None): + return self.run(result=result) + + def __repr__(self): + internal = [self._test_id] + if self._short_description is not None: + internal.append(self._short_description) + return "<%s.%s(%s)>" % ( + self.__class__.__module__, + self.__class__.__name__, + ", ".join(map(repr, internal))) + + def __str__(self): + return self.id() + + def countTestCases(self): + return 1 + + def debug(self): + pass + + def id(self): + return self._test_id + + def run(self, result=None): + if result is None: + result = TestResult() + result.startTest(self) + result.addSuccess(self) + result.stopTest(self) + + def shortDescription(self): + if self._short_description is None: + return self.id() + else: + return self._short_description + + +class ErrorHolder(PlaceHolder): + """A placeholder test that will error out when run.""" + + failureException = None + + def __init__(self, test_id, error, short_description=None): + """Construct an `ErrorHolder`. + + :param test_id: The id of the test. + :param error: The exc info tuple that will be used as the test's error. + :param short_description: An optional short description of the test. + """ + super(ErrorHolder, self).__init__( + test_id, short_description=short_description) + self._error = error + + def __repr__(self): + internal = [self._test_id, self._error] + if self._short_description is not None: + internal.append(self._short_description) + return "<%s.%s(%s)>" % ( + self.__class__.__module__, + self.__class__.__name__, + ", ".join(map(repr, internal))) + + def run(self, result=None): + if result is None: + result = TestResult() + result.startTest(self) + result.addError(self, self._error) + result.stopTest(self) + + # Python 2.4 did not know how to copy functions. if types.FunctionType not in copy._copy_dispatch: copy._copy_dispatch[types.FunctionType] = copy._copy_immutable - def clone_test_with_new_id(test, new_id): """Copy a TestCase, and give the copied test a new id. - + This is only expected to be used on tests that have been constructed but not executed. """ diff --git a/lib/testtools/testtools/testresult/__init__.py b/lib/testtools/testtools/testresult/__init__.py index 2ee3d25293..1f779419d2 100644 --- a/lib/testtools/testtools/testresult/__init__.py +++ b/lib/testtools/testtools/testresult/__init__.py @@ -10,7 +10,7 @@ __all__ = [ 'ThreadsafeForwardingResult', ] -from real import ( +from testtools.testresult.real import ( ExtendedToOriginalDecorator, MultiTestResult, TestResult, diff --git a/lib/testtools/testtools/testresult/real.py b/lib/testtools/testtools/testresult/real.py index 8c8a3edd6e..95f6e8f04c 100644 --- a/lib/testtools/testtools/testresult/real.py +++ b/lib/testtools/testtools/testresult/real.py @@ -13,6 +13,8 @@ __all__ = [ import datetime import unittest +from testtools.compat import _format_exc_info, str_is_unicode, _u + class TestResult(unittest.TestResult): """Subclass of unittest.TestResult extending the protocol for flexability. @@ -105,10 +107,27 @@ class TestResult(unittest.TestResult): """Called when a test was expected to fail, but succeed.""" self.unexpectedSuccesses.append(test) + if str_is_unicode: + # Python 3 and IronPython strings are unicode, use parent class method + _exc_info_to_unicode = unittest.TestResult._exc_info_to_string + else: + # For Python 2, need to decode components of traceback according to + # their source, so can't use traceback.format_exception + # Here follows a little deep magic to copy the existing method and + # replace the formatter with one that returns unicode instead + from types import FunctionType as __F, ModuleType as __M + __f = unittest.TestResult._exc_info_to_string.im_func + __g = dict(__f.func_globals) + __m = __M("__fake_traceback") + __m.format_exception = _format_exc_info + __g["traceback"] = __m + _exc_info_to_unicode = __F(__f.func_code, __g, "_exc_info_to_unicode") + del __F, __M, __f, __g, __m + def _err_details_to_string(self, test, err=None, details=None): """Convert an error in exc_info form or a contents dict to a string.""" if err is not None: - return self._exc_info_to_string(err, test) + return self._exc_info_to_unicode(err, test) return _details_to_str(details) def _now(self): @@ -165,41 +184,43 @@ class MultiTestResult(TestResult): self._results = map(ExtendedToOriginalDecorator, results) def _dispatch(self, message, *args, **kwargs): - for result in self._results: + return tuple( getattr(result, message)(*args, **kwargs) + for result in self._results) def startTest(self, test): - self._dispatch('startTest', test) + return self._dispatch('startTest', test) def stopTest(self, test): - self._dispatch('stopTest', test) + return self._dispatch('stopTest', test) def addError(self, test, error=None, details=None): - self._dispatch('addError', test, error, details=details) + return self._dispatch('addError', test, error, details=details) def addExpectedFailure(self, test, err=None, details=None): - self._dispatch('addExpectedFailure', test, err, details=details) + return self._dispatch( + 'addExpectedFailure', test, err, details=details) def addFailure(self, test, err=None, details=None): - self._dispatch('addFailure', test, err, details=details) + return self._dispatch('addFailure', test, err, details=details) def addSkip(self, test, reason=None, details=None): - self._dispatch('addSkip', test, reason, details=details) + return self._dispatch('addSkip', test, reason, details=details) def addSuccess(self, test, details=None): - self._dispatch('addSuccess', test, details=details) + return self._dispatch('addSuccess', test, details=details) def addUnexpectedSuccess(self, test, details=None): - self._dispatch('addUnexpectedSuccess', test, details=details) + return self._dispatch('addUnexpectedSuccess', test, details=details) def startTestRun(self): - self._dispatch('startTestRun') + return self._dispatch('startTestRun') def stopTestRun(self): - self._dispatch('stopTestRun') + return self._dispatch('stopTestRun') def done(self): - self._dispatch('done') + return self._dispatch('done') class TextTestResult(TestResult): @@ -508,13 +529,23 @@ class ExtendedToOriginalDecorator(object): class _StringException(Exception): """An exception made from an arbitrary string.""" + if not str_is_unicode: + def __init__(self, string): + if type(string) is not unicode: + raise TypeError("_StringException expects unicode, got %r" % + (string,)) + Exception.__init__(self, string) + + def __str__(self): + return self.args[0].encode("utf-8") + + def __unicode__(self): + return self.args[0] + # For 3.0 and above the default __str__ is fine, so we don't define one. + def __hash__(self): return id(self) - def __str__(self): - """Stringify better than 2.x's default behaviour of ascii encoding.""" - return self.args[0] - def __eq__(self, other): try: return self.args == other.args @@ -537,4 +568,4 @@ def _details_to_str(details): if not chars[-1].endswith('\n'): chars.append('\n') chars.append('------------\n') - return ''.join(chars) + return _u('').join(chars) diff --git a/lib/testtools/testtools/tests/__init__.py b/lib/testtools/testtools/tests/__init__.py index 2cceba91e2..5e22000bb4 100644 --- a/lib/testtools/testtools/tests/__init__.py +++ b/lib/testtools/testtools/tests/__init__.py @@ -4,9 +4,11 @@ import unittest from testtools.tests import ( + test_compat, test_content, test_content_type, test_matchers, + test_monkey, test_runtest, test_testtools, test_testresult, @@ -17,9 +19,11 @@ from testtools.tests import ( def test_suite(): suites = [] modules = [ + test_compat, test_content, test_content_type, test_matchers, + test_monkey, test_runtest, test_testresult, test_testsuite, diff --git a/lib/testtools/testtools/tests/test_compat.py b/lib/testtools/testtools/tests/test_compat.py new file mode 100644 index 0000000000..138b286d5d --- /dev/null +++ b/lib/testtools/testtools/tests/test_compat.py @@ -0,0 +1,251 @@ +# Copyright (c) 2010 testtools developers. See LICENSE for details. + +"""Tests for miscellaneous compatibility functions""" + +import linecache +import os +import sys +import tempfile +import traceback + +import testtools + +from testtools.compat import ( + _b, + _detect_encoding, + _get_source_encoding, + _u, + unicode_output_stream, + ) + + +class TestDetectEncoding(testtools.TestCase): + """Test detection of Python source encodings""" + + def _check_encoding(self, expected, lines, possibly_invalid=False): + """Check lines are valid Python and encoding is as expected""" + if not possibly_invalid: + compile(_b("".join(lines)), "<str>", "exec") + encoding = _detect_encoding(lines) + self.assertEqual(expected, encoding, + "Encoding %r expected but got %r from lines %r" % + (expected, encoding, lines)) + + def test_examples_from_pep(self): + """Check the examples given in PEP 263 all work as specified + + See 'Examples' section of <http://www.python.org/dev/peps/pep-0263/> + """ + # With interpreter binary and using Emacs style file encoding comment: + self._check_encoding("latin-1", ( + "#!/usr/bin/python\n", + "# -*- coding: latin-1 -*-\n", + "import os, sys\n")) + self._check_encoding("iso-8859-15", ( + "#!/usr/bin/python\n", + "# -*- coding: iso-8859-15 -*-\n", + "import os, sys\n")) + self._check_encoding("ascii", ( + "#!/usr/bin/python\n", + "# -*- coding: ascii -*-\n", + "import os, sys\n")) + # Without interpreter line, using plain text: + self._check_encoding("utf-8", ( + "# This Python file uses the following encoding: utf-8\n", + "import os, sys\n")) + # Text editors might have different ways of defining the file's + # encoding, e.g. + self._check_encoding("latin-1", ( + "#!/usr/local/bin/python\n", + "# coding: latin-1\n", + "import os, sys\n")) + # Without encoding comment, Python's parser will assume ASCII text: + self._check_encoding("ascii", ( + "#!/usr/local/bin/python\n", + "import os, sys\n")) + # Encoding comments which don't work: + # Missing "coding:" prefix: + self._check_encoding("ascii", ( + "#!/usr/local/bin/python\n", + "# latin-1\n", + "import os, sys\n")) + # Encoding comment not on line 1 or 2: + self._check_encoding("ascii", ( + "#!/usr/local/bin/python\n", + "#\n", + "# -*- coding: latin-1 -*-\n", + "import os, sys\n")) + # Unsupported encoding: + self._check_encoding("ascii", ( + "#!/usr/local/bin/python\n", + "# -*- coding: utf-42 -*-\n", + "import os, sys\n"), + possibly_invalid=True) + + def test_bom(self): + """Test the UTF-8 BOM counts as an encoding declaration""" + self._check_encoding("utf-8", ( + "\xef\xbb\xbfimport sys\n", + )) + self._check_encoding("utf-8", ( + "\xef\xbb\xbf# File encoding: UTF-8\n", + )) + self._check_encoding("utf-8", ( + '\xef\xbb\xbf"""Module docstring\n', + '\xef\xbb\xbfThat should just be a ZWNB"""\n')) + self._check_encoding("latin-1", ( + '"""Is this coding: latin-1 or coding: utf-8 instead?\n', + '\xef\xbb\xbfThose should be latin-1 bytes"""\n')) + self._check_encoding("utf-8", ( + "\xef\xbb\xbf# Is the coding: utf-8 or coding: euc-jp instead?\n", + '"""Module docstring say \xe2\x98\x86"""\n')) + + def test_multiple_coding_comments(self): + """Test only the first of multiple coding declarations counts""" + self._check_encoding("iso-8859-1", ( + "# Is the coding: iso-8859-1\n", + "# Or is it coding: iso-8859-2\n"), + possibly_invalid=True) + self._check_encoding("iso-8859-1", ( + "#!/usr/bin/python\n", + "# Is the coding: iso-8859-1\n", + "# Or is it coding: iso-8859-2\n")) + self._check_encoding("iso-8859-1", ( + "# Is the coding: iso-8859-1 or coding: iso-8859-2\n", + "# Or coding: iso-8859-3 or coding: iso-8859-4\n"), + possibly_invalid=True) + self._check_encoding("iso-8859-2", ( + "# Is the coding iso-8859-1 or coding: iso-8859-2\n", + "# Spot the missing colon above\n")) + + +class TestGetSourceEncoding(testtools.TestCase): + """Test reading and caching the encodings of source files""" + + def setUp(self): + testtools.TestCase.setUp(self) + dir = tempfile.mkdtemp() + self.addCleanup(os.rmdir, dir) + self.filename = os.path.join(dir, self.id().rsplit(".", 1)[1] + ".py") + self._written = False + + def put_source(self, text): + f = open(self.filename, "w") + try: + f.write(text) + finally: + f.close() + if not self._written: + self._written = True + self.addCleanup(os.remove, self.filename) + self.addCleanup(linecache.cache.pop, self.filename, None) + + def test_nonexistant_file_as_ascii(self): + """When file can't be found, the encoding should default to ascii""" + self.assertEquals("ascii", _get_source_encoding(self.filename)) + + def test_encoding_is_cached(self): + """The encoding should stay the same if the cache isn't invalidated""" + self.put_source( + "# coding: iso-8859-13\n" + "import os\n") + self.assertEquals("iso-8859-13", _get_source_encoding(self.filename)) + self.put_source( + "# coding: rot-13\n" + "vzcbeg bf\n") + self.assertEquals("iso-8859-13", _get_source_encoding(self.filename)) + + def test_traceback_rechecks_encoding(self): + """A traceback function checks the cache and resets the encoding""" + self.put_source( + "# coding: iso-8859-8\n" + "import os\n") + self.assertEquals("iso-8859-8", _get_source_encoding(self.filename)) + self.put_source( + "# coding: utf-8\n" + "import os\n") + try: + exec (compile("raise RuntimeError\n", self.filename, "exec")) + except RuntimeError: + traceback.extract_tb(sys.exc_info()[2]) + else: + self.fail("RuntimeError not raised") + self.assertEquals("utf-8", _get_source_encoding(self.filename)) + + +class _FakeOutputStream(object): + """A simple file-like object for testing""" + + def __init__(self): + self.writelog = [] + + def write(self, obj): + self.writelog.append(obj) + + +class TestUnicodeOutputStream(testtools.TestCase): + """Test wrapping output streams so they work with arbitrary unicode""" + + uni = _u("pa\u026a\u03b8\u0259n") + + def setUp(self): + super(TestUnicodeOutputStream, self).setUp() + if sys.platform == "cli": + self.skip("IronPython shouldn't wrap streams to do encoding") + + def test_no_encoding_becomes_ascii(self): + """A stream with no encoding attribute gets ascii/replace strings""" + sout = _FakeOutputStream() + unicode_output_stream(sout).write(self.uni) + self.assertEqual([_b("pa???n")], sout.writelog) + + def test_encoding_as_none_becomes_ascii(self): + """A stream with encoding value of None gets ascii/replace strings""" + sout = _FakeOutputStream() + sout.encoding = None + unicode_output_stream(sout).write(self.uni) + self.assertEqual([_b("pa???n")], sout.writelog) + + def test_bogus_encoding_becomes_ascii(self): + """A stream with a bogus encoding gets ascii/replace strings""" + sout = _FakeOutputStream() + sout.encoding = "bogus" + unicode_output_stream(sout).write(self.uni) + self.assertEqual([_b("pa???n")], sout.writelog) + + def test_partial_encoding_replace(self): + """A string which can be partly encoded correctly should be""" + sout = _FakeOutputStream() + sout.encoding = "iso-8859-7" + unicode_output_stream(sout).write(self.uni) + self.assertEqual([_b("pa?\xe8?n")], sout.writelog) + + def test_unicode_encodings_not_wrapped(self): + """A unicode encoding is left unwrapped as needs no error handler""" + sout = _FakeOutputStream() + sout.encoding = "utf-8" + self.assertIs(unicode_output_stream(sout), sout) + sout = _FakeOutputStream() + sout.encoding = "utf-16-be" + self.assertIs(unicode_output_stream(sout), sout) + + def test_stringio(self): + """A StringIO object should maybe get an ascii native str type""" + try: + from cStringIO import StringIO + newio = False + except ImportError: + from io import StringIO + newio = True + sout = StringIO() + soutwrapper = unicode_output_stream(sout) + if newio: + self.expectFailure("Python 3 StringIO expects text not bytes", + self.assertRaises, TypeError, soutwrapper.write, self.uni) + soutwrapper.write(self.uni) + self.assertEqual("pa???n", sout.getvalue()) + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/test_content.py b/lib/testtools/testtools/tests/test_content.py index 1159362036..741256ef7a 100644 --- a/lib/testtools/testtools/tests/test_content.py +++ b/lib/testtools/testtools/tests/test_content.py @@ -1,18 +1,14 @@ -# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details. +# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details. import unittest +from testtools import TestCase +from testtools.compat import _u from testtools.content import Content, TracebackContent from testtools.content_type import ContentType -from testtools.utils import _u from testtools.tests.helpers import an_exc_info -def test_suite(): - from unittest import TestLoader - return TestLoader().loadTestsFromName(__name__) - - -class TestContent(unittest.TestCase): +class TestContent(TestCase): def test___init___None_errors(self): self.assertRaises(ValueError, Content, None, None) @@ -57,7 +53,7 @@ class TestContent(unittest.TestCase): self.assertEqual([text], list(content.iter_text())) -class TestTracebackContent(unittest.TestCase): +class TestTracebackContent(TestCase): def test___init___None_errors(self): self.assertRaises(ValueError, TracebackContent, None, None) @@ -70,3 +66,8 @@ class TestTracebackContent(unittest.TestCase): result = unittest.TestResult() expected = result._exc_info_to_string(an_exc_info, self) self.assertEqual(expected, ''.join(list(content.iter_text()))) + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/test_content_type.py b/lib/testtools/testtools/tests/test_content_type.py index dbefc21dec..d593a14eaf 100644 --- a/lib/testtools/testtools/tests/test_content_type.py +++ b/lib/testtools/testtools/tests/test_content_type.py @@ -1,15 +1,11 @@ # Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details. -import unittest -from testtools.content_type import ContentType +from testtools import TestCase +from testtools.matchers import Equals +from testtools.content_type import ContentType, UTF8_TEXT -def test_suite(): - from unittest import TestLoader - return TestLoader().loadTestsFromName(__name__) - - -class TestContentType(unittest.TestCase): +class TestContentType(TestCase): def test___init___None_errors(self): self.assertRaises(ValueError, ContentType, None, None) @@ -23,12 +19,26 @@ class TestContentType(unittest.TestCase): self.assertEqual({}, content_type.parameters) def test___init___with_parameters(self): - content_type = ContentType("foo", "bar", {"quux":"thing"}) - self.assertEqual({"quux":"thing"}, content_type.parameters) + content_type = ContentType("foo", "bar", {"quux": "thing"}) + self.assertEqual({"quux": "thing"}, content_type.parameters) def test___eq__(self): - content_type1 = ContentType("foo", "bar", {"quux":"thing"}) - content_type2 = ContentType("foo", "bar", {"quux":"thing"}) - content_type3 = ContentType("foo", "bar", {"quux":"thing2"}) + content_type1 = ContentType("foo", "bar", {"quux": "thing"}) + content_type2 = ContentType("foo", "bar", {"quux": "thing"}) + content_type3 = ContentType("foo", "bar", {"quux": "thing2"}) self.assertTrue(content_type1.__eq__(content_type2)) self.assertFalse(content_type1.__eq__(content_type3)) + + +class TestBuiltinContentTypes(TestCase): + + def test_plain_text(self): + # The UTF8_TEXT content type represents UTF-8 encoded text/plain. + self.assertThat(UTF8_TEXT.type, Equals('text')) + self.assertThat(UTF8_TEXT.subtype, Equals('plain')) + self.assertThat(UTF8_TEXT.parameters, Equals({'charset': 'utf8'})) + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/test_matchers.py b/lib/testtools/testtools/tests/test_matchers.py index 74b1ebc56a..164a6a0c50 100644 --- a/lib/testtools/testtools/tests/test_matchers.py +++ b/lib/testtools/testtools/tests/test_matchers.py @@ -12,14 +12,33 @@ from testtools.matchers import ( Annotate, Equals, DocTestMatches, + Is, + LessThan, MatchesAny, MatchesAll, + Mismatch, Not, NotEquals, ) +# Silence pyflakes. +Matcher -class TestMatchersInterface: + +class TestMismatch(TestCase): + + def test_constructor_arguments(self): + mismatch = Mismatch("some description", {'detail': "things"}) + self.assertEqual("some description", mismatch.describe()) + self.assertEqual({'detail': "things"}, mismatch.get_details()) + + def test_constructor_no_arguments(self): + mismatch = Mismatch() + self.assertRaises(NotImplementedError, mismatch.describe) + self.assertEqual({}, mismatch.get_details()) + + +class TestMatchersInterface(object): def test_matches_match(self): matcher = self.matches_matcher @@ -45,6 +64,15 @@ class TestMatchersInterface: mismatch = matcher.match(matchee) self.assertEqual(difference, mismatch.describe()) + def test_mismatch_details(self): + # The mismatch object must provide get_details, which must return a + # dictionary mapping names to Content objects. + examples = self.describe_examples + for difference, matchee, matcher in examples: + mismatch = matcher.match(matchee) + details = mismatch.get_details() + self.assertEqual(dict(details), details) + class TestDocTestMatchesInterface(TestCase, TestMatchersInterface): @@ -97,6 +125,33 @@ class TestNotEqualsInterface(TestCase, TestMatchersInterface): describe_examples = [("1 == 1", 1, NotEquals(1))] +class TestIsInterface(TestCase, TestMatchersInterface): + + foo = object() + bar = object() + + matches_matcher = Is(foo) + matches_matches = [foo] + matches_mismatches = [bar, 1] + + str_examples = [("Is(2)", Is(2))] + + describe_examples = [("1 is not 2", 2, Is(1))] + + +class TestLessThanInterface(TestCase, TestMatchersInterface): + + matches_matcher = LessThan(4) + matches_matches = [-5, 3] + matches_mismatches = [4, 5, 5000] + + str_examples = [ + ("LessThan(12)", LessThan(12)), + ] + + describe_examples = [('4 is >= 4', 4, LessThan(4))] + + class TestNotInterface(TestCase, TestMatchersInterface): matches_matcher = Not(Equals(1)) diff --git a/lib/testtools/testtools/tests/test_monkey.py b/lib/testtools/testtools/tests/test_monkey.py new file mode 100644 index 0000000000..09388b22f1 --- /dev/null +++ b/lib/testtools/testtools/tests/test_monkey.py @@ -0,0 +1,166 @@ +# Copyright (c) 2010 Twisted Matrix Laboratories. +# See LICENSE for details. + +"""Tests for testtools.monkey.""" + +from testtools import TestCase +from testtools.monkey import MonkeyPatcher, patch + + +class TestObj: + + def __init__(self): + self.foo = 'foo value' + self.bar = 'bar value' + self.baz = 'baz value' + + +class MonkeyPatcherTest(TestCase): + """ + Tests for 'MonkeyPatcher' monkey-patching class. + """ + + def setUp(self): + super(MonkeyPatcherTest, self).setUp() + self.test_object = TestObj() + self.original_object = TestObj() + self.monkey_patcher = MonkeyPatcher() + + def test_empty(self): + # A monkey patcher without patches doesn't change a thing. + self.monkey_patcher.patch() + + # We can't assert that all state is unchanged, but at least we can + # check our test object. + self.assertEquals(self.original_object.foo, self.test_object.foo) + self.assertEquals(self.original_object.bar, self.test_object.bar) + self.assertEquals(self.original_object.baz, self.test_object.baz) + + def test_construct_with_patches(self): + # Constructing a 'MonkeyPatcher' with patches adds all of the given + # patches to the patch list. + patcher = MonkeyPatcher((self.test_object, 'foo', 'haha'), + (self.test_object, 'bar', 'hehe')) + patcher.patch() + self.assertEquals('haha', self.test_object.foo) + self.assertEquals('hehe', self.test_object.bar) + self.assertEquals(self.original_object.baz, self.test_object.baz) + + def test_patch_existing(self): + # Patching an attribute that exists sets it to the value defined in the + # patch. + self.monkey_patcher.add_patch(self.test_object, 'foo', 'haha') + self.monkey_patcher.patch() + self.assertEquals(self.test_object.foo, 'haha') + + def test_patch_non_existing(self): + # Patching a non-existing attribute sets it to the value defined in + # the patch. + self.monkey_patcher.add_patch(self.test_object, 'doesntexist', 'value') + self.monkey_patcher.patch() + self.assertEquals(self.test_object.doesntexist, 'value') + + def test_restore_non_existing(self): + # Restoring a value that didn't exist before the patch deletes the + # value. + self.monkey_patcher.add_patch(self.test_object, 'doesntexist', 'value') + self.monkey_patcher.patch() + self.monkey_patcher.restore() + marker = object() + self.assertIs(marker, getattr(self.test_object, 'doesntexist', marker)) + + def test_patch_already_patched(self): + # Adding a patch for an object and attribute that already have a patch + # overrides the existing patch. + self.monkey_patcher.add_patch(self.test_object, 'foo', 'blah') + self.monkey_patcher.add_patch(self.test_object, 'foo', 'BLAH') + self.monkey_patcher.patch() + self.assertEquals(self.test_object.foo, 'BLAH') + self.monkey_patcher.restore() + self.assertEquals(self.test_object.foo, self.original_object.foo) + + def test_restore_twice_is_a_no_op(self): + # Restoring an already-restored monkey patch is a no-op. + self.monkey_patcher.add_patch(self.test_object, 'foo', 'blah') + self.monkey_patcher.patch() + self.monkey_patcher.restore() + self.assertEquals(self.test_object.foo, self.original_object.foo) + self.monkey_patcher.restore() + self.assertEquals(self.test_object.foo, self.original_object.foo) + + def test_run_with_patches_decoration(self): + # run_with_patches runs the given callable, passing in all arguments + # and keyword arguments, and returns the return value of the callable. + log = [] + + def f(a, b, c=None): + log.append((a, b, c)) + return 'foo' + + result = self.monkey_patcher.run_with_patches(f, 1, 2, c=10) + self.assertEquals('foo', result) + self.assertEquals([(1, 2, 10)], log) + + def test_repeated_run_with_patches(self): + # We can call the same function with run_with_patches more than + # once. All patches apply for each call. + def f(): + return (self.test_object.foo, self.test_object.bar, + self.test_object.baz) + + self.monkey_patcher.add_patch(self.test_object, 'foo', 'haha') + result = self.monkey_patcher.run_with_patches(f) + self.assertEquals( + ('haha', self.original_object.bar, self.original_object.baz), + result) + result = self.monkey_patcher.run_with_patches(f) + self.assertEquals( + ('haha', self.original_object.bar, self.original_object.baz), + result) + + def test_run_with_patches_restores(self): + # run_with_patches restores the original values after the function has + # executed. + self.monkey_patcher.add_patch(self.test_object, 'foo', 'haha') + self.assertEquals(self.original_object.foo, self.test_object.foo) + self.monkey_patcher.run_with_patches(lambda: None) + self.assertEquals(self.original_object.foo, self.test_object.foo) + + def test_run_with_patches_restores_on_exception(self): + # run_with_patches restores the original values even when the function + # raises an exception. + def _(): + self.assertEquals(self.test_object.foo, 'haha') + self.assertEquals(self.test_object.bar, 'blahblah') + raise RuntimeError, "Something went wrong!" + + self.monkey_patcher.add_patch(self.test_object, 'foo', 'haha') + self.monkey_patcher.add_patch(self.test_object, 'bar', 'blahblah') + + self.assertRaises( + RuntimeError, self.monkey_patcher.run_with_patches, _) + self.assertEquals(self.test_object.foo, self.original_object.foo) + self.assertEquals(self.test_object.bar, self.original_object.bar) + + +class TestPatchHelper(TestCase): + + def test_patch_patches(self): + # patch(obj, name, value) sets obj.name to value. + test_object = TestObj() + patch(test_object, 'foo', 42) + self.assertEqual(42, test_object.foo) + + def test_patch_returns_cleanup(self): + # patch(obj, name, value) returns a nullary callable that restores obj + # to its original state when run. + test_object = TestObj() + original = test_object.foo + cleanup = patch(test_object, 'foo', 42) + cleanup() + self.assertEqual(original, test_object.foo) + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/tests/test_runtest.py b/lib/testtools/testtools/tests/test_runtest.py index 5c46ad1784..a4c0a728b1 100644 --- a/lib/testtools/testtools/tests/test_runtest.py +++ b/lib/testtools/testtools/tests/test_runtest.py @@ -77,14 +77,12 @@ class TestRunTest(TestCase): e = KeyError('Yo') def raises(): raise e - def log_exc(self, result, err): - log.append((result, err)) - run = RunTest(case, [(KeyError, log_exc)]) + run = RunTest(case, [(KeyError, None)]) run.result = ExtendedTestResult() status = run._run_user(raises) self.assertEqual(run.exception_caught, status) self.assertEqual([], run.result._events) - self.assertEqual(["got it", (run.result, e)], log) + self.assertEqual(["got it"], log) def test__run_user_can_catch_Exception(self): case = self.make_case() @@ -92,14 +90,12 @@ class TestRunTest(TestCase): def raises(): raise e log = [] - def log_exc(self, result, err): - log.append((result, err)) - run = RunTest(case, [(Exception, log_exc)]) + run = RunTest(case, [(Exception, None)]) run.result = ExtendedTestResult() status = run._run_user(raises) self.assertEqual(run.exception_caught, status) self.assertEqual([], run.result._events) - self.assertEqual([(run.result, e)], log) + self.assertEqual([], log) def test__run_user_uncaught_Exception_raised(self): case = self.make_case() diff --git a/lib/testtools/testtools/tests/test_testresult.py b/lib/testtools/testtools/tests/test_testresult.py index df15b91244..1a19440069 100644 --- a/lib/testtools/testtools/tests/test_testresult.py +++ b/lib/testtools/testtools/tests/test_testresult.py @@ -4,14 +4,19 @@ __metaclass__ = type +import codecs import datetime try: - from cStringIO import StringIO + from StringIO import StringIO except ImportError: from io import StringIO import doctest +import os +import shutil import sys +import tempfile import threading +import warnings from testtools import ( ExtendedToOriginalDecorator, @@ -22,9 +27,15 @@ from testtools import ( ThreadsafeForwardingResult, testresult, ) +from testtools.compat import ( + _b, + _get_exception_encoding, + _r, + _u, + str_is_unicode, + ) from testtools.content import Content, ContentType from testtools.matchers import DocTestMatches -from testtools.utils import _u, _b from testtools.tests.helpers import ( LoggingResult, Python26TestResult, @@ -253,8 +264,19 @@ class TestMultiTestResult(TestWithFakeExceptions): self.multiResult.stopTestRun() self.assertResultLogsEqual([('stopTestRun')]) + def test_stopTestRun_returns_results(self): + # `MultiTestResult.stopTestRun` returns a tuple of all of the return + # values the `stopTestRun`s that it forwards to. + class Result(LoggingResult): + def stopTestRun(self): + super(Result, self).stopTestRun() + return 'foo' + multi_result = MultiTestResult(Result([]), Result([])) + result = multi_result.stopTestRun() + self.assertEqual(('foo', 'foo'), result) + -class TestTextTestResult(TestWithFakeExceptions): +class TestTextTestResult(TestCase): """Tests for `TextTestResult`.""" def setUp(self): @@ -377,7 +399,7 @@ Traceback (most recent call last): testMethod() File "...testtools...tests...test_testresult.py", line ..., in error 1/0 -ZeroDivisionError: int... division or modulo by zero +ZeroDivisionError:... divi... by zero... ------------ ====================================================================== FAIL: testtools.tests.test_testresult.Test.failed @@ -578,7 +600,7 @@ class TestExtendedToOriginalResultDecorator( self.make_26_result() self.converter.startTest(self) self.assertEqual([('startTest', self)], self.result._events) - + def test_startTest_py27(self): self.make_27_result() self.converter.startTest(self) @@ -593,7 +615,7 @@ class TestExtendedToOriginalResultDecorator( self.make_26_result() self.converter.startTestRun() self.assertEqual([], self.result._events) - + def test_startTestRun_py27(self): self.make_27_result() self.converter.startTestRun() @@ -608,7 +630,7 @@ class TestExtendedToOriginalResultDecorator( self.make_26_result() self.converter.stopTest(self) self.assertEqual([('stopTest', self)], self.result._events) - + def test_stopTest_py27(self): self.make_27_result() self.converter.stopTest(self) @@ -623,7 +645,7 @@ class TestExtendedToOriginalResultDecorator( self.make_26_result() self.converter.stopTestRun() self.assertEqual([], self.result._events) - + def test_stopTestRun_py27(self): self.make_27_result() self.converter.stopTestRun() @@ -668,7 +690,7 @@ class TestExtendedToOriginalAddError(TestExtendedToOriginalResultDecoratorBase): def test_outcome_Original_py26(self): self.make_26_result() self.check_outcome_exc_info(self.outcome) - + def test_outcome_Original_py27(self): self.make_27_result() self.check_outcome_exc_info(self.outcome) @@ -680,7 +702,7 @@ class TestExtendedToOriginalAddError(TestExtendedToOriginalResultDecoratorBase): def test_outcome_Extended_py26(self): self.make_26_result() self.check_outcome_details_to_exec_info(self.outcome) - + def test_outcome_Extended_py27(self): self.make_27_result() self.check_outcome_details_to_exec_info(self.outcome) @@ -709,11 +731,11 @@ class TestExtendedToOriginalAddExpectedFailure( def test_outcome_Original_py26(self): self.make_26_result() self.check_outcome_exc_info_to_nothing(self.outcome, 'addSuccess') - + def test_outcome_Extended_py26(self): self.make_26_result() self.check_outcome_details_to_nothing(self.outcome, 'addSuccess') - + class TestExtendedToOriginalAddSkip( @@ -724,7 +746,7 @@ class TestExtendedToOriginalAddSkip( def test_outcome_Original_py26(self): self.make_26_result() self.check_outcome_string_nothing(self.outcome, 'addSuccess') - + def test_outcome_Original_py27(self): self.make_27_result() self.check_outcome_string(self.outcome) @@ -736,7 +758,7 @@ class TestExtendedToOriginalAddSkip( def test_outcome_Extended_py26(self): self.make_26_result() self.check_outcome_string_nothing(self.outcome, 'addSuccess') - + def test_outcome_Extended_py27(self): self.make_27_result() self.check_outcome_details_to_string(self.outcome) @@ -760,7 +782,7 @@ class TestExtendedToOriginalAddSuccess( def test_outcome_Original_py26(self): self.make_26_result() self.check_outcome_nothing(self.outcome, self.expected) - + def test_outcome_Original_py27(self): self.make_27_result() self.check_outcome_nothing(self.outcome) @@ -772,7 +794,7 @@ class TestExtendedToOriginalAddSuccess( def test_outcome_Extended_py26(self): self.make_26_result() self.check_outcome_details_to_nothing(self.outcome, self.expected) - + def test_outcome_Extended_py27(self): self.make_27_result() self.check_outcome_details_to_nothing(self.outcome) @@ -800,7 +822,299 @@ class TestExtendedToOriginalResultOtherAttributes( self.make_converter() self.assertEqual(1, self.converter.bar) self.assertEqual(2, self.converter.foo()) - + + +class TestNonAsciiResults(TestCase): + """Test all kinds of tracebacks are cleanly interpreted as unicode + + Currently only uses weak "contains" assertions, would be good to be much + stricter about the expected output. This would add a few failures for the + current release of IronPython for instance, which gets some traceback + lines muddled. + """ + + _sample_texts = ( + _u("pa\u026a\u03b8\u0259n"), # Unicode encodings only + _u("\u5357\u7121"), # In ISO 2022 encodings + _u("\xa7\xa7\xa7"), # In ISO 8859 encodings + ) + # Everything but Jython shows syntax errors on the current character + _error_on_character = os.name != "java" + + def _run(self, stream, test): + """Run the test, the same as in testtools.run but not to stdout""" + result = TextTestResult(stream) + result.startTestRun() + try: + return test.run(result) + finally: + result.stopTestRun() + + def _write_module(self, name, encoding, contents): + """Create Python module on disk with contents in given encoding""" + try: + # Need to pre-check that the coding is valid or codecs.open drops + # the file without closing it which breaks non-refcounted pythons + codecs.lookup(encoding) + except LookupError: + self.skip("Encoding unsupported by implementation: %r" % encoding) + f = codecs.open(os.path.join(self.dir, name + ".py"), "w", encoding) + try: + f.write(contents) + finally: + f.close() + + def _test_external_case(self, testline, coding="ascii", modulelevel="", + suffix=""): + """Create and run a test case in a seperate module""" + self._setup_external_case(testline, coding, modulelevel, suffix) + return self._run_external_case() + + def _setup_external_case(self, testline, coding="ascii", modulelevel="", + suffix=""): + """Create a test case in a seperate module""" + _, prefix, self.modname = self.id().rsplit(".", 2) + self.dir = tempfile.mkdtemp(prefix=prefix, suffix=suffix) + self.addCleanup(shutil.rmtree, self.dir) + self._write_module(self.modname, coding, + # Older Python 2 versions don't see a coding declaration in a + # docstring so it has to be in a comment, but then we can't + # workaround bug: <http://ironpython.codeplex.com/workitem/26940> + "# coding: %s\n" + "import testtools\n" + "%s\n" + "class Test(testtools.TestCase):\n" + " def runTest(self):\n" + " %s\n" % (coding, modulelevel, testline)) + + def _run_external_case(self): + """Run the prepared test case in a seperate module""" + sys.path.insert(0, self.dir) + self.addCleanup(sys.path.remove, self.dir) + module = __import__(self.modname) + self.addCleanup(sys.modules.pop, self.modname) + stream = StringIO() + self._run(stream, module.Test()) + return stream.getvalue() + + def _silence_deprecation_warnings(self): + """Shut up DeprecationWarning for this test only""" + warnings.simplefilter("ignore", DeprecationWarning) + self.addCleanup(warnings.filters.remove, warnings.filters[0]) + + def _get_sample_text(self, encoding="unicode_internal"): + if encoding is None and str_is_unicode: + encoding = "unicode_internal" + for u in self._sample_texts: + try: + b = u.encode(encoding) + if u == b.decode(encoding): + if str_is_unicode: + return u, u + return u, b + except (LookupError, UnicodeError): + pass + self.skip("Could not find a sample text for encoding: %r" % encoding) + + def _as_output(self, text): + return text + + def test_non_ascii_failure_string(self): + """Assertion contents can be non-ascii and should get decoded""" + text, raw = self._get_sample_text(_get_exception_encoding()) + textoutput = self._test_external_case("self.fail(%s)" % _r(raw)) + self.assertIn(self._as_output(text), textoutput) + + def test_non_ascii_failure_string_via_exec(self): + """Assertion via exec can be non-ascii and still gets decoded""" + text, raw = self._get_sample_text(_get_exception_encoding()) + textoutput = self._test_external_case( + testline='exec ("self.fail(%s)")' % _r(raw)) + self.assertIn(self._as_output(text), textoutput) + + def test_control_characters_in_failure_string(self): + """Control characters in assertions should be escaped""" + textoutput = self._test_external_case("self.fail('\\a\\a\\a')") + self.expectFailure("Defense against the beeping horror unimplemented", + self.assertNotIn, self._as_output("\a\a\a"), textoutput) + self.assertIn(self._as_output(_u("\uFFFD\uFFFD\uFFFD")), textoutput) + + def test_os_error(self): + """Locale error messages from the OS shouldn't break anything""" + textoutput = self._test_external_case( + modulelevel="import os", + testline="os.mkdir('/')") + if os.name != "nt" or sys.version_info < (2, 5): + self.assertIn(self._as_output("OSError: "), textoutput) + else: + self.assertIn(self._as_output("WindowsError: "), textoutput) + + def test_assertion_text_shift_jis(self): + """A terminal raw backslash in an encoded string is weird but fine""" + example_text = _u("\u5341") + textoutput = self._test_external_case( + coding="shift_jis", + testline="self.fail('%s')" % example_text) + if str_is_unicode: + output_text = example_text + else: + output_text = example_text.encode("shift_jis").decode( + _get_exception_encoding(), "replace") + self.assertIn(self._as_output("AssertionError: %s" % output_text), + textoutput) + + def test_file_comment_iso2022_jp(self): + """Control character escapes must be preserved if valid encoding""" + example_text, _ = self._get_sample_text("iso2022_jp") + textoutput = self._test_external_case( + coding="iso2022_jp", + testline="self.fail('Simple') # %s" % example_text) + self.assertIn(self._as_output(example_text), textoutput) + + def test_unicode_exception(self): + """Exceptions that can be formated losslessly as unicode should be""" + example_text, _ = self._get_sample_text() + exception_class = ( + "class FancyError(Exception):\n" + # A __unicode__ method does nothing on py3k but the default works + " def __unicode__(self):\n" + " return self.args[0]\n") + textoutput = self._test_external_case( + modulelevel=exception_class, + testline="raise FancyError(%s)" % _r(example_text)) + self.assertIn(self._as_output(example_text), textoutput) + + def test_unprintable_exception(self): + """A totally useless exception instance still prints something""" + exception_class = ( + "class UnprintableError(Exception):\n" + " def __str__(self):\n" + " raise RuntimeError\n" + " def __repr__(self):\n" + " raise RuntimeError\n") + textoutput = self._test_external_case( + modulelevel=exception_class, + testline="raise UnprintableError") + self.assertIn(self._as_output( + "UnprintableError: <unprintable UnprintableError object>\n"), + textoutput) + + def test_string_exception(self): + """Raise a string rather than an exception instance if supported""" + if sys.version_info > (2, 6): + self.skip("No string exceptions in Python 2.6 or later") + elif sys.version_info > (2, 5): + self._silence_deprecation_warnings() + textoutput = self._test_external_case(testline="raise 'plain str'") + self.assertIn(self._as_output("\nplain str\n"), textoutput) + + def test_non_ascii_dirname(self): + """Script paths in the traceback can be non-ascii""" + text, raw = self._get_sample_text(sys.getfilesystemencoding()) + textoutput = self._test_external_case( + # Avoid bug in Python 3 by giving a unicode source encoding rather + # than just ascii which raises a SyntaxError with no other details + coding="utf-8", + testline="self.fail('Simple')", + suffix=raw) + self.assertIn(self._as_output(text), textoutput) + + def test_syntax_error(self): + """Syntax errors should still have fancy special-case formatting""" + textoutput = self._test_external_case("exec ('f(a, b c)')") + self.assertIn(self._as_output( + ' File "<string>", line 1\n' + ' f(a, b c)\n' + + ' ' * self._error_on_character + + ' ^\n' + 'SyntaxError: ' + ), textoutput) + + def test_syntax_error_import_binary(self): + """Importing a binary file shouldn't break SyntaxError formatting""" + if sys.version_info < (2, 5): + # Python 2.4 assumes the file is latin-1 and tells you off + self._silence_deprecation_warnings() + self._setup_external_case("import bad") + f = open(os.path.join(self.dir, "bad.py"), "wb") + try: + f.write(_b("x\x9c\xcb*\xcd\xcb\x06\x00\x04R\x01\xb9")) + finally: + f.close() + textoutput = self._run_external_case() + self.assertIn(self._as_output("\nSyntaxError: "), textoutput) + + def test_syntax_error_line_iso_8859_1(self): + """Syntax error on a latin-1 line shows the line decoded""" + text, raw = self._get_sample_text("iso-8859-1") + textoutput = self._setup_external_case("import bad") + self._write_module("bad", "iso-8859-1", + "# coding: iso-8859-1\n! = 0 # %s\n" % text) + textoutput = self._run_external_case() + self.assertIn(self._as_output(_u( + #'bad.py", line 2\n' + ' ! = 0 # %s\n' + ' ^\n' + 'SyntaxError: ') % + (text,)), textoutput) + + def test_syntax_error_line_iso_8859_5(self): + """Syntax error on a iso-8859-5 line shows the line decoded""" + text, raw = self._get_sample_text("iso-8859-5") + textoutput = self._setup_external_case("import bad") + self._write_module("bad", "iso-8859-5", + "# coding: iso-8859-5\n%% = 0 # %s\n" % text) + textoutput = self._run_external_case() + self.assertIn(self._as_output(_u( + #'bad.py", line 2\n' + ' %% = 0 # %s\n' + + ' ' * self._error_on_character + + ' ^\n' + 'SyntaxError: ') % + (text,)), textoutput) + + def test_syntax_error_line_euc_jp(self): + """Syntax error on a euc_jp line shows the line decoded""" + text, raw = self._get_sample_text("euc_jp") + textoutput = self._setup_external_case("import bad") + self._write_module("bad", "euc_jp", + "# coding: euc_jp\n$ = 0 # %s\n" % text) + textoutput = self._run_external_case() + self.assertIn(self._as_output(_u( + #'bad.py", line 2\n' + ' $ = 0 # %s\n' + + ' ' * self._error_on_character + + ' ^\n' + 'SyntaxError: ') % + (text,)), textoutput) + + def test_syntax_error_line_utf_8(self): + """Syntax error on a utf-8 line shows the line decoded""" + text, raw = self._get_sample_text("utf-8") + textoutput = self._setup_external_case("import bad") + self._write_module("bad", "utf-8", _u("\ufeff^ = 0 # %s\n") % text) + textoutput = self._run_external_case() + self.assertIn(self._as_output(_u( + 'bad.py", line 1\n' + ' ^ = 0 # %s\n' + + ' ' * self._error_on_character + + ' ^\n' + 'SyntaxError: ') % + text), textoutput) + + +class TestNonAsciiResultsWithUnittest(TestNonAsciiResults): + """Test that running under unittest produces clean ascii strings""" + + def _run(self, stream, test): + from unittest import TextTestRunner as _Runner + return _Runner(stream).run(test) + + def _as_output(self, text): + if str_is_unicode: + return text + return text.encode("utf-8") + def test_suite(): from unittest import TestLoader diff --git a/lib/testtools/testtools/tests/test_testtools.py b/lib/testtools/testtools/tests/test_testtools.py index af1fd794c3..9edc5a5176 100644 --- a/lib/testtools/testtools/tests/test_testtools.py +++ b/lib/testtools/testtools/tests/test_testtools.py @@ -1,11 +1,14 @@ -# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details. +# Copyright (c) 2008-2010 Jonathan M. Lange. See LICENSE for details. """Tests for extensions to the base test library.""" +from pprint import pformat import sys import unittest from testtools import ( + ErrorHolder, + PlaceHolder, TestCase, clone_test_with_new_id, content, @@ -26,6 +29,167 @@ from testtools.tests.helpers import ( ) +class TestPlaceHolder(TestCase): + + def makePlaceHolder(self, test_id="foo", short_description=None): + return PlaceHolder(test_id, short_description) + + def test_id_comes_from_constructor(self): + # The id() of a PlaceHolder is whatever you pass into the constructor. + test = PlaceHolder("test id") + self.assertEqual("test id", test.id()) + + def test_shortDescription_is_id(self): + # The shortDescription() of a PlaceHolder is the id, by default. + test = PlaceHolder("test id") + self.assertEqual(test.id(), test.shortDescription()) + + def test_shortDescription_specified(self): + # If a shortDescription is provided to the constructor, then + # shortDescription() returns that instead. + test = PlaceHolder("test id", "description") + self.assertEqual("description", test.shortDescription()) + + def test_repr_just_id(self): + # repr(placeholder) shows you how the object was constructed. + test = PlaceHolder("test id") + self.assertEqual( + "<testtools.testcase.PlaceHolder(%s)>" % repr(test.id()), + repr(test)) + + def test_repr_with_description(self): + # repr(placeholder) shows you how the object was constructed. + test = PlaceHolder("test id", "description") + self.assertEqual( + "<testtools.testcase.PlaceHolder(%r, %r)>" % ( + test.id(), test.shortDescription()), + repr(test)) + + def test_counts_as_one_test(self): + # A placeholder test counts as one test. + test = self.makePlaceHolder() + self.assertEqual(1, test.countTestCases()) + + def test_str_is_id(self): + # str(placeholder) is always the id(). We are not barbarians. + test = self.makePlaceHolder() + self.assertEqual(test.id(), str(test)) + + def test_runs_as_success(self): + # When run, a PlaceHolder test records a success. + test = self.makePlaceHolder() + log = [] + test.run(LoggingResult(log)) + self.assertEqual( + [('startTest', test), ('addSuccess', test), ('stopTest', test)], + log) + + def test_call_is_run(self): + # A PlaceHolder can be called, in which case it behaves like run. + test = self.makePlaceHolder() + run_log = [] + test.run(LoggingResult(run_log)) + call_log = [] + test(LoggingResult(call_log)) + self.assertEqual(run_log, call_log) + + def test_runs_without_result(self): + # A PlaceHolder can be run without a result, in which case there's no + # way to actually get at the result. + self.makePlaceHolder().run() + + def test_debug(self): + # A PlaceHolder can be debugged. + self.makePlaceHolder().debug() + + +class TestErrorHolder(TestCase): + + def makeException(self): + try: + raise RuntimeError("danger danger") + except: + return sys.exc_info() + + def makePlaceHolder(self, test_id="foo", error=None, + short_description=None): + if error is None: + error = self.makeException() + return ErrorHolder(test_id, error, short_description) + + def test_id_comes_from_constructor(self): + # The id() of a PlaceHolder is whatever you pass into the constructor. + test = ErrorHolder("test id", self.makeException()) + self.assertEqual("test id", test.id()) + + def test_shortDescription_is_id(self): + # The shortDescription() of a PlaceHolder is the id, by default. + test = ErrorHolder("test id", self.makeException()) + self.assertEqual(test.id(), test.shortDescription()) + + def test_shortDescription_specified(self): + # If a shortDescription is provided to the constructor, then + # shortDescription() returns that instead. + test = ErrorHolder("test id", self.makeException(), "description") + self.assertEqual("description", test.shortDescription()) + + def test_repr_just_id(self): + # repr(placeholder) shows you how the object was constructed. + error = self.makeException() + test = ErrorHolder("test id", error) + self.assertEqual( + "<testtools.testcase.ErrorHolder(%r, %r)>" % (test.id(), error), + repr(test)) + + def test_repr_with_description(self): + # repr(placeholder) shows you how the object was constructed. + error = self.makeException() + test = ErrorHolder("test id", error, "description") + self.assertEqual( + "<testtools.testcase.ErrorHolder(%r, %r, %r)>" % ( + test.id(), error, test.shortDescription()), + repr(test)) + + def test_counts_as_one_test(self): + # A placeholder test counts as one test. + test = self.makePlaceHolder() + self.assertEqual(1, test.countTestCases()) + + def test_str_is_id(self): + # str(placeholder) is always the id(). We are not barbarians. + test = self.makePlaceHolder() + self.assertEqual(test.id(), str(test)) + + def test_runs_as_error(self): + # When run, a PlaceHolder test records a success. + error = self.makeException() + test = self.makePlaceHolder(error=error) + log = [] + test.run(LoggingResult(log)) + self.assertEqual( + [('startTest', test), + ('addError', test, error), + ('stopTest', test)], log) + + def test_call_is_run(self): + # A PlaceHolder can be called, in which case it behaves like run. + test = self.makePlaceHolder() + run_log = [] + test.run(LoggingResult(run_log)) + call_log = [] + test(LoggingResult(call_log)) + self.assertEqual(run_log, call_log) + + def test_runs_without_result(self): + # A PlaceHolder can be run without a result, in which case there's no + # way to actually get at the result. + self.makePlaceHolder().run() + + def test_debug(self): + # A PlaceHolder can be debugged. + self.makePlaceHolder().debug() + + class TestEquality(TestCase): """Test `TestCase`'s equality implementation.""" @@ -47,16 +211,16 @@ class TestAssertions(TestCase): def test_formatTypes_single(self): # Given a single class, _formatTypes returns the name. - class Foo: + class Foo(object): pass self.assertEqual('Foo', self._formatTypes(Foo)) def test_formatTypes_multiple(self): # Given multiple types, _formatTypes returns the names joined by # commas. - class Foo: + class Foo(object): pass - class Bar: + class Bar(object): pass self.assertEqual('Foo, Bar', self._formatTypes([Foo, Bar])) @@ -164,7 +328,7 @@ class TestAssertions(TestCase): def test_assertIsInstance(self): # assertIsInstance asserts that an object is an instance of a class. - class Foo: + class Foo(object): """Simple class for testing assertIsInstance.""" foo = Foo() @@ -174,10 +338,10 @@ class TestAssertions(TestCase): # assertIsInstance asserts that an object is an instance of one of a # group of classes. - class Foo: + class Foo(object): """Simple class for testing assertIsInstance.""" - class Bar: + class Bar(object): """Another simple class for testing assertIsInstance.""" foo = Foo() @@ -188,7 +352,7 @@ class TestAssertions(TestCase): # assertIsInstance(obj, klass) fails the test when obj is not an # instance of klass. - class Foo: + class Foo(object): """Simple class for testing assertIsInstance.""" self.assertFails( @@ -199,10 +363,10 @@ class TestAssertions(TestCase): # assertIsInstance(obj, (klass1, klass2)) fails the test when obj is # not an instance of klass1 or klass2. - class Foo: + class Foo(object): """Simple class for testing assertIsInstance.""" - class Bar: + class Bar(object): """Another simple class for testing assertIsInstance.""" self.assertFails( @@ -251,20 +415,22 @@ class TestAssertions(TestCase): 'None is None: foo bar', self.assertIsNot, None, None, "foo bar") def test_assertThat_matches_clean(self): - class Matcher: + class Matcher(object): def match(self, foo): return None self.assertThat("foo", Matcher()) def test_assertThat_mismatch_raises_description(self): calls = [] - class Mismatch: + class Mismatch(object): def __init__(self, thing): self.thing = thing def describe(self): calls.append(('describe_diff', self.thing)) return "object is not a thing" - class Matcher: + def get_details(self): + return {} + class Matcher(object): def match(self, thing): calls.append(('match', thing)) return Mismatch(thing) @@ -282,6 +448,35 @@ class TestAssertions(TestCase): ], calls) self.assertFalse(result.wasSuccessful()) + def test_assertEqual_nice_formatting(self): + message = "These things ought not be equal." + a = ['apple', 'banana', 'cherry'] + b = {'Thatcher': 'One who mends roofs of straw', + 'Major': 'A military officer, ranked below colonel', + 'Blair': 'To shout loudly', + 'Brown': 'The colour of healthy human faeces'} + expected_error = '\n'.join( + [message, + 'not equal:', + 'a = %s' % pformat(a), + 'b = %s' % pformat(b), + '']) + self.assertFails(expected_error, self.assertEqual, a, b, message) + self.assertFails(expected_error, self.assertEquals, a, b, message) + self.assertFails(expected_error, self.failUnlessEqual, a, b, message) + + def test_assertEqual_formatting_no_message(self): + a = "cat" + b = "dog" + expected_error = '\n'.join( + ['not equal:', + 'a = %s' % pformat(a), + 'b = %s' % pformat(b), + '']) + self.assertFails(expected_error, self.assertEqual, a, b) + self.assertFails(expected_error, self.assertEquals, a, b) + self.assertFails(expected_error, self.failUnlessEqual, a, b) + class TestAddCleanup(TestCase): """Tests for TestCase.addCleanup.""" @@ -301,6 +496,9 @@ class TestAddCleanup(TestCase): def runTest(self): self._calls.append('runTest') + def brokenTest(self): + raise RuntimeError('Deliberate broken test') + def tearDown(self): self._calls.append('tearDown') TestCase.tearDown(self) @@ -400,13 +598,29 @@ class TestAddCleanup(TestCase): self.assertRaises( KeyboardInterrupt, self.test.run, self.logging_result) - def test_multipleErrorsReported(self): - # Errors from all failing cleanups are reported. + def test_multipleCleanupErrorsReported(self): + # Errors from all failing cleanups are reported as separate backtraces. + self.test.addCleanup(lambda: 1/0) + self.test.addCleanup(lambda: 1/0) + self.logging_result = ExtendedTestResult() + self.test.run(self.logging_result) + self.assertEqual(['startTest', 'addError', 'stopTest'], + [event[0] for event in self.logging_result._events]) + self.assertEqual(set(['traceback', 'traceback-1']), + set(self.logging_result._events[1][2].keys())) + + def test_multipleErrorsCoreAndCleanupReported(self): + # Errors from all failing cleanups are reported, with stopTest, + # startTest inserted. + self.test = TestAddCleanup.LoggingTest('brokenTest') self.test.addCleanup(lambda: 1/0) self.test.addCleanup(lambda: 1/0) + self.logging_result = ExtendedTestResult() self.test.run(self.logging_result) - self.assertErrorLogEqual( - ['startTest', 'addError', 'addError', 'stopTest']) + self.assertEqual(['startTest', 'addError', 'stopTest'], + [event[0] for event in self.logging_result._events]) + self.assertEqual(set(['traceback', 'traceback-1', 'traceback-2']), + set(self.logging_result._events[1][2].keys())) class TestWithDetails(TestCase): @@ -594,6 +808,61 @@ class TestDetailsProvided(TestWithDetails): self.assertDetailsProvided(Case("test"), "addUnexpectedSuccess", ["foo"]) + def test_addDetails_from_Mismatch(self): + content = self.get_content() + class Mismatch(object): + def describe(self): + return "Mismatch" + def get_details(self): + return {"foo": content} + class Matcher(object): + def match(self, thing): + return Mismatch() + def __str__(self): + return "a description" + class Case(TestCase): + def test(self): + self.assertThat("foo", Matcher()) + self.assertDetailsProvided(Case("test"), "addFailure", + ["foo", "traceback"]) + + def test_multiple_addDetails_from_Mismatch(self): + content = self.get_content() + class Mismatch(object): + def describe(self): + return "Mismatch" + def get_details(self): + return {"foo": content, "bar": content} + class Matcher(object): + def match(self, thing): + return Mismatch() + def __str__(self): + return "a description" + class Case(TestCase): + def test(self): + self.assertThat("foo", Matcher()) + self.assertDetailsProvided(Case("test"), "addFailure", + ["bar", "foo", "traceback"]) + + def test_addDetails_with_same_name_as_key_from_get_details(self): + content = self.get_content() + class Mismatch(object): + def describe(self): + return "Mismatch" + def get_details(self): + return {"foo": content} + class Matcher(object): + def match(self, thing): + return Mismatch() + def __str__(self): + return "a description" + class Case(TestCase): + def test(self): + self.addDetail("foo", content) + self.assertThat("foo", Matcher()) + self.assertDetailsProvided(Case("test"), "addFailure", + ["foo", "foo-1", "traceback"]) + class TestSetupTearDown(TestCase): @@ -624,6 +893,9 @@ class TestSkipping(TestCase): def test_skip_causes_skipException(self): self.assertRaises(self.skipException, self.skip, "Skip this test") + def test_can_use_skipTest(self): + self.assertRaises(self.skipException, self.skipTest, "Skip this test") + def test_skip_without_reason_works(self): class Test(TestCase): def test(self): @@ -750,6 +1022,64 @@ class TestOnException(TestCase): self.assertThat(events, Equals([])) +class TestPatchSupport(TestCase): + + class Case(TestCase): + def test(self): + pass + + def test_patch(self): + # TestCase.patch masks obj.attribute with the new value. + self.foo = 'original' + test = self.Case('test') + test.patch(self, 'foo', 'patched') + self.assertEqual('patched', self.foo) + + def test_patch_restored_after_run(self): + # TestCase.patch masks obj.attribute with the new value, but restores + # the original value after the test is finished. + self.foo = 'original' + test = self.Case('test') + test.patch(self, 'foo', 'patched') + test.run() + self.assertEqual('original', self.foo) + + def test_successive_patches_apply(self): + # TestCase.patch can be called multiple times per test. Each time you + # call it, it overrides the original value. + self.foo = 'original' + test = self.Case('test') + test.patch(self, 'foo', 'patched') + test.patch(self, 'foo', 'second') + self.assertEqual('second', self.foo) + + def test_successive_patches_restored_after_run(self): + # TestCase.patch restores the original value, no matter how many times + # it was called. + self.foo = 'original' + test = self.Case('test') + test.patch(self, 'foo', 'patched') + test.patch(self, 'foo', 'second') + test.run() + self.assertEqual('original', self.foo) + + def test_patch_nonexistent_attribute(self): + # TestCase.patch can be used to patch a non-existent attribute. + test = self.Case('test') + test.patch(self, 'doesntexist', 'patched') + self.assertEqual('patched', self.doesntexist) + + def test_restore_nonexistent_attribute(self): + # TestCase.patch can be used to patch a non-existent attribute, after + # the test run, the attribute is then removed from the object. + test = self.Case('test') + test.patch(self, 'doesntexist', 'patched') + test.run() + marker = object() + value = getattr(self, 'doesntexist', marker) + self.assertIs(marker, value) + + def test_suite(): from unittest import TestLoader return TestLoader().loadTestsFromName(__name__) diff --git a/lib/testtools/testtools/testsuite.py b/lib/testtools/testtools/testsuite.py index 26b193799b..fd802621e3 100644 --- a/lib/testtools/testtools/testsuite.py +++ b/lib/testtools/testtools/testsuite.py @@ -5,18 +5,31 @@ __metaclass__ = type __all__ = [ 'ConcurrentTestSuite', + 'iterate_tests', ] try: - import Queue + from Queue import Queue except ImportError: - import queue as Queue + from queue import Queue import threading import unittest import testtools +def iterate_tests(test_suite_or_case): + """Iterate through all of the test cases in 'test_suite_or_case'.""" + try: + suite = iter(test_suite_or_case) + except TypeError: + yield test_suite_or_case + else: + for test in suite: + for subtest in iterate_tests(test): + yield subtest + + class ConcurrentTestSuite(unittest.TestSuite): """A TestSuite whose run() calls out to a concurrency strategy.""" @@ -49,7 +62,7 @@ class ConcurrentTestSuite(unittest.TestSuite): tests = self.make_tests(self) try: threads = {} - queue = Queue.Queue() + queue = Queue() result_semaphore = threading.Semaphore(1) for test in tests: process_result = testtools.ThreadsafeForwardingResult(result, diff --git a/lib/testtools/testtools/utils.py b/lib/testtools/testtools/utils.py index c0845b610c..0f39d8f5b6 100644 --- a/lib/testtools/testtools/utils.py +++ b/lib/testtools/testtools/utils.py @@ -1,39 +1,13 @@ -# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details. +# Copyright (c) 2008-2010 testtools developers. See LICENSE for details. -"""Utilities for dealing with stuff in unittest.""" +"""Utilities for dealing with stuff in unittest. +Legacy - deprecated - use testtools.testsuite.iterate_tests +""" -import sys +import warnings +warnings.warn("Please import iterate_tests from testtools.testsuite - " + "testtools.utils is deprecated.", DeprecationWarning, stacklevel=2) -__metaclass__ = type -__all__ = [ - 'iterate_tests', - ] +from testtools.testsuite import iterate_tests - -if sys.version_info > (3, 0): - def _u(s): - """Replacement for u'some string' in Python 3.""" - return s - def _b(s): - """A byte literal.""" - return s.encode("latin-1") - advance_iterator = next -else: - def _u(s): - return unicode(s, "latin-1") - def _b(s): - return s - advance_iterator = lambda it: it.next() - - -def iterate_tests(test_suite_or_case): - """Iterate through all of the test cases in 'test_suite_or_case'.""" - try: - suite = iter(test_suite_or_case) - except TypeError: - yield test_suite_or_case - else: - for test in suite: - for subtest in iterate_tests(test): - yield subtest |