mirror of
https://github.com/samba-team/samba.git
synced 2025-02-23 09:57:40 +03:00
testtools: Import latest upstream.
This commit is contained in:
parent
9550765304
commit
ed42535041
@ -122,6 +122,7 @@ In no particular order:
|
||||
* Possibly write a blurb into NEWS.
|
||||
|
||||
* Replace any additional references to NEXT with the version being released.
|
||||
(should be none).
|
||||
|
||||
* Create a source distribution and upload to pypi ('make release').
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
Copyright (c) 2008 Jonathan M. Lange <jml@mumak.net> and the testtools authors.
|
||||
Copyright (c) 2008-2010 Jonathan M. Lange <jml@mumak.net> and the testtools
|
||||
authors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -17,3 +18,22 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Some code in testtools/run.py taken from Python's unittest module:
|
||||
Copyright (c) 1999-2003 Steve Purcell
|
||||
Copyright (c) 2003-2010 Python Software Foundation
|
||||
|
||||
This module is free software, and you may redistribute it and/or modify
|
||||
it under the same terms as Python itself, so long as this copyright message
|
||||
and disclaimer are retained in their original form.
|
||||
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
|
||||
SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
|
||||
THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
|
||||
THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
|
||||
AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
|
||||
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||
|
@ -52,10 +52,28 @@ given the exc_info for the exception, and can use this opportunity to attach
|
||||
more data (via the addDetails API) and potentially other uses.
|
||||
|
||||
|
||||
TestCase.skip
|
||||
~~~~~~~~~~~~~
|
||||
TestCase.patch
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
``skip`` is a simple way to have a test stop running and be reported as a
|
||||
``patch`` is a convenient way to monkey-patch a Python object for the duration
|
||||
of your test. It's especially useful for testing legacy code. e.g.::
|
||||
|
||||
def test_foo(self):
|
||||
my_stream = StringIO()
|
||||
self.patch(sys, 'stderr', my_stream)
|
||||
run_some_code_that_prints_to_stderr()
|
||||
self.assertEqual('', my_stream.getvalue())
|
||||
|
||||
The call to ``patch`` above masks sys.stderr with 'my_stream' so that anything
|
||||
printed to stderr will be captured in a StringIO variable that can be actually
|
||||
tested. Once the test is done, the real sys.stderr is restored to its rightful
|
||||
place.
|
||||
|
||||
|
||||
TestCase.skipTest
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
``skipTest`` is a simple way to have a test stop running and be reported as a
|
||||
skipped test, rather than a success/error/failure. This is an alternative to
|
||||
convoluted logic during test loading, permitting later and more localized
|
||||
decisions about the appropriateness of running a test. Many reasons exist to
|
||||
@ -64,7 +82,9 @@ expensive and should not be run while on laptop battery power, or if the test
|
||||
is testing an incomplete feature (this is sometimes called a TODO). Using this
|
||||
feature when running your test suite with a TestResult object that is missing
|
||||
the ``addSkip`` method will result in the ``addError`` method being invoked
|
||||
instead.
|
||||
instead. ``skipTest`` was previously known as ``skip`` but as Python 2.7 adds
|
||||
``skipTest`` support, the ``skip`` name is now deprecated (but no warning
|
||||
is emitted yet - some time in the future we may do so).
|
||||
|
||||
|
||||
New assertion methods
|
||||
@ -211,3 +231,16 @@ Running tests
|
||||
|
||||
Testtools provides a convenient way to run a test suite using the testtools
|
||||
result object: python -m testtools.run testspec [testspec...].
|
||||
|
||||
Test discovery
|
||||
--------------
|
||||
|
||||
Testtools includes a backported version of the Python 2.7 glue for using the
|
||||
discover test discovery module. If you either have Python 2.7/3.1 or newer, or
|
||||
install the 'discover' module, then you can invoke discovery::
|
||||
|
||||
python -m testtools.run discover [path]
|
||||
|
||||
For more information see the Python 2.7 unittest documentation, or::
|
||||
|
||||
python -m testtools.run --help
|
||||
|
@ -17,6 +17,8 @@ clean:
|
||||
find testtools -name "*.pyc" -exec rm '{}' \;
|
||||
|
||||
release:
|
||||
# An existing MANIFEST breaks distutils sometimes. Avoid that.
|
||||
-rm MANIFEST
|
||||
./setup.py sdist upload --sign
|
||||
|
||||
apidocs:
|
||||
|
@ -4,6 +4,127 @@ testtools NEWS
|
||||
NEXT
|
||||
~~~~
|
||||
|
||||
0.9.6
|
||||
~~~~~
|
||||
|
||||
Nothing major in this release, just enough small bits and pieces to make it
|
||||
useful enough to upgrade to.
|
||||
|
||||
In particular, a serious bug in assertThat() has been fixed, it's easier to
|
||||
write Matchers, there's a TestCase.patch() method for those inevitable monkey
|
||||
patches and TestCase.assertEqual gives slightly nicer errors.
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
* 'TestCase.assertEqual' now formats errors a little more nicely, in the
|
||||
style of bzrlib.
|
||||
|
||||
* Added `PlaceHolder` and `ErrorHolder`, TestCase-like objects that can be
|
||||
used to add results to a `TestResult`.
|
||||
|
||||
* 'Mismatch' now takes optional description and details parameters, so
|
||||
custom Matchers aren't compelled to make their own subclass.
|
||||
|
||||
* jml added a built-in UTF8_TEXT ContentType to make it slightly easier to
|
||||
add details to test results. See bug #520044.
|
||||
|
||||
* Fix a bug in our built-in matchers where assertThat would blow up if any
|
||||
of them failed. All built-in mismatch objects now provide get_details().
|
||||
|
||||
* New 'Is' matcher, which lets you assert that a thing is identical to
|
||||
another thing.
|
||||
|
||||
* New 'LessThan' matcher which lets you assert that a thing is less than
|
||||
another thing.
|
||||
|
||||
* TestCase now has a 'patch()' method to make it easier to monkey-patching
|
||||
objects in tests. See the manual for more information. Fixes bug #310770.
|
||||
|
||||
* MultiTestResult methods now pass back return values from the results it
|
||||
forwards to.
|
||||
|
||||
0.9.5
|
||||
~~~~~
|
||||
|
||||
This release fixes some obscure traceback formatting issues that probably
|
||||
weren't affecting you but were certainly breaking our own test suite.
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
* Jamu Kakar has updated classes in testtools.matchers and testtools.runtest
|
||||
to be new-style classes, fixing bug #611273.
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
* Martin[gz] fixed traceback handling to handle cases where extract_tb returns
|
||||
a source line of None. Fixes bug #611307.
|
||||
|
||||
* Martin[gz] fixed an unicode issue that was causing the tests to fail,
|
||||
closing bug #604187.
|
||||
|
||||
* testtools now handles string exceptions (although why would you want to use
|
||||
them?) and formats their tracebacks correctly. Thanks to Martin[gz] for
|
||||
fixing bug #592262.
|
||||
|
||||
0.9.4
|
||||
~~~~~
|
||||
|
||||
This release overhauls the traceback formatting layer to deal with Python 2
|
||||
line numbers and traceback objects often being local user encoded strings
|
||||
rather than unicode objects. Test discovery has also been added and Python 3.1
|
||||
is also supported. Finally, the Mismatch protocol has been extended to let
|
||||
Matchers collaborate with tests in supplying detailed data about failures.
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
* testtools.utils has been renamed to testtools.compat. Importing
|
||||
testtools.utils will now generate a deprecation warning.
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
* Add machinery for Python 2 to create unicode tracebacks like those used by
|
||||
Python 3. This means testtools no longer throws on encountering non-ascii
|
||||
filenames, source lines, or exception strings when displaying test results.
|
||||
Largely contributed by Martin[gz] with some tweaks from Robert Collins.
|
||||
|
||||
* James Westby has supplied test discovery support using the Python 2.7
|
||||
TestRunner in testtools.run. This requires the 'discover' module. This
|
||||
closes bug #250764.
|
||||
|
||||
* Python 3.1 is now supported, thanks to Martin[gz] for a partial patch.
|
||||
This fixes bug #592375.
|
||||
|
||||
* TestCase.addCleanup has had its docstring corrected about when cleanups run.
|
||||
|
||||
* TestCase.skip is now deprecated in favour of TestCase.skipTest, which is the
|
||||
Python2.7 spelling for skip. This closes bug #560436.
|
||||
|
||||
* Tests work on IronPython patch from Martin[gz] applied.
|
||||
|
||||
* Thanks to a patch from James Westby testtools.matchers.Mismatch can now
|
||||
supply a get_details method, which assertThat will query to provide
|
||||
additional attachments. This can be used to provide additional detail
|
||||
about the mismatch that doesn't suite being included in describe(). For
|
||||
instance, if the match process was complex, a log of the process could be
|
||||
included, permitting debugging.
|
||||
|
||||
* testtools.testresults.real._StringException will now answer __str__ if its
|
||||
value is unicode by encoding with UTF8, and vice versa to answer __unicode__.
|
||||
This permits subunit decoded exceptions to contain unicode and still format
|
||||
correctly.
|
||||
|
||||
0.9.3
|
||||
~~~~~
|
||||
|
||||
More matchers, Python 2.4 support, faster test cloning by switching to copy
|
||||
rather than deepcopy and better output when exceptions occur in cleanups are
|
||||
the defining characteristics of this release.
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
@ -34,6 +155,11 @@ Improvements
|
||||
* The backtrace test result output tests should now pass on windows and other
|
||||
systems where os.sep is not '/'.
|
||||
|
||||
* When a cleanUp or tearDown exception occurs, it is now accumulated as a new
|
||||
traceback in the test details, rather than as a separate call to addError /
|
||||
addException. This makes testtools work better with most TestResult objects
|
||||
and fixes bug #335816.
|
||||
|
||||
|
||||
0.9.2
|
||||
~~~~~
|
||||
|
@ -14,6 +14,10 @@ Licensing
|
||||
This project is distributed under the MIT license and copyright is owned by
|
||||
Jonathan M. Lange. See LICENSE for details.
|
||||
|
||||
Some code in testtools/run.py is taken from Python's unittest module, and
|
||||
is copyright Steve Purcell and the Python Software Foundation, it is
|
||||
distributed under the same license as Python, see LICENSE for details.
|
||||
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
@ -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)
|
||||
|
246
lib/testtools/testtools/compat.py
Normal file
246
lib/testtools/testtools/compat.py
Normal file
@ -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
|
@ -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")])
|
||||
|
@ -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'})
|
||||
|
@ -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 match(self, other):
|
||||
if self.expected == other:
|
||||
return None
|
||||
return EqualsMismatch(self.expected, other)
|
||||
|
||||
def __str__(self):
|
||||
return "Equals(%r)" % self.expected
|
||||
return "%s(%r)" % (self.__class__.__name__, self.expected)
|
||||
|
||||
def match(self, other):
|
||||
if self.comparator(other, self.expected):
|
||||
return None
|
||||
return _BinaryMismatch(self.expected, self.mismatch_string, other)
|
||||
|
||||
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 NotEquals:
|
||||
class Equals(_BinaryComparison):
|
||||
"""Matches if the items are equal."""
|
||||
|
||||
comparator = operator.eq
|
||||
mismatch_string = '!='
|
||||
|
||||
|
||||
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
|
||||
|
||||
def __str__(self):
|
||||
return 'NotEquals(%r)' % (self.expected,)
|
||||
|
||||
def match(self, other):
|
||||
if self.expected != other:
|
||||
return None
|
||||
return NotEqualsMismatch(self.expected, other)
|
||||
comparator = operator.ne
|
||||
mismatch_string = '=='
|
||||
|
||||
|
||||
class NotEqualsMismatch:
|
||||
"""Two things are the same."""
|
||||
class Is(_BinaryComparison):
|
||||
"""Matches if the items are identical."""
|
||||
|
||||
def __init__(self, expected, other):
|
||||
self.expected = expected
|
||||
self.other = other
|
||||
|
||||
def describe(self):
|
||||
return '%r == %r' % (self.expected, self.other)
|
||||
comparator = operator.is_
|
||||
mismatch_string = 'is not'
|
||||
|
||||
|
||||
class MatchesAny:
|
||||
class LessThan(_BinaryComparison):
|
||||
"""Matches if the item is less than the matchers reference object."""
|
||||
|
||||
comparator = operator.__lt__
|
||||
mismatch_string = 'is >='
|
||||
|
||||
|
||||
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):
|
||||
|
97
lib/testtools/testtools/monkey.py
Normal file
97
lib/testtools/testtools/monkey.py
Normal file
@ -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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -10,7 +10,7 @@ __all__ = [
|
||||
'ThreadsafeForwardingResult',
|
||||
]
|
||||
|
||||
from real import (
|
||||
from testtools.testresult.real import (
|
||||
ExtendedToOriginalDecorator,
|
||||
MultiTestResult,
|
||||
TestResult,
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
251
lib/testtools/testtools/tests/test_compat.py
Normal file
251
lib/testtools/testtools/tests/test_compat.py
Normal file
@ -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__)
|
@ -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__)
|
||||
|
@ -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__)
|
||||
|
@ -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))
|
||||
|
166
lib/testtools/testtools/tests/test_monkey.py
Normal file
166
lib/testtools/testtools/tests/test_monkey.py
Normal file
@ -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__)
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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.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']),
|
||||
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.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__)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user