Index: Doc/library/unittest.rst =================================================================== --- Doc/library/unittest.rst (revision 85670) +++ Doc/library/unittest.rst (working copy) @@ -798,7 +798,8 @@ .. versionchanged:: 3.2 :meth:`assertMultiLineEqual` added as the default type equality - function for comparing strings. + function for comparing strings, and :meth:`assertBytesEqual` + for comparing bytes. .. deprecated:: 3.1 :meth:`failUnlessEqual`; use :meth:`assertEqual`. @@ -895,6 +896,23 @@ .. versionadded:: 3.1 + .. method:: assertBytesEqual(self, first, second, splitat=None, msg=None) + + Test that the byte string *first* is equal to the byte string *second*. + When not equal a diff of the reprs of the two byte strings highlighting + the differences will be included in the error message. If *splitat* is + specified, the input byte strings are split into pieces at any + occurrences of the *splitat* bytestring, and the generated diff compares + the pieces pairwise. This can produce more meaningful diffs when the + byte strings being compared contain structured data with a useful break + point. This method is used by default when comparing bytes objects with + :meth:`assertEqual`. + + If specified, *msg* will be used as the error message on failure. + + .. versionadded:: 3.2 + + .. method:: assertRegexpMatches(text, regexp, msg=None) Verifies that a *regexp* search matches *text*. Fails with an error @@ -1234,8 +1252,8 @@ methods that report diffs on failure. It defaults to 80*8 characters. Assert methods affected by this attribute are :meth:`assertSequenceEqual` (including all the sequence comparison - methods that delegate to it), :meth:`assertDictEqual` and - :meth:`assertMultiLineEqual`. + methods that delegate to it), :meth:`assertDictEqual`, + :meth:`assertMultiLineEqual` and :meth:`assertBytesEqual`. Setting ``maxDiff`` to None means that there is no maximum length of diffs. Index: Lib/unittest/test/test_case.py =================================================================== --- Lib/unittest/test/test_case.py (revision 85670) +++ Lib/unittest/test/test_case.py (working copy) @@ -671,6 +671,18 @@ else: self.fail('assertMultiLineEqual did not fail') + def testAssertBytesEqualTruncates(self): + test = unittest.TestCase('assertEqual') + def truncate(msg, diff): + return 'foo' + test._truncateMessage = truncate + try: + test.assertBytesEqual(b'foo', b'bar') + except self.failureException as e: + self.assertEqual(str(e), 'foo') + else: + self.fail('assertBytesEqual did not fail') + def testAssertItemsEqual(self): a = object() self.assertItemsEqual([1, 2, 3], [3, 2, 1]) @@ -858,6 +870,76 @@ error = str(e).split('\n', 1)[1] self.assertTrue(sample_text_error == error) + def testAssertBytesEqual(self): + sample_bytes = bytes([25, 49, 230, 190, 17, 156]) + revised_sample_bytes = bytes([25, 49, 23, 190, 17, 156]) + # The carats look like they are off here because we have to escape the \s. + sample_error_text = """\ +- b'\\x191\\xe6\\xbe\\x11\\x9c' +? ^^ ++ b'\\x191\\x17\\xbe\\x11\\x9c' +? ^^ +""" + try: + self.assertBytesEqual(sample_bytes, revised_sample_bytes) + except self.failureException as e: + # need to remove the first line of the error message + error = str(e).split('\n', 1)[1] + self.assertEqual(sample_error_text, error) + + def testAssertBytesEqualWithSplitInMiddle(self): + sample_bytes = bytes([25, 49, 230, 190, 17, 156]) + revised_sample_bytes = bytes([25, 49, 23, 190, 17, 156]) + # The carats look like they are off here because we have to escape the \s. + sample_error_text = """\ +- b'\\x191\\xe6\\xbe' +? ^^ ++ b'\\x191\\x17\\xbe' +? ^^ + b'\\x11\\x9c' +""" + try: + self.assertBytesEqual(sample_bytes, revised_sample_bytes, + splitat=bytes((190,))) + except self.failureException as e: + # need to remove the first line of the error message + error = str(e).split('\n', 1)[1] + self.assertEqual(sample_error_text, error) + + def testAssertBytesEqualWithSplitCharAtEnd(self): + sample_text = b"""\ +http://www.python.org/doc/2.3/lib/module-unittest.html +test case + A test case is the smallest unit of testing. [...] +""" + revised_sample_text = b"""\ +http://www.python.org/doc/2.4.1/lib/module-unittest.html +test case + A test case is the smallest unit of testing. [...] You may provide your + own implementation that does not subclass from TestCase, of course. +""" + sample_text_error = """\ +- b'http://www.python.org/doc/2.3/lib/module-unittest.html\\n' +? ^ ++ b'http://www.python.org/doc/2.4.1/lib/module-unittest.html\\n' +? ^^^ + b'test case\\n' +- b' A test case is the smallest unit of testing. [...]\\n' ++ b' A test case is the smallest unit of testing. [...] You may provide your\\n' +? +++++++++++++++++++++ ++ b' own implementation that does not subclass from TestCase, of course.\\n' +""" + self.maxDiff = None + try: + self.assertBytesEqual(sample_text, revised_sample_text, splitat=b'\n') + except self.failureException as e: + # need to remove the first line of the error message + error = str(e).split('\n', 1)[1] + # We're comparing strings here, so we can use assertEqual. But be + # warned, if this fails you have to look at the output carefully to + # sort out the real failures from the sample failure lines :) + self.assertEqual(sample_text_error, error) + def testAssertIsNone(self): self.assertIsNone(None) self.assertRaises(self.failureException, self.assertIsNone, False) Index: Lib/unittest/case.py =================================================================== --- Lib/unittest/case.py (revision 85670) +++ Lib/unittest/case.py (working copy) @@ -260,6 +260,7 @@ self.addTypeEqualityFunc(set, self.assertSetEqual) self.addTypeEqualityFunc(frozenset, self.assertSetEqual) self.addTypeEqualityFunc(str, self.assertMultiLineEqual) + self.addTypeEqualityFunc(bytes, self.assertBytesEqual) def addTypeEqualityFunc(self, typeobj, function): """Add a type specific assertEqual style function to compare a type. @@ -1035,6 +1036,33 @@ standardMsg = self._truncateMessage(standardMsg, diff) self.fail(self._formatMessage(msg, standardMsg)) + def assertBytesEqual(self, first, second, splitat=None, msg=None): + """Assert that two multi-line byte strings are equal.""" + self.assert_(isinstance(first, bytes), ( + 'First argument is not a byte string')) + self.assert_(isinstance(second, bytes), ( + 'Second argument is not a byte string')) + + if first != second: + if splitat: + firstlines = first.split(splitat) + secondlines = second.split(splitat) + firstlines = ([safe_repr(x+splitat)+'\n' + for x in firstlines[:-1]] + + ([safe_repr(firstlines[-1])] if firstlines[-1] else [])) + secondlines = ([safe_repr(x+splitat)+'\n' + for x in secondlines[:-1]] + + ([safe_repr(secondlines[-1])] if secondlines[-1] else [])) + else: + firstlines = [safe_repr(first)+'\n'] + secondlines = [safe_repr(second)+'\n'] + standardMsg = '%s != %s' % (safe_repr(first, True), + safe_repr(second, True)) + + diff = '\n' + ''.join(difflib.ndiff(firstlines, secondlines)) + standardMsg = self._truncateMessage(standardMsg, diff) + self.fail(self._formatMessage(msg, standardMsg)) + def assertLess(self, a, b, msg=None): """Just like self.assertTrue(a < b), but with a nicer default message.""" if not a < b: