1
0
mirror of https://github.com/samba-team/samba.git synced 2025-03-08 04:58:40 +03:00

testtools: Import new upstream snapshot.

Autobuild-User: Jelmer Vernooij <jelmer@samba.org>
Autobuild-Date: Mon Oct  3 13:54:06 CEST 2011 on sn-devel-104
This commit is contained in:
Jelmer Vernooij 2011-10-03 12:20:19 +02:00
parent 1dbcb61c79
commit d6c949b074
10 changed files with 597 additions and 33 deletions

View File

@ -6,6 +6,22 @@ Changes and improvements to testtools_, grouped by release.
NEXT
~~~~
0.9.12
~~~~~~
This is a very big release. We've made huge improvements on three fronts:
1. Test failures are way nicer and easier to read
2. Matchers and ``assertThat`` are much more convenient to use
3. Correct handling of extended unicode characters
We've trimmed off the fat from the stack trace you get when tests fail, we've
cut out the bits of error messages that just didn't help, we've made it easier
to annotate mismatch failures, to compare complex objects and to match raised
exceptions.
Testing code was never this fun.
Changes
-------
@ -14,6 +30,12 @@ Changes
now deprecated. Please stop using it.
(Jonathan Lange, #813460)
* ``assertThat`` raises ``MismatchError`` instead of
``TestCase.failureException``. ``MismatchError`` is a subclass of
``AssertionError``, so in most cases this change will not matter. However,
if ``self.failureException`` has been set to a non-default value, then
mismatches will become test errors rather than test failures.
* ``gather_details`` takes two dicts, rather than two detailed objects.
(Jonathan Lange, #801027)
@ -30,12 +52,16 @@ Improvements
* All public matchers are now in ``testtools.matchers.__all__``.
(Jonathan Lange, #784859)
* assertThat output is much less verbose, displaying only what the mismatch
* ``assertThat`` can actually display mismatches and matchers that contain
extended unicode characters. (Jonathan Lange, Martin [gz], #804127)
* ``assertThat`` output is much less verbose, displaying only what the mismatch
tells us to display. Old-style verbose output can be had by passing
``verbose=True`` to assertThat. (Jonathan Lange, #675323, #593190)
* assertThat accepts a message which will be used to annotate the matcher. This
can be given as a third parameter or as a keyword parameter. (Robert Collins)
* ``assertThat`` accepts a message which will be used to annotate the matcher.
This can be given as a third parameter or as a keyword parameter.
(Robert Collins)
* Automated the Launchpad part of the release process.
(Jonathan Lange, #623486)

View File

@ -717,7 +717,7 @@ generates. Here's an example mismatch::
self.remainder = remainder
def describe(self):
return "%s is not divisible by %s, %s remains" % (
return "%r is not divisible by %r, %r remains" % (
self.number, self.divider, self.remainder)
def get_details(self):
@ -738,11 +738,19 @@ in the Matcher itself like this::
remainder = actual % self.divider
if remainder != 0:
return Mismatch(
"%s is not divisible by %s, %s remains" % (
"%r is not divisible by %r, %r remains" % (
actual, self.divider, remainder))
else:
return None
When writing a ``describe`` method or constructing a ``Mismatch`` object the
code should ensure it only emits printable unicode. As this output must be
combined with other text and forwarded for presentation, letting through
non-ascii bytes of ambiguous encoding or control characters could throw an
exception or mangle the display. In most cases simply avoiding the ``%s``
format specifier and using ``%r`` instead will be enough. For examples of
more complex formatting see the ``testtools.matchers`` implementatons.
Details
=======

View File

@ -29,7 +29,9 @@ from testtools.content import text_content
ROOT = os.path.dirname(os.path.dirname(__file__))
def run_for_python(version, result):
def run_for_python(version, result, tests):
if not tests:
tests = ['testtools.tests.test_suite']
# XXX: This could probably be broken up and put into subunit.
python = 'python%s' % (version,)
# XXX: Correct API, but subunit doesn't support it. :(
@ -58,7 +60,8 @@ def run_for_python(version, result):
cmd = [
python,
'-W', 'ignore:Module testtools was already imported',
subunit_path, 'testtools.tests.test_suite']
subunit_path]
cmd.extend(tests)
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
_make_stream_binary(process.stdout)
@ -87,4 +90,4 @@ if __name__ == '__main__':
sys.path.append(ROOT)
result = TestProtocolClient(sys.stdout)
for version in '2.4 2.5 2.6 2.7 3.0 3.1 3.2'.split():
run_for_python(version, result)
run_for_python(version, result, sys.argv[1:])

View File

@ -80,4 +80,4 @@ from testtools.distutilscmd import (
# If the releaselevel is 'final', then the tarball will be major.minor.micro.
# Otherwise it is major.minor.micro~$(revno).
__version__ = (0, 9, 12, 'dev', 0)
__version__ = (0, 9, 13, 'dev', 0)

View File

@ -25,6 +25,7 @@ import os
import re
import sys
import traceback
import unicodedata
from testtools.helpers import try_imports
@ -52,6 +53,7 @@ appropriately and the no-op _u for Python 3 lets it through, in Python
"""
if sys.version_info > (3, 0):
import builtins
def _u(s):
return s
_r = ascii
@ -59,12 +61,14 @@ if sys.version_info > (3, 0):
"""A byte literal."""
return s.encode("latin-1")
advance_iterator = next
# GZ 2011-08-24: Seems istext() is easy to misuse and makes for bad code.
def istext(x):
return isinstance(x, str)
def classtypes():
return (type,)
str_is_unicode = True
else:
import __builtin__ as builtins
def _u(s):
# The double replace mangling going on prepares the string for
# unicode-escape - \foo is preserved, \u and \U are decoded.
@ -112,6 +116,95 @@ else:
return isinstance(exception, (KeyboardInterrupt, SystemExit))
# GZ 2011-08-24: Using isinstance checks like this encourages bad interfaces,
# there should be better ways to write code needing this.
if not issubclass(getattr(builtins, "bytes", str), str):
def _isbytes(x):
return isinstance(x, bytes)
else:
# Never return True on Pythons that provide the name but not the real type
def _isbytes(x):
return False
def _slow_escape(text):
"""Escape unicode `text` leaving printable characters unmodified
The behaviour emulates the Python 3 implementation of repr, see
unicode_repr in unicodeobject.c and isprintable definition.
Because this iterates over the input a codepoint at a time, it's slow, and
does not handle astral characters correctly on Python builds with 16 bit
rather than 32 bit unicode type.
"""
output = []
for c in text:
o = ord(c)
if o < 256:
if o < 32 or 126 < o < 161:
output.append(c.encode("unicode-escape"))
elif o == 92:
# Separate due to bug in unicode-escape codec in Python 2.4
output.append("\\\\")
else:
output.append(c)
else:
# To get correct behaviour would need to pair up surrogates here
if unicodedata.category(c)[0] in "CZ":
output.append(c.encode("unicode-escape"))
else:
output.append(c)
return "".join(output)
def text_repr(text, multiline=None):
"""Rich repr for `text` returning unicode, triple quoted if `multiline`"""
is_py3k = sys.version_info > (3, 0)
nl = _isbytes(text) and bytes((0xA,)) or "\n"
if multiline is None:
multiline = nl in text
if not multiline and (is_py3k or not str_is_unicode and type(text) is str):
# Use normal repr for single line of unicode on Python 3 or bytes
return repr(text)
prefix = repr(text[:0])[:-2]
if multiline:
# To escape multiline strings, split and process each line in turn,
# making sure that quotes are not escaped.
if is_py3k:
offset = len(prefix) + 1
lines = []
for l in text.split(nl):
r = repr(l)
q = r[-1]
lines.append(r[offset:-1].replace("\\" + q, q))
elif not str_is_unicode and isinstance(text, str):
lines = [l.encode("string-escape").replace("\\'", "'")
for l in text.split("\n")]
else:
lines = [_slow_escape(l) for l in text.split("\n")]
# Combine the escaped lines and append two of the closing quotes,
# then iterate over the result to escape triple quotes correctly.
_semi_done = "\n".join(lines) + "''"
p = 0
while True:
p = _semi_done.find("'''", p)
if p == -1:
break
_semi_done = "\\".join([_semi_done[:p], _semi_done[p:]])
p += 2
return "".join([prefix, "'''\\\n", _semi_done, "'"])
escaped_text = _slow_escape(text)
# Determine which quote character to use and if one gets prefixed with a
# backslash following the same logic Python uses for repr() on strings
quote = "'"
if "'" in text:
if '"' in text:
escaped_text = escaped_text.replace("'", "\\'")
else:
quote = '"'
return "".join([prefix, quote, escaped_text, quote])
def unicode_output_stream(stream):
"""Get wrapper for given stream that writes any unicode without exception
@ -143,7 +236,7 @@ def unicode_output_stream(stream):
stream.newlines, stream.line_buffering)
except AttributeError:
pass
return writer(stream, "replace")
return writer(stream, "replace")
# The default source encoding is actually "iso-8859-1" until Python 2.5 but

View File

@ -49,7 +49,10 @@ from testtools.compat import (
classtypes,
_error_repr,
isbaseexception,
_isbytes,
istext,
str_is_unicode,
text_repr
)
@ -102,6 +105,8 @@ class Mismatch(object):
"""Describe the mismatch.
This should be either a human-readable string or castable to a string.
In particular, is should either be plain ascii or unicode on Python 2,
and care should be taken to escape control characters.
"""
try:
return self._description
@ -131,6 +136,46 @@ class Mismatch(object):
id(self), self.__dict__)
class MismatchError(AssertionError):
"""Raised when a mismatch occurs."""
# This class exists to work around
# <https://bugs.launchpad.net/testtools/+bug/804127>. It provides a
# guaranteed way of getting a readable exception, no matter what crazy
# characters are in the matchee, matcher or mismatch.
def __init__(self, matchee, matcher, mismatch, verbose=False):
# Have to use old-style upcalling for Python 2.4 and 2.5
# compatibility.
AssertionError.__init__(self)
self.matchee = matchee
self.matcher = matcher
self.mismatch = mismatch
self.verbose = verbose
def __str__(self):
difference = self.mismatch.describe()
if self.verbose:
# GZ 2011-08-24: Smelly API? Better to take any object and special
# case text inside?
if istext(self.matchee) or _isbytes(self.matchee):
matchee = text_repr(self.matchee, multiline=False)
else:
matchee = repr(self.matchee)
return (
'Match failed. Matchee: %s\nMatcher: %s\nDifference: %s\n'
% (matchee, self.matcher, difference))
else:
return difference
if not str_is_unicode:
__unicode__ = __str__
def __str__(self):
return self.__unicode__().encode("ascii", "backslashreplace")
class MismatchDecorator(object):
"""Decorate a ``Mismatch``.
@ -241,7 +286,12 @@ class DocTestMismatch(Mismatch):
self.with_nl = with_nl
def describe(self):
return self.matcher._describe_difference(self.with_nl)
s = self.matcher._describe_difference(self.with_nl)
if str_is_unicode or isinstance(s, unicode):
return s
# GZ 2011-08-24: This is actually pretty bogus, most C0 codes should
# be escaped, in addition to non-ascii bytes.
return s.decode("latin1").encode("ascii", "backslashreplace")
class DoesNotContain(Mismatch):
@ -271,8 +321,8 @@ class DoesNotStartWith(Mismatch):
self.expected = expected
def describe(self):
return "'%s' does not start with '%s'." % (
self.matchee, self.expected)
return "%s does not start with %s." % (
text_repr(self.matchee), text_repr(self.expected))
class DoesNotEndWith(Mismatch):
@ -287,8 +337,8 @@ class DoesNotEndWith(Mismatch):
self.expected = expected
def describe(self):
return "'%s' does not end with '%s'." % (
self.matchee, self.expected)
return "%s does not end with %s." % (
text_repr(self.matchee), text_repr(self.expected))
class _BinaryComparison(object):
@ -320,8 +370,8 @@ class _BinaryMismatch(Mismatch):
def _format(self, thing):
# Blocks of text with newlines are formatted as triple-quote
# strings. Everything else is pretty-printed.
if istext(thing) and '\n' in thing:
return '"""\\\n%s"""' % (thing,)
if istext(thing) or _isbytes(thing):
return text_repr(thing)
return pformat(thing)
def describe(self):
@ -332,7 +382,7 @@ class _BinaryMismatch(Mismatch):
self._mismatch_string, self._format(self.expected),
self._format(self.other))
else:
return "%s %s %s" % (left, self._mismatch_string,right)
return "%s %s %s" % (left, self._mismatch_string, right)
class Equals(_BinaryComparison):
@ -572,7 +622,7 @@ class StartsWith(Matcher):
self.expected = expected
def __str__(self):
return "Starts with '%s'." % self.expected
return "StartsWith(%r)" % (self.expected,)
def match(self, matchee):
if not matchee.startswith(self.expected):
@ -591,7 +641,7 @@ class EndsWith(Matcher):
self.expected = expected
def __str__(self):
return "Ends with '%s'." % self.expected
return "EndsWith(%r)" % (self.expected,)
def match(self, matchee):
if not matchee.endswith(self.expected):
@ -848,8 +898,12 @@ class MatchesRegex(object):
def match(self, value):
if not re.match(self.pattern, value, self.flags):
pattern = self.pattern
if not isinstance(pattern, str_is_unicode and str or unicode):
pattern = pattern.decode("latin1")
pattern = pattern.encode("unicode_escape").decode("ascii")
return Mismatch("%r does not match /%s/" % (
value, self.pattern))
value, pattern.replace("\\\\", "\\")))
class MatchesSetwise(object):

View File

@ -34,6 +34,7 @@ from testtools.matchers import (
Equals,
MatchesAll,
MatchesException,
MismatchError,
Is,
IsInstance,
Not,
@ -393,7 +394,7 @@ class TestCase(unittest.TestCase):
:param matchee: An object to match with matcher.
:param matcher: An object meeting the testtools.Matcher protocol.
:raises self.failureException: When matcher does not match thing.
:raises MismatchError: When matcher does not match thing.
"""
matcher = Annotate.if_message(message, matcher)
mismatch = matcher.match(matchee)
@ -407,13 +408,7 @@ class TestCase(unittest.TestCase):
full_name = "%s-%d" % (name, suffix)
suffix += 1
self.addDetail(full_name, content)
if verbose:
message = (
'Match failed. Matchee: "%s"\nMatcher: %s\nDifference: %s\n'
% (matchee, matcher, mismatch.describe()))
else:
message = mismatch.describe()
self.fail(message)
raise MismatchError(matchee, matcher, mismatch, verbose)
def defaultTestResult(self):
return TestResult()

View File

@ -16,6 +16,7 @@ from testtools.compat import (
_get_source_encoding,
_u,
str_is_unicode,
text_repr,
unicode_output_stream,
)
from testtools.matchers import (
@ -262,6 +263,132 @@ class TestUnicodeOutputStream(testtools.TestCase):
self.assertEqual("pa???n", sout.getvalue())
class TestTextRepr(testtools.TestCase):
"""Ensure in extending repr, basic behaviours are not being broken"""
ascii_examples = (
# Single character examples
# C0 control codes should be escaped except multiline \n
("\x00", "'\\x00'", "'''\\\n\\x00'''"),
("\b", "'\\x08'", "'''\\\n\\x08'''"),
("\t", "'\\t'", "'''\\\n\\t'''"),
("\n", "'\\n'", "'''\\\n\n'''"),
("\r", "'\\r'", "'''\\\n\\r'''"),
# Quotes and backslash should match normal repr behaviour
('"', "'\"'", "'''\\\n\"'''"),
("'", "\"'\"", "'''\\\n\\''''"),
("\\", "'\\\\'", "'''\\\n\\\\'''"),
# DEL is also unprintable and should be escaped
("\x7F", "'\\x7f'", "'''\\\n\\x7f'''"),
# Character combinations that need double checking
("\r\n", "'\\r\\n'", "'''\\\n\\r\n'''"),
("\"'", "'\"\\''", "'''\\\n\"\\''''"),
("'\"", "'\\'\"'", "'''\\\n'\"'''"),
("\\n", "'\\\\n'", "'''\\\n\\\\n'''"),
("\\\n", "'\\\\\\n'", "'''\\\n\\\\\n'''"),
("\\' ", "\"\\\\' \"", "'''\\\n\\\\' '''"),
("\\'\n", "\"\\\\'\\n\"", "'''\\\n\\\\'\n'''"),
("\\'\"", "'\\\\\\'\"'", "'''\\\n\\\\'\"'''"),
("\\'''", "\"\\\\'''\"", "'''\\\n\\\\\\'\\'\\''''"),
)
# Bytes with the high bit set should always be escaped
bytes_examples = (
(_b("\x80"), "'\\x80'", "'''\\\n\\x80'''"),
(_b("\xA0"), "'\\xa0'", "'''\\\n\\xa0'''"),
(_b("\xC0"), "'\\xc0'", "'''\\\n\\xc0'''"),
(_b("\xFF"), "'\\xff'", "'''\\\n\\xff'''"),
(_b("\xC2\xA7"), "'\\xc2\\xa7'", "'''\\\n\\xc2\\xa7'''"),
)
# Unicode doesn't escape printable characters as per the Python 3 model
unicode_examples = (
# C1 codes are unprintable
(_u("\x80"), "'\\x80'", "'''\\\n\\x80'''"),
(_u("\x9F"), "'\\x9f'", "'''\\\n\\x9f'''"),
# No-break space is unprintable
(_u("\xA0"), "'\\xa0'", "'''\\\n\\xa0'''"),
# Letters latin alphabets are printable
(_u("\xA1"), _u("'\xa1'"), _u("'''\\\n\xa1'''")),
(_u("\xFF"), _u("'\xff'"), _u("'''\\\n\xff'''")),
(_u("\u0100"), _u("'\u0100'"), _u("'''\\\n\u0100'''")),
# Line and paragraph seperators are unprintable
(_u("\u2028"), "'\\u2028'", "'''\\\n\\u2028'''"),
(_u("\u2029"), "'\\u2029'", "'''\\\n\\u2029'''"),
# Unpaired surrogates are unprintable
(_u("\uD800"), "'\\ud800'", "'''\\\n\\ud800'''"),
(_u("\uDFFF"), "'\\udfff'", "'''\\\n\\udfff'''"),
# Unprintable general categories not fully tested: Cc, Cf, Co, Cn, Zs
)
b_prefix = repr(_b(""))[:-2]
u_prefix = repr(_u(""))[:-2]
def test_ascii_examples_oneline_bytes(self):
for s, expected, _ in self.ascii_examples:
b = _b(s)
actual = text_repr(b, multiline=False)
# Add self.assertIsInstance check?
self.assertEqual(actual, self.b_prefix + expected)
self.assertEqual(eval(actual), b)
def test_ascii_examples_oneline_unicode(self):
for s, expected, _ in self.ascii_examples:
u = _u(s)
actual = text_repr(u, multiline=False)
self.assertEqual(actual, self.u_prefix + expected)
self.assertEqual(eval(actual), u)
def test_ascii_examples_multiline_bytes(self):
for s, _, expected in self.ascii_examples:
b = _b(s)
actual = text_repr(b, multiline=True)
self.assertEqual(actual, self.b_prefix + expected)
self.assertEqual(eval(actual), b)
def test_ascii_examples_multiline_unicode(self):
for s, _, expected in self.ascii_examples:
u = _u(s)
actual = text_repr(u, multiline=True)
self.assertEqual(actual, self.u_prefix + expected)
self.assertEqual(eval(actual), u)
def test_ascii_examples_defaultline_bytes(self):
for s, one, multi in self.ascii_examples:
expected = "\n" in s and multi or one
self.assertEqual(text_repr(_b(s)), self.b_prefix + expected)
def test_ascii_examples_defaultline_unicode(self):
for s, one, multi in self.ascii_examples:
expected = "\n" in s and multi or one
self.assertEqual(text_repr(_u(s)), self.u_prefix + expected)
def test_bytes_examples_oneline(self):
for b, expected, _ in self.bytes_examples:
actual = text_repr(b, multiline=False)
self.assertEqual(actual, self.b_prefix + expected)
self.assertEqual(eval(actual), b)
def test_bytes_examples_multiline(self):
for b, _, expected in self.bytes_examples:
actual = text_repr(b, multiline=True)
self.assertEqual(actual, self.b_prefix + expected)
self.assertEqual(eval(actual), b)
def test_unicode_examples_oneline(self):
for u, expected, _ in self.unicode_examples:
actual = text_repr(u, multiline=False)
self.assertEqual(actual, self.u_prefix + expected)
self.assertEqual(eval(actual), u)
def test_unicode_examples_multiline(self):
for u, _, expected in self.unicode_examples:
actual = text_repr(u, multiline=True)
self.assertEqual(actual, self.u_prefix + expected)
self.assertEqual(eval(actual), u)
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)

View File

@ -12,6 +12,9 @@ from testtools import (
)
from testtools.compat import (
StringIO,
str_is_unicode,
text_repr,
_b,
_u,
)
from testtools.matchers import (
@ -19,6 +22,7 @@ from testtools.matchers import (
AllMatch,
Annotate,
AnnotatedMismatch,
_BinaryMismatch,
Contains,
Equals,
DocTestMatches,
@ -39,6 +43,7 @@ from testtools.matchers import (
MatchesStructure,
Mismatch,
MismatchDecorator,
MismatchError,
Not,
NotEquals,
Raises,
@ -67,6 +72,125 @@ class TestMismatch(TestCase):
self.assertEqual({}, mismatch.get_details())
class TestMismatchError(TestCase):
def test_is_assertion_error(self):
# MismatchError is an AssertionError, so that most of the time, it
# looks like a test failure, rather than an error.
def raise_mismatch_error():
raise MismatchError(2, Equals(3), Equals(3).match(2))
self.assertRaises(AssertionError, raise_mismatch_error)
def test_default_description_is_mismatch(self):
mismatch = Equals(3).match(2)
e = MismatchError(2, Equals(3), mismatch)
self.assertEqual(mismatch.describe(), str(e))
def test_default_description_unicode(self):
matchee = _u('\xa7')
matcher = Equals(_u('a'))
mismatch = matcher.match(matchee)
e = MismatchError(matchee, matcher, mismatch)
self.assertEqual(mismatch.describe(), str(e))
def test_verbose_description(self):
matchee = 2
matcher = Equals(3)
mismatch = matcher.match(2)
e = MismatchError(matchee, matcher, mismatch, True)
expected = (
'Match failed. Matchee: %r\n'
'Matcher: %s\n'
'Difference: %s\n' % (
matchee,
matcher,
matcher.match(matchee).describe(),
))
self.assertEqual(expected, str(e))
def test_verbose_unicode(self):
# When assertThat is given matchees or matchers that contain non-ASCII
# unicode strings, we can still provide a meaningful error.
matchee = _u('\xa7')
matcher = Equals(_u('a'))
mismatch = matcher.match(matchee)
expected = (
'Match failed. Matchee: %s\n'
'Matcher: %s\n'
'Difference: %s\n' % (
text_repr(matchee),
matcher,
mismatch.describe(),
))
e = MismatchError(matchee, matcher, mismatch, True)
if str_is_unicode:
actual = str(e)
else:
actual = unicode(e)
# Using str() should still work, and return ascii only
self.assertEqual(
expected.replace(matchee, matchee.encode("unicode-escape")),
str(e).decode("ascii"))
self.assertEqual(expected, actual)
class Test_BinaryMismatch(TestCase):
"""Mismatches from binary comparisons need useful describe output"""
_long_string = "This is a longish multiline non-ascii string\n\xa7"
_long_b = _b(_long_string)
_long_u = _u(_long_string)
def test_short_objects(self):
o1, o2 = object(), object()
mismatch = _BinaryMismatch(o1, "!~", o2)
self.assertEqual(mismatch.describe(), "%r !~ %r" % (o1, o2))
def test_short_mixed_strings(self):
b, u = _b("\xa7"), _u("\xa7")
mismatch = _BinaryMismatch(b, "!~", u)
self.assertEqual(mismatch.describe(), "%r !~ %r" % (b, u))
def test_long_bytes(self):
one_line_b = self._long_b.replace(_b("\n"), _b(" "))
mismatch = _BinaryMismatch(one_line_b, "!~", self._long_b)
self.assertEqual(mismatch.describe(),
"%s:\nreference = %s\nactual = %s\n" % ("!~",
text_repr(one_line_b),
text_repr(self._long_b, multiline=True)))
def test_long_unicode(self):
one_line_u = self._long_u.replace("\n", " ")
mismatch = _BinaryMismatch(one_line_u, "!~", self._long_u)
self.assertEqual(mismatch.describe(),
"%s:\nreference = %s\nactual = %s\n" % ("!~",
text_repr(one_line_u),
text_repr(self._long_u, multiline=True)))
def test_long_mixed_strings(self):
mismatch = _BinaryMismatch(self._long_b, "!~", self._long_u)
self.assertEqual(mismatch.describe(),
"%s:\nreference = %s\nactual = %s\n" % ("!~",
text_repr(self._long_b, multiline=True),
text_repr(self._long_u, multiline=True)))
def test_long_bytes_and_object(self):
obj = object()
mismatch = _BinaryMismatch(self._long_b, "!~", obj)
self.assertEqual(mismatch.describe(),
"%s:\nreference = %s\nactual = %s\n" % ("!~",
text_repr(self._long_b, multiline=True),
repr(obj)))
def test_long_unicode_and_object(self):
obj = object()
mismatch = _BinaryMismatch(self._long_u, "!~", obj)
self.assertEqual(mismatch.describe(),
"%s:\nreference = %s\nactual = %s\n" % ("!~",
text_repr(self._long_u, multiline=True),
repr(obj)))
class TestMatchersInterface(object):
run_tests_with = FullStackRunTest
@ -150,6 +274,23 @@ class TestDocTestMatchesSpecific(TestCase):
self.assertEqual("bar\n", matcher.want)
self.assertEqual(doctest.ELLIPSIS, matcher.flags)
def test_describe_non_ascii_bytes(self):
"""Even with bytestrings, the mismatch should be coercible to unicode
DocTestMatches is intended for text, but the Python 2 str type also
permits arbitrary binary inputs. This is a slightly bogus thing to do,
and under Python 3 using bytes objects will reasonably raise an error.
"""
header = _b("\x89PNG\r\n\x1a\n...")
if str_is_unicode:
self.assertRaises(TypeError,
DocTestMatches, header, doctest.ELLIPSIS)
return
matcher = DocTestMatches(header, doctest.ELLIPSIS)
mismatch = matcher.match(_b("GIF89a\1\0\1\0\0\0\0;"))
# Must be treatable as unicode text, the exact output matters less
self.assertTrue(unicode(mismatch.describe()))
class TestEqualsInterface(TestCase, TestMatchersInterface):
@ -552,6 +693,21 @@ class DoesNotStartWithTests(TestCase):
mismatch = DoesNotStartWith("fo", "bo")
self.assertEqual("'fo' does not start with 'bo'.", mismatch.describe())
def test_describe_non_ascii_unicode(self):
string = _u("A\xA7")
suffix = _u("B\xA7")
mismatch = DoesNotStartWith(string, suffix)
self.assertEqual("%s does not start with %s." % (
text_repr(string), text_repr(suffix)),
mismatch.describe())
def test_describe_non_ascii_bytes(self):
string = _b("A\xA7")
suffix = _b("B\xA7")
mismatch = DoesNotStartWith(string, suffix)
self.assertEqual("%r does not start with %r." % (string, suffix),
mismatch.describe())
class StartsWithTests(TestCase):
@ -559,7 +715,17 @@ class StartsWithTests(TestCase):
def test_str(self):
matcher = StartsWith("bar")
self.assertEqual("Starts with 'bar'.", str(matcher))
self.assertEqual("StartsWith('bar')", str(matcher))
def test_str_with_bytes(self):
b = _b("\xA7")
matcher = StartsWith(b)
self.assertEqual("StartsWith(%r)" % (b,), str(matcher))
def test_str_with_unicode(self):
u = _u("\xA7")
matcher = StartsWith(u)
self.assertEqual("StartsWith(%r)" % (u,), str(matcher))
def test_match(self):
matcher = StartsWith("bar")
@ -588,6 +754,21 @@ class DoesNotEndWithTests(TestCase):
mismatch = DoesNotEndWith("fo", "bo")
self.assertEqual("'fo' does not end with 'bo'.", mismatch.describe())
def test_describe_non_ascii_unicode(self):
string = _u("A\xA7")
suffix = _u("B\xA7")
mismatch = DoesNotEndWith(string, suffix)
self.assertEqual("%s does not end with %s." % (
text_repr(string), text_repr(suffix)),
mismatch.describe())
def test_describe_non_ascii_bytes(self):
string = _b("A\xA7")
suffix = _b("B\xA7")
mismatch = DoesNotEndWith(string, suffix)
self.assertEqual("%r does not end with %r." % (string, suffix),
mismatch.describe())
class EndsWithTests(TestCase):
@ -595,7 +776,17 @@ class EndsWithTests(TestCase):
def test_str(self):
matcher = EndsWith("bar")
self.assertEqual("Ends with 'bar'.", str(matcher))
self.assertEqual("EndsWith('bar')", str(matcher))
def test_str_with_bytes(self):
b = _b("\xA7")
matcher = EndsWith(b)
self.assertEqual("EndsWith(%r)" % (b,), str(matcher))
def test_str_with_unicode(self):
u = _u("\xA7")
matcher = EndsWith(u)
self.assertEqual("EndsWith(%r)" % (u,), str(matcher))
def test_match(self):
matcher = EndsWith("arf")
@ -712,11 +903,17 @@ class TestMatchesRegex(TestCase, TestMatchersInterface):
("MatchesRegex('a|b')", MatchesRegex('a|b')),
("MatchesRegex('a|b', re.M)", MatchesRegex('a|b', re.M)),
("MatchesRegex('a|b', re.I|re.M)", MatchesRegex('a|b', re.I|re.M)),
("MatchesRegex(%r)" % (_b("\xA7"),), MatchesRegex(_b("\xA7"))),
("MatchesRegex(%r)" % (_u("\xA7"),), MatchesRegex(_u("\xA7"))),
]
describe_examples = [
("'c' does not match /a|b/", 'c', MatchesRegex('a|b')),
("'c' does not match /a\d/", 'c', MatchesRegex(r'a\d')),
("%r does not match /\\s+\\xa7/" % (_b('c'),),
_b('c'), MatchesRegex(_b("\\s+\xA7"))),
("%r does not match /\\s+\\xa7/" % (_u('c'),),
_u('c'), MatchesRegex(_u("\\s+\xA7"))),
]

View File

@ -19,7 +19,10 @@ from testtools import (
skipUnless,
testcase,
)
from testtools.compat import _b
from testtools.compat import (
_b,
_u,
)
from testtools.matchers import (
Annotate,
DocTestMatches,
@ -32,6 +35,7 @@ from testtools.testresult.doubles import (
Python27TestResult,
ExtendedTestResult,
)
from testtools.testresult.real import TestResult
from testtools.tests.helpers import (
an_exc_info,
FullStackRunTest,
@ -484,7 +488,7 @@ class TestAssertions(TestCase):
matchee = 'foo'
matcher = Equals('bar')
expected = (
'Match failed. Matchee: "%s"\n'
'Match failed. Matchee: %r\n'
'Matcher: %s\n'
'Difference: %s\n' % (
matchee,
@ -494,6 +498,48 @@ class TestAssertions(TestCase):
self.assertFails(
expected, self.assertThat, matchee, matcher, verbose=True)
def get_error_string(self, e):
"""Get the string showing how 'e' would be formatted in test output.
This is a little bit hacky, since it's designed to give consistent
output regardless of Python version.
In testtools, TestResult._exc_info_to_unicode is the point of dispatch
between various different implementations of methods that format
exceptions, so that's what we have to call. However, that method cares
about stack traces and formats the exception class. We don't care
about either of these, so we take its output and parse it a little.
"""
error = TestResult()._exc_info_to_unicode((e.__class__, e, None), self)
# We aren't at all interested in the traceback.
if error.startswith('Traceback (most recent call last):\n'):
lines = error.splitlines(True)[1:]
for i, line in enumerate(lines):
if not line.startswith(' '):
break
error = ''.join(lines[i:])
# We aren't interested in how the exception type is formatted.
exc_class, error = error.split(': ', 1)
return error
def test_assertThat_verbose_unicode(self):
# When assertThat is given matchees or matchers that contain non-ASCII
# unicode strings, we can still provide a meaningful error.
matchee = _u('\xa7')
matcher = Equals(_u('a'))
expected = (
'Match failed. Matchee: %s\n'
'Matcher: %s\n'
'Difference: %s\n\n' % (
repr(matchee).replace("\\xa7", matchee),
matcher,
matcher.match(matchee).describe(),
))
e = self.assertRaises(
self.failureException, self.assertThat, matchee, matcher,
verbose=True)
self.assertEqual(expected, self.get_error_string(e))
def test_assertEqual_nice_formatting(self):
message = "These things ought not be equal."
a = ['apple', 'banana', 'cherry']
@ -519,6 +565,21 @@ class TestAssertions(TestCase):
self.assertFails(expected_error, self.assertEquals, a, b)
self.assertFails(expected_error, self.failUnlessEqual, a, b)
def test_assertEqual_non_ascii_str_with_newlines(self):
message = _u("Be careful mixing unicode and bytes")
a = "a\n\xa7\n"
b = "Just a longish string so the more verbose output form is used."
expected_error = '\n'.join([
'!=:',
"reference = '''\\",
'a',
repr('\xa7')[1:-1],
"'''",
'actual = %r' % (b,),
': ' + message,
])
self.assertFails(expected_error, self.assertEqual, a, b, message)
def test_assertIsNone(self):
self.assertIsNone(None)