subunit: Import latest upstream.
authorJelmer Vernooij <jelmer@samba.org>
Sat, 4 Sep 2010 21:04:28 +0000 (23:04 +0200)
committerJelmer Vernooij <jelmer@samba.org>
Sat, 4 Sep 2010 21:04:28 +0000 (23:04 +0200)
13 files changed:
lib/subunit/MANIFEST.in [new file with mode: 0644]
lib/subunit/NEWS
lib/subunit/README
lib/subunit/configure.ac
lib/subunit/filters/subunit-ls
lib/subunit/perl/lib/Subunit.pm
lib/subunit/python/subunit/__init__.py
lib/subunit/python/subunit/details.py
lib/subunit/python/subunit/run.py
lib/subunit/python/subunit/test_results.py
lib/subunit/python/subunit/tests/test_details.py
lib/subunit/python/subunit/tests/test_test_protocol.py
lib/subunit/setup.py [new file with mode: 0755]

diff --git a/lib/subunit/MANIFEST.in b/lib/subunit/MANIFEST.in
new file mode 100644 (file)
index 0000000..7c449cf
--- /dev/null
@@ -0,0 +1,21 @@
+exclude .bzrignore
+exclude aclocal.m4
+prune autom4te.cache
+prune c
+prune c++
+prune compile
+exclude configure*
+exclude depcomp
+exclude INSTALL
+exclude install-sh
+exclude lib*
+exclude ltmain.sh
+prune m4
+exclude Makefile*
+exclude missing
+prune perl
+exclude py-compile
+prune shell
+prune python/iso8601
+exclude stamp-h1
+include NEWS
index 7c933c8f6e68cc37f4822528d2c59d16a4a1319f..1af8ef57081202d9cc27570d17edf757a1c62d1a 100644 (file)
@@ -5,9 +5,20 @@ subunit release notes
 NEXT (In development)
 ---------------------
 
+0.0.6
+-----
+
+This release of subunit fixes a number of unicode related bugs. This depends on
+testtools 0.9.4 and will not function without it. Thanks to Tres Seaver there
+is also an optional native setup.py file for use with easy_install and the
+like.
+
 BUG FIXES
 ~~~~~~~~~
 
+* Be consistent about delivering unicode content to testtools StringException
+  class which has become (appropriately) conservative. (Robert Collins)
+
 * Fix incorrect reference to subunit_test_failf in c/README.
   (Brad Hards, #524341)
 
@@ -15,6 +26,28 @@ BUG FIXES
   is purely cosmetic as the parameters are passed down with no interpretation.
   (Robert Collins, #537611)
 
+* Old style tracebacks with no encoding info are now treated as UTF8 rather
+  than some-random-codec-like-ascii. (Robert Collins)
+
+* On windows, ProtocolTestCase and TestProtocolClient will set their streams to
+  binary mode by calling into msvcrt; this avoids having their input or output
+  mangled by the default line ending translation on that platform.
+  (Robert Collins, Martin [gz], #579296)
+
+IMPROVEMENTS
+~~~~~~~~~~~~
+
+* Subunit now has a setup.py for python deployments that are not using
+  distribution packages. (Tres Seaver, #538181)
+
+* Subunit now supports test discovery by building on the testtools support for
+  it. You can take advantage of it with "python -m subunit.run discover [path]"
+  and see "python -m subunit.run discover --help" for more options.
+
+* Subunit now uses the improved unicode support in testtools when outputting
+  non-details based test information; this should consistently UTF8 encode such
+  strings.
+
 0.0.5
 -----
 
index 9740d013a53487cff602abe9f06867a588fa9ad9..6ac258485fc80e623ff07f9d124520bc24422745 100644 (file)
@@ -160,7 +160,7 @@ tags: [-]TAG ...
 time: YYYY-MM-DD HH:MM:SSZ
 
 DETAILS ::= BRACKETED | MULTIPART
-BRACKETED ::= '[' CR lines ']' CR
+BRACKETED ::= '[' CR UTF8-lines ']' CR
 MULTIPART ::= '[ multipart' CR PART* ']' CR
 PART ::= PART_TYPE CR NAME CR PART_BYTES CR
 PART_TYPE ::= Content-Type: type/sub-type(;parameter=value,parameter=value)
index 496aea5719d05a7c684e085b8779ce497b38c265..569657346423a2072d727eefc06fbb4f50124a36 100644 (file)
@@ -1,6 +1,6 @@
 m4_define([SUBUNIT_MAJOR_VERSION], [0])
 m4_define([SUBUNIT_MINOR_VERSION], [0])
-m4_define([SUBUNIT_MICRO_VERSION], [5])
+m4_define([SUBUNIT_MICRO_VERSION], [6])
 m4_define([SUBUNIT_VERSION],
 m4_defn([SUBUNIT_MAJOR_VERSION]).m4_defn([SUBUNIT_MINOR_VERSION]).m4_defn([SUBUNIT_MICRO_VERSION]))
 AC_PREREQ([2.59])
index 15ec4b01e6ffbd4861aa66f92c37dd45d1a38510..86461347d36b047ddbdf83b61a5e23dff3a4819a 100755 (executable)
 
 from optparse import OptionParser
 import sys
-import unittest
 
 from subunit import DiscardStream, ProtocolTestCase
-
-class TestIdPrintingResult(unittest.TestResult):
-
-    def __init__(self, stream, show_times=False):
-        """Create a FilterResult object outputting to stream."""
-        unittest.TestResult.__init__(self)
-        self._stream = stream
-        self.failed_tests = 0
-        self.__time = 0
-        self.show_times = show_times
-        self._test = None
-        self._test_duration = 0
-        
-    def addError(self, test, err):
-        self.failed_tests += 1
-        self._test = test
-
-    def addFailure(self, test, err):
-        self.failed_tests += 1
-        self._test = test
-
-    def addSuccess(self, test):
-        self._test = test
-
-    def reportTest(self, test, duration):
-        if self.show_times:
-            seconds = duration.seconds
-            seconds += duration.days * 3600 * 24
-            seconds += duration.microseconds / 1000000.0
-            self._stream.write(test.id() + ' %0.3f\n' % seconds)
-        else:
-            self._stream.write(test.id() + '\n')
-
-    def startTest(self, test):
-        self._start_time = self._time()
-
-    def stopTest(self, test):
-        test_duration = self._time() - self._start_time
-        self.reportTest(self._test, test_duration)
-
-    def time(self, time):
-        self.__time = time
-
-    def _time(self):
-        return self.__time
-
-    def wasSuccessful(self):
-        "Tells whether or not this result was a success"
-        return self.failed_tests == 0
+from subunit.test_results import TestIdPrintingResult
 
 
 parser = OptionParser(description=__doc__)
index 05206748e2daf1c2a58a18e81e1a09bb441d7b96..dac4a2601df48683c552458be8fc828c807ef125 100644 (file)
@@ -159,4 +159,25 @@ sub report_time($)
        printf "time: %04d-%02d-%02d %02d:%02d:%02dZ\n", $year+1900, $mon, $mday, $hour, $min, $sec;
 }
 
+sub progress_pop()
+{
+       print "progress: pop\n";
+}
+
+sub progress_push()
+{
+       print "progress: push\n";
+}
+
+sub progress($;$)
+{
+       my ($count, $whence) = @_;
+
+       unless(defined($whence)) {
+               $whence = "";
+       }
+
+       print "progress: $whence$count\n";
+}
+
 1;
index 4b25ca3a39fc7a3dfd44aad75966e95f5108b052..b6f0108f618d4b2ef52bab3e1db69724d3b22e12 100644 (file)
@@ -133,9 +133,7 @@ try:
 except ImportError:
     raise ImportError ("testtools.testresult.real does not contain "
         "_StringException, check your version.")
-
-
-from testtools.testresult.real import _StringException
+from testtools import testresult
 
 import chunked, details, test_results
 
@@ -244,7 +242,7 @@ class _ParserState(object):
 
     def lostConnection(self):
         """Connection lost."""
-        self.parser._lostConnectionInTest('unknown state of ')
+        self.parser._lostConnectionInTest(u'unknown state of ')
 
     def startTest(self, offset, line):
         """A test start command received."""
@@ -324,7 +322,7 @@ class _InTest(_ParserState):
 
     def lostConnection(self):
         """Connection lost."""
-        self.parser._lostConnectionInTest('')
+        self.parser._lostConnectionInTest(u'')
 
 
 class _OutSideTest(_ParserState):
@@ -359,7 +357,7 @@ class _ReadingDetails(_ParserState):
 
     def lostConnection(self):
         """Connection lost."""
-        self.parser._lostConnectionInTest('%s report of ' %
+        self.parser._lostConnectionInTest(u'%s report of ' %
             self._outcome_label())
 
     def _outcome_label(self):
@@ -501,7 +499,7 @@ class TestProtocolServer(object):
         self._state.lineReceived(line)
 
     def _lostConnectionInTest(self, state_string):
-        error_string = "lost connection during %stest '%s'" % (
+        error_string = u"lost connection during %stest '%s'" % (
             state_string, self.current_test_description)
         self.client.addError(self._current_test, RemoteError(error_string))
         self.client.stopTest(self._current_test)
@@ -531,7 +529,7 @@ class TestProtocolServer(object):
         self._stream.write(line)
 
 
-class TestProtocolClient(unittest.TestResult):
+class TestProtocolClient(testresult.TestResult):
     """A TestResult which generates a subunit stream for a test run.
     
     # Get a TestSuite or TestCase to run
@@ -550,8 +548,9 @@ class TestProtocolClient(unittest.TestResult):
     """
 
     def __init__(self, stream):
-        unittest.TestResult.__init__(self)
+        testresult.TestResult.__init__(self)
         self._stream = stream
+        _make_stream_binary(stream)
 
     def addError(self, test, error=None, details=None):
         """Report an error in test test.
@@ -618,8 +617,10 @@ class TestProtocolClient(unittest.TestResult):
             raise ValueError
         if error is not None:
             self._stream.write(" [\n")
-            for line in self._exc_info_to_string(error, test).splitlines():
-                self._stream.write("%s\n" % line)
+            # XXX: this needs to be made much stricter, along the lines of
+            # Martin[gz]'s work in testtools. Perhaps subunit can use that?
+            for line in self._exc_info_to_unicode(error, test).splitlines():
+                self._stream.write(("%s\n" % line).encode('utf8'))
         else:
             self._write_details(details)
         self._stream.write("]\n")
@@ -704,7 +705,7 @@ class TestProtocolClient(unittest.TestResult):
         """Obey the testtools result.done() interface."""
 
 
-def RemoteError(description=""):
+def RemoteError(description=u""):
     return (_StringException, _StringException(description), None)
 
 
@@ -754,7 +755,7 @@ class RemotedTestCase(unittest.TestCase):
     def run(self, result=None):
         if result is None: result = self.defaultTestResult()
         result.startTest(self)
-        result.addError(self, RemoteError("Cannot run RemotedTestCases.\n"))
+        result.addError(self, RemoteError(u"Cannot run RemotedTestCases.\n"))
         result.stopTest(self)
 
     def _strclass(self):
@@ -784,7 +785,7 @@ class ExecTestCase(unittest.TestCase):
 
     def debug(self):
         """Run the test without collecting errors in a TestResult"""
-        self._run(unittest.TestResult())
+        self._run(testresult.TestResult())
 
     def _run(self, result):
         protocol = TestProtocolServer(result)
@@ -816,7 +817,7 @@ class IsolatedTestSuite(unittest.TestSuite):
     """
 
     def run(self, result=None):
-        if result is None: result = unittest.TestResult()
+        if result is None: result = testresult.TestResult()
         run_isolated(unittest.TestSuite, self, result)
 
 
@@ -1045,8 +1046,10 @@ class ProtocolTestCase(object):
             subunit input is not forwarded.
         """
         self._stream = stream
+        _make_stream_binary(stream)
         self._passthrough = passthrough
         self._forward = forward
+        _make_stream_binary(forward)
 
     def __call__(self, result=None):
         return self.run(result)
@@ -1062,7 +1065,7 @@ class ProtocolTestCase(object):
         protocol.lostConnection()
 
 
-class TestResultStats(unittest.TestResult):
+class TestResultStats(testresult.TestResult):
     """A pyunit TestResult interface implementation for making statistics.
     
     :ivar total_tests: The total tests seen.
@@ -1073,7 +1076,7 @@ class TestResultStats(unittest.TestResult):
 
     def __init__(self, stream):
         """Create a TestResultStats which outputs to stream."""
-        unittest.TestResult.__init__(self)
+        testresult.TestResult.__init__(self)
         self._stream = stream
         self.failed_tests = 0
         self.skipped_tests = 0
@@ -1124,3 +1127,14 @@ def get_default_formatter():
     else:
         return sys.stdout
 
+
+def _make_stream_binary(stream):
+    """Ensure that a stream will be binary safe. See _make_binary_on_windows."""
+    if getattr(stream, 'fileno', None) is not None:
+        _make_binary_on_windows(stream.fileno())
+
+def _make_binary_on_windows(fileno):
+    """Win32 mangles \r\n to \n and that breaks streams. See bug lp:505078."""
+    if sys.platform == "win32":
+        import msvcrt
+        msvcrt.setmode(fileno, os.O_BINARY)
index 65a04046d9b89a2df3f5ff0442fa4ddd8a182517..a37b2acb932dd958baa29ec5be7e5d92cba8c3e1 100644 (file)
@@ -47,8 +47,12 @@ class SimpleDetailsParser(DetailsParser):
     def get_details(self, style=None):
         result = {}
         if not style:
+            # We know that subunit/testtools serialise [] formatted
+            # tracebacks as utf8, but perhaps we need a ReplacingContent
+            # or something like that.
             result['traceback'] = content.Content(
-                content_type.ContentType("text", "x-traceback"),
+                content_type.ContentType("text", "x-traceback",
+                {"charset": "utf8"}),
                 lambda:[self._message])
         else:
             if style == 'skip':
@@ -92,7 +96,7 @@ class MultipartDetailsParser(DetailsParser):
         residue = self._chunk_parser.write(line)
         if residue is not None:
             # Line based use always ends on no residue.
-            assert residue == ''
+            assert residue == '', 'residue: %r' % (residue,)
             body = self._body
             self._details[self._name] = content.Content(
                 self._content_type, lambda:[body.getvalue()])
index e57939fdfa52d8041c549697e7cfdb3cf1c5f11e..daa241a606bf6630408fd066f0ea91b5629c457a 100755 (executable)
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/python
 #
 # Simple subunit testrunner for python
 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007
 import sys
 
 from subunit import TestProtocolClient, get_default_formatter
+from testtools.run import (
+    BUFFEROUTPUT,
+    CATCHBREAK,
+    FAILFAST,
+    TestProgram,
+    USAGE_AS_MAIN,
+    )
 
 
 class SubunitTestRunner(object):
@@ -36,12 +43,30 @@ class SubunitTestRunner(object):
         return result
 
 
+class SubunitTestProgram(TestProgram):
+
+    USAGE = USAGE_AS_MAIN
+
+    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
+        usage_text = self.USAGE % usage
+        usage_lines = usage_text.split('\n')
+        usage_lines.insert(2, "Run a test suite with a subunit reporter.")
+        usage_lines.insert(3, "")
+        print('\n'.join(usage_lines))
+        sys.exit(2)
+
+
 if __name__ == '__main__':
-    import optparse
-    from unittest import TestProgram
-    parser = optparse.OptionParser(__doc__)
-    args = parser.parse_args()[1]
     stream = get_default_formatter()
     runner = SubunitTestRunner(stream)
-    program = TestProgram(module=None, argv=[sys.argv[0]] + args,
-                          testRunner=runner)
+    SubunitTestProgram(module=None, argv=sys.argv, testRunner=runner)
index 6cf84c519e9bc4feccd0ed1cea9c55c893e23cae..1c91daadc6a470dce78be0cfd89eb1dc20e715f1 100644 (file)
@@ -6,7 +6,7 @@
 #  license at the users choice. A copy of both licenses are available in the
 #  project source as Apache-2.0 and BSD. You may not use this file except in
 #  compliance with one of these two licences.
-#  
+#
 #  Unless required by applicable law or agreed to in writing, software
 #  distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
 #  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
@@ -21,16 +21,14 @@ import datetime
 import iso8601
 import testtools
 
-import subunit
-
 
 # NOT a TestResult, because we are implementing the interface, not inheriting
 # it.
 class TestResultDecorator(object):
     """General pass-through decorator.
 
-    This provides a base that other TestResults can inherit from to 
-    gain basic forwarding functionality. It also takes care of 
+    This provides a base that other TestResults can inherit from to
+    gain basic forwarding functionality. It also takes care of
     handling the case where the target doesn't support newer methods
     or features by degrading them.
     """
@@ -201,11 +199,11 @@ class TestResultFilter(TestResultDecorator):
     """A pyunit TestResult interface implementation which filters tests.
 
     Tests that pass the filter are handed on to another TestResult instance
-    for further processing/reporting. To obtain the filtered results, 
+    for further processing/reporting. To obtain the filtered results,
     the other instance must be interrogated.
 
     :ivar result: The result that tests are passed to after filtering.
-    :ivar filter_predicate: The callback run to decide whether to pass 
+    :ivar filter_predicate: The callback run to decide whether to pass
         a result.
     """
 
@@ -213,7 +211,7 @@ class TestResultFilter(TestResultDecorator):
         filter_success=True, filter_skip=False,
         filter_predicate=None):
         """Create a FilterResult object filtering to result.
-        
+
         :param filter_error: Filter out errors.
         :param filter_failure: Filter out failures.
         :param filter_success: Filter out successful tests.
@@ -238,9 +236,9 @@ class TestResultFilter(TestResultDecorator):
         self._current_test_filtered = None
         # The (new, gone) tags for the current test.
         self._current_test_tags = None
-        
+
     def addError(self, test, err=None, details=None):
-        if (not self._filter_error and 
+        if (not self._filter_error and
             self.filter_predicate(test, 'error', err, details)):
             self.decorated.startTest(test)
             self.decorated.addError(test, err, details=details)
@@ -288,17 +286,17 @@ class TestResultFilter(TestResultDecorator):
 
     def startTest(self, test):
         """Start a test.
-        
+
         Not directly passed to the client, but used for handling of tags
         correctly.
         """
         self._current_test = test
         self._current_test_filtered = False
         self._current_test_tags = set(), set()
-    
+
     def stopTest(self, test):
         """Stop a test.
-        
+
         Not directly passed to the client, but used for handling of tags
         correctly.
         """
@@ -316,7 +314,7 @@ class TestResultFilter(TestResultDecorator):
 
         Adds and removes tags as appropriate. If a test is currently running,
         tags are not affected for subsequent tests.
-        
+
         :param new_tags: Tags to add,
         :param gone_tags: Tags to remove.
         """
@@ -332,3 +330,53 @@ class TestResultFilter(TestResultDecorator):
         if id.startswith("subunit.RemotedTestCase."):
             return id[len("subunit.RemotedTestCase."):]
         return id
+
+
+class TestIdPrintingResult(testtools.TestResult):
+
+    def __init__(self, stream, show_times=False):
+        """Create a FilterResult object outputting to stream."""
+        testtools.TestResult.__init__(self)
+        self._stream = stream
+        self.failed_tests = 0
+        self.__time = 0
+        self.show_times = show_times
+        self._test = None
+        self._test_duration = 0
+
+    def addError(self, test, err):
+        self.failed_tests += 1
+        self._test = test
+
+    def addFailure(self, test, err):
+        self.failed_tests += 1
+        self._test = test
+
+    def addSuccess(self, test):
+        self._test = test
+
+    def reportTest(self, test, duration):
+        if self.show_times:
+            seconds = duration.seconds
+            seconds += duration.days * 3600 * 24
+            seconds += duration.microseconds / 1000000.0
+            self._stream.write(test.id() + ' %0.3f\n' % seconds)
+        else:
+            self._stream.write(test.id() + '\n')
+
+    def startTest(self, test):
+        self._start_time = self._time()
+
+    def stopTest(self, test):
+        test_duration = self._time() - self._start_time
+        self.reportTest(self._test, test_duration)
+
+    def time(self, time):
+        self.__time = time
+
+    def _time(self):
+        return self.__time
+
+    def wasSuccessful(self):
+        "Tells whether or not this result was a success"
+        return self.failed_tests == 0
index 2700d4afc75c7fa223605e7d683dba173dd05327..41c32129d04f5c02bce69b8d136a84ced6e571a2 100644 (file)
@@ -51,7 +51,8 @@ class TestSimpleDetails(unittest.TestCase):
         traceback = ""
         expected = {}
         expected['traceback'] = content.Content(
-            content_type.ContentType("text", "x-traceback"),
+            content_type.ContentType("text", "x-traceback",
+                {'charset': 'utf8'}),
             lambda:[""])
         found = parser.get_details()
         self.assertEqual(expected.keys(), found.keys())
index f10380b09b69323c850a9ae6daa9e267a137d0f9..e1287b6c8111549f180568ac97004671993ef4da 100644 (file)
@@ -102,6 +102,9 @@ class TestTestProtocolServerPipe(unittest.TestCase):
                 "------------\n\n")])
         self.assertEqual(client.testsRun, 3)
 
+    def test_non_test_characters_forwarded_immediately(self):
+        pass
+
 
 class TestTestProtocolServerStartTest(unittest.TestCase):
 
@@ -243,7 +246,8 @@ class TestTestProtocolServerPassThrough(unittest.TestCase):
         self.protocol.lineReceived("]\n")
         self.assertEqual(self.stdout.getvalue(), "")
         details = {}
-        details['traceback'] = Content(ContentType("text", "x-traceback"),
+        details['traceback'] = Content(ContentType("text", "x-traceback",
+            {'charset': 'utf8'}),
             lambda:[
             "test old mcdonald\n"
             "failure a\n"
@@ -285,7 +289,7 @@ class TestTestProtocolServerLostConnection(unittest.TestCase):
         self.protocol.lineReceived("test old mcdonald\n")
         self.protocol.lostConnection()
         failure = subunit.RemoteError(
-            "lost connection during test 'old mcdonald'")
+            u"lost connection during test 'old mcdonald'")
         self.assertEqual([
             ('startTest', self.test),
             ('addError', self.test, failure),
@@ -298,7 +302,7 @@ class TestTestProtocolServerLostConnection(unittest.TestCase):
         self.protocol.lostConnection()
         self.assertEqual([
             ('startTest', self.test),
-            ('addError', self.test, subunit.RemoteError("")),
+            ('addError', self.test, subunit.RemoteError(u"")),
             ('stopTest', self.test),
             ], self.client._events)
 
@@ -307,7 +311,7 @@ class TestTestProtocolServerLostConnection(unittest.TestCase):
         self.protocol.lineReceived("%s old mcdonald %s" % (outcome, opening))
         self.protocol.lostConnection()
         failure = subunit.RemoteError(
-            "lost connection during %s report of test 'old mcdonald'" % 
+            u"lost connection during %s report of test 'old mcdonald'" % 
             outcome)
         self.assertEqual([
             ('startTest', self.test),
@@ -327,7 +331,7 @@ class TestTestProtocolServerLostConnection(unittest.TestCase):
         self.protocol.lostConnection()
         self.assertEqual([
             ('startTest', self.test),
-            ('addFailure', self.test, subunit.RemoteError("")),
+            ('addFailure', self.test, subunit.RemoteError(u"")),
             ('stopTest', self.test),
             ], self.client._events)
 
@@ -411,8 +415,8 @@ class TestTestProtocolServerAddError(unittest.TestCase):
         self.protocol.lineReceived("error mcdonalds farm [\n")
         self.protocol.lineReceived("]\n")
         details = {}
-        details['traceback'] = Content(ContentType("text", "x-traceback"),
-            lambda:[""])
+        details['traceback'] = Content(ContentType("text", "x-traceback",
+            {'charset': 'utf8'}), lambda:[""])
         self.assertEqual([
             ('startTest', self.test),
             ('addError', self.test, details),
@@ -424,8 +428,8 @@ class TestTestProtocolServerAddError(unittest.TestCase):
         self.protocol.lineReceived(" ]\n")
         self.protocol.lineReceived("]\n")
         details = {}
-        details['traceback'] = Content(ContentType("text", "x-traceback"),
-            lambda:["]\n"])
+        details['traceback'] = Content(ContentType("text", "x-traceback",
+            {'charset': 'utf8'}), lambda:["]\n"])
         self.assertEqual([
             ('startTest', self.test),
             ('addError', self.test, details),
@@ -469,8 +473,8 @@ class TestTestProtocolServerAddFailure(unittest.TestCase):
         self.protocol.lineReceived("failure mcdonalds farm [\n")
         self.protocol.lineReceived("]\n")
         details = {}
-        details['traceback'] = Content(ContentType("text", "x-traceback"),
-            lambda:[""])
+        details['traceback'] = Content(ContentType("text", "x-traceback",
+            {'charset': 'utf8'}), lambda:[""])
         self.assertFailure(details)
 
     def failure_quoted_bracket(self, keyword):
@@ -478,8 +482,8 @@ class TestTestProtocolServerAddFailure(unittest.TestCase):
         self.protocol.lineReceived(" ]\n")
         self.protocol.lineReceived("]\n")
         details = {}
-        details['traceback'] = Content(ContentType("text", "x-traceback"),
-            lambda:["]\n"])
+        details['traceback'] = Content(ContentType("text", "x-traceback",
+            {'charset': 'utf8'}), lambda:["]\n"])
         self.assertFailure(details)
 
     def test_failure_quoted_bracket(self):
@@ -535,12 +539,13 @@ class TestTestProtocolServerAddxFail(unittest.TestCase):
             details = {}
             if error_message is not None:
                 details['traceback'] = Content(
-                    ContentType("text", "x-traceback"), lambda:[error_message])
+                    ContentType("text", "x-traceback", {'charset': 'utf8'}),
+                    lambda:[error_message])
             if isinstance(self.client, ExtendedTestResult):
                 value = details
             else:
                 if error_message is not None:
-                    value = subunit.RemoteError('Text attachment: traceback\n'
+                    value = subunit.RemoteError(u'Text attachment: traceback\n'
                         '------------\n' + error_message + '------------\n')
                 else:
                     value = subunit.RemoteError()
@@ -845,15 +850,15 @@ class TestRemotedTestCase(unittest.TestCase):
 class TestRemoteError(unittest.TestCase):
 
     def test_eq(self):
-        error = subunit.RemoteError("Something went wrong")
-        another_error = subunit.RemoteError("Something went wrong")
-        different_error = subunit.RemoteError("boo!")
+        error = subunit.RemoteError(u"Something went wrong")
+        another_error = subunit.RemoteError(u"Something went wrong")
+        different_error = subunit.RemoteError(u"boo!")
         self.assertEqual(error, another_error)
         self.assertNotEqual(error, different_error)
         self.assertNotEqual(different_error, another_error)
 
     def test_empty_constructor(self):
-        self.assertEqual(subunit.RemoteError(), subunit.RemoteError(""))
+        self.assertEqual(subunit.RemoteError(), subunit.RemoteError(u""))
 
 
 class TestExecTestCase(unittest.TestCase):
@@ -887,8 +892,8 @@ class TestExecTestCase(unittest.TestCase):
         mcdonald = subunit.RemotedTestCase("old mcdonald")
         bing = subunit.RemotedTestCase("bing crosby")
         bing_details = {}
-        bing_details['traceback'] = Content(ContentType("text", "x-traceback"),
-            lambda:["foo.c:53:ERROR invalid state\n"])
+        bing_details['traceback'] = Content(ContentType("text", "x-traceback",
+            {'charset': 'utf8'}), lambda:["foo.c:53:ERROR invalid state\n"])
         an_error = subunit.RemotedTestCase("an error")
         error_details = {}
         self.assertEqual([
@@ -1004,7 +1009,7 @@ class TestTestProtocolClient(unittest.TestCase):
             ContentType('text', 'plain'), lambda:['serialised\nform'])}
         self.sample_tb_details = dict(self.sample_details)
         self.sample_tb_details['traceback'] = TracebackContent(
-            subunit.RemoteError("boo qux"), self.test)
+            subunit.RemoteError(u"boo qux"), self.test)
 
     def test_start_test(self):
         """Test startTest on a TestProtocolClient."""
@@ -1034,7 +1039,7 @@ class TestTestProtocolClient(unittest.TestCase):
     def test_add_failure(self):
         """Test addFailure on a TestProtocolClient."""
         self.protocol.addFailure(
-            self.test, subunit.RemoteError("boo qux"))
+            self.test, subunit.RemoteError(u"boo qux"))
         self.assertEqual(
             self.io.getvalue(),
             ('failure: %s [\n' + _remote_exception_str + ': boo qux\n]\n')
@@ -1058,7 +1063,7 @@ class TestTestProtocolClient(unittest.TestCase):
     def test_add_error(self):
         """Test stopTest on a TestProtocolClient."""
         self.protocol.addError(
-            self.test, subunit.RemoteError("phwoar crikey"))
+            self.test, subunit.RemoteError(u"phwoar crikey"))
         self.assertEqual(
             self.io.getvalue(),
             ('error: %s [\n' +
@@ -1083,7 +1088,7 @@ class TestTestProtocolClient(unittest.TestCase):
     def test_add_expected_failure(self):
         """Test addExpectedFailure on a TestProtocolClient."""
         self.protocol.addExpectedFailure(
-            self.test, subunit.RemoteError("phwoar crikey"))
+            self.test, subunit.RemoteError(u"phwoar crikey"))
         self.assertEqual(
             self.io.getvalue(),
             ('xfail: %s [\n' +
diff --git a/lib/subunit/setup.py b/lib/subunit/setup.py
new file mode 100755 (executable)
index 0000000..b9d6c5a
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+try:
+    # If the user has setuptools / distribute installed, use it
+    from setuptools import setup
+except ImportError:
+    # Otherwise, fall back to distutils.
+    from distutils.core import setup
+    extra = {}
+else:
+    extra = {
+        'install_requires': [
+            'testtools',
+        ]
+    }
+
+try:
+    # Assume we are in a distribution, which has PKG-INFO
+    version_lines = [x for x in open('PKG-INFO').readlines()
+                        if x.startswith('Version:')]
+    version_line = version_lines and version_lines[-1] or 'VERSION = 0.0'
+    VERSION = version_line.split(':')[1].strip()
+except IOError:
+    # Must be a development checkout, so use the Makefile
+    version_lines = [x for x in open('Makefile').readlines()
+                        if x.startswith('VERSION')]
+    version_line = version_lines and version_lines[-1] or 'VERSION = 0.0'
+    VERSION = version_line.split('=')[1].strip()
+
+
+setup(
+    name='python-subunit',
+    version=VERSION,
+    description=('Python implementation of subunit test streaming protocol'),
+    long_description=open('README').read(),
+    classifiers=[
+        'Intended Audience :: Developers',
+        'Programming Language :: Python',
+        'Topic :: Software Development :: Testing',
+    ],
+    keywords='python test streaming',
+    author='Robert Collins',
+    author_email='subunit-dev@lists.launchpad.net',
+    url='http://launchpad.net/subunit',
+    packages=['subunit'],
+    package_dir={'subunit': 'python/subunit'},
+    scripts = [
+        'filters/subunit2gtk',
+        'filters/subunit2junitxml',
+        'filters/subunit2pyunit',
+        'filters/subunit-filter',
+        'filters/subunit-ls',
+        'filters/subunit-notify',
+        'filters/subunit-stats',
+        'filters/subunit-tags',
+        'filters/tap2subunit',
+    ],
+    **extra
+)