~testing-cabal/ubuntu/oneiric/subunit/daily-build-packaging

« back to all changes in this revision

Viewing changes to python/subunit/__init__.py

  • Committer: Jelmer Vernooij
  • Date: 2011-05-02 20:55:32 UTC
  • mfrom: (135.1.6 upstream)
  • Revision ID: jelmer@samba.org-20110502205532-butnpa8myx2s2mwi
New upstream snapshot.

Show diffs side-by-side

added added

removed removed

Lines of Context:
118
118
 
119
119
import os
120
120
import re
121
 
try:
122
 
    from StringIO import StringIO
123
 
except ImportError:
124
 
    from io import StringIO
125
121
import subprocess
126
122
import sys
127
123
import unittest
128
124
 
129
 
from subunit import iso8601
130
 
from testtools.compat import _b, _u
131
 
from testtools import ExtendedToOriginalDecorator
 
125
from testtools import content, content_type, ExtendedToOriginalDecorator
 
126
from testtools.compat import _b, _u, BytesIO, StringIO
132
127
try:
133
128
    from testtools.testresult.real import _StringException
134
129
    RemoteException = _StringException
135
 
    _remote_exception_str = '_StringException' # For testing.
 
130
    # For testing: different pythons have different str() implementations.
 
131
    if sys.version_info > (3, 0):
 
132
        _remote_exception_str = "testtools.testresult.real._StringException"
 
133
        _remote_exception_str_chunked = "34\r\n" + _remote_exception_str
 
134
    else:
 
135
        _remote_exception_str = "_StringException" 
 
136
        _remote_exception_str_chunked = "1A\r\n" + _remote_exception_str
136
137
except ImportError:
137
138
    raise ImportError ("testtools.testresult.real does not contain "
138
139
        "_StringException, check your version.")
139
 
from testtools import content, content_type, testresult
 
140
from testtools import testresult
140
141
 
141
 
from subunit import chunked, details
 
142
from subunit import chunked, details, iso8601, test_results
142
143
 
143
144
 
144
145
PROGRESS_SET = 0
190
191
 
191
192
    def __init__(self, parser):
192
193
        self.parser = parser
 
194
        self._test_sym = (_b('test'), _b('testing'))
 
195
        self._colon_sym = _b(':')
 
196
        self._error_sym = (_b('error'),)
 
197
        self._failure_sym = (_b('failure'),)
 
198
        self._progress_sym = (_b('progress'),)
 
199
        self._skip_sym = _b('skip')
 
200
        self._success_sym = (_b('success'), _b('successful'))
 
201
        self._tags_sym = (_b('tags'),)
 
202
        self._time_sym = (_b('time'),)
 
203
        self._xfail_sym = (_b('xfail'),)
 
204
        self._uxsuccess_sym = (_b('uxsuccess'),)
 
205
        self._start_simple = _u(" [")
 
206
        self._start_multipart = _u(" [ multipart")
193
207
 
194
208
    def addError(self, offset, line):
195
209
        """An 'error:' directive has been read."""
217
231
        if len(parts) == 2 and line.startswith(parts[0]):
218
232
            cmd, rest = parts
219
233
            offset = len(cmd) + 1
220
 
            cmd = cmd.rstrip(':')
221
 
            if cmd in ('test', 'testing'):
 
234
            cmd = cmd.rstrip(self._colon_sym)
 
235
            if cmd in self._test_sym:
222
236
                self.startTest(offset, line)
223
 
            elif cmd == 'error':
 
237
            elif cmd in self._error_sym:
224
238
                self.addError(offset, line)
225
 
            elif cmd == 'failure':
 
239
            elif cmd in self._failure_sym:
226
240
                self.addFailure(offset, line)
227
 
            elif cmd == 'progress':
 
241
            elif cmd in self._progress_sym:
228
242
                self.parser._handleProgress(offset, line)
229
 
            elif cmd == 'skip':
 
243
            elif cmd in self._skip_sym:
230
244
                self.addSkip(offset, line)
231
 
            elif cmd in ('success', 'successful'):
 
245
            elif cmd in self._success_sym:
232
246
                self.addSuccess(offset, line)
233
 
            elif cmd in ('tags',):
 
247
            elif cmd in self._tags_sym:
234
248
                self.parser._handleTags(offset, line)
235
249
                self.parser.subunitLineReceived(line)
236
 
            elif cmd in ('time',):
 
250
            elif cmd in self._time_sym:
237
251
                self.parser._handleTime(offset, line)
238
252
                self.parser.subunitLineReceived(line)
239
 
            elif cmd == 'xfail':
 
253
            elif cmd in self._xfail_sym:
240
254
                self.addExpectedFail(offset, line)
 
255
            elif cmd in self._uxsuccess_sym:
 
256
                self.addUnexpectedSuccess(offset, line)
241
257
            else:
242
258
                self.parser.stdOutLineReceived(line)
243
259
        else:
262
278
        :param details_state: The state to switch to for details
263
279
            processing of this outcome.
264
280
        """
265
 
        if self.parser.current_test_description == line[offset:-1]:
 
281
        test_name = line[offset:-1].decode('utf8')
 
282
        if self.parser.current_test_description == test_name:
266
283
            self.parser._state = self.parser._outside_test
267
284
            self.parser.current_test_description = None
268
285
            no_details()
269
286
            self.parser.client.stopTest(self.parser._current_test)
270
287
            self.parser._current_test = None
271
288
            self.parser.subunitLineReceived(line)
272
 
        elif self.parser.current_test_description + " [" == line[offset:-1]:
 
289
        elif self.parser.current_test_description + self._start_simple == \
 
290
            test_name:
273
291
            self.parser._state = details_state
274
292
            details_state.set_simple()
275
293
            self.parser.subunitLineReceived(line)
276
 
        elif self.parser.current_test_description + " [ multipart" == \
277
 
            line[offset:-1]:
 
294
        elif self.parser.current_test_description + self._start_multipart == \
 
295
            test_name:
278
296
            self.parser._state = details_state
279
297
            details_state.set_multipart()
280
298
            self.parser.subunitLineReceived(line)
299
317
        self._outcome(offset, line, self._xfail,
300
318
            self.parser._reading_xfail_details)
301
319
 
 
320
    def _uxsuccess(self):
 
321
        self.parser.client.addUnexpectedSuccess(self.parser._current_test)
 
322
 
 
323
    def addUnexpectedSuccess(self, offset, line):
 
324
        """A 'uxsuccess:' directive has been read."""
 
325
        self._outcome(offset, line, self._uxsuccess,
 
326
            self.parser._reading_uxsuccess_details)
 
327
 
302
328
    def _failure(self):
303
329
        self.parser.client.addFailure(self.parser._current_test, details={})
304
330
 
337
363
    def startTest(self, offset, line):
338
364
        """A test start command received."""
339
365
        self.parser._state = self.parser._in_test
340
 
        self.parser._current_test = RemotedTestCase(line[offset:-1])
341
 
        self.parser.current_test_description = line[offset:-1]
 
366
        test_name = line[offset:-1].decode('utf8')
 
367
        self.parser._current_test = RemotedTestCase(test_name)
 
368
        self.parser.current_test_description = test_name
342
369
        self.parser.client.startTest(self.parser._current_test)
343
370
        self.parser.subunitLineReceived(line)
344
371
 
360
387
 
361
388
    def lostConnection(self):
362
389
        """Connection lost."""
363
 
        self.parser._lostConnectionInTest(
364
 
            _u('%s report of ' % self._outcome_label()))
 
390
        self.parser._lostConnectionInTest(_u('%s report of ') %
 
391
            self._outcome_label())
365
392
 
366
393
    def _outcome_label(self):
367
394
        """The label to describe this outcome."""
409
436
        return "xfail"
410
437
 
411
438
 
 
439
class _ReadingUnexpectedSuccessDetails(_ReadingDetails):
 
440
    """State for the subunit parser when reading uxsuccess details."""
 
441
 
 
442
    def _report_outcome(self):
 
443
        self.parser.client.addUnexpectedSuccess(self.parser._current_test,
 
444
            details=self.details_parser.get_details())
 
445
 
 
446
    def _outcome_label(self):
 
447
        return "uxsuccess"
 
448
 
 
449
 
412
450
class _ReadingSkipDetails(_ReadingDetails):
413
451
    """State for the subunit parser when reading skip details."""
414
452
 
444
482
        :param stream: The stream that lines received which are not part of the
445
483
            subunit protocol should be written to. This allows custom handling
446
484
            of mixed protocols. By default, sys.stdout will be used for
447
 
            convenience.
 
485
            convenience. It should accept bytes to its write() method.
448
486
        :param forward_stream: A stream to forward subunit lines to. This
449
487
            allows a filter to forward the entire stream while still parsing
450
488
            and acting on it. By default forward_stream is set to
453
491
        self.client = ExtendedToOriginalDecorator(client)
454
492
        if stream is None:
455
493
            stream = sys.stdout
 
494
            if sys.version_info > (3, 0):
 
495
                stream = stream.buffer
456
496
        self._stream = stream
457
497
        self._forward_stream = forward_stream or DiscardStream()
458
498
        # state objects we can switch too
463
503
        self._reading_skip_details = _ReadingSkipDetails(self)
464
504
        self._reading_success_details = _ReadingSuccessDetails(self)
465
505
        self._reading_xfail_details = _ReadingExpectedFailureDetails(self)
 
506
        self._reading_uxsuccess_details = _ReadingUnexpectedSuccessDetails(self)
466
507
        # start with outside test.
467
508
        self._state = self._outside_test
 
509
        # Avoid casts on every call
 
510
        self._plusminus = _b('+-')
 
511
        self._push_sym = _b('push')
 
512
        self._pop_sym = _b('pop')
468
513
 
469
514
    def _handleProgress(self, offset, line):
470
515
        """Process a progress directive."""
471
516
        line = line[offset:].strip()
472
 
        if line[0] in '+-':
 
517
        if line[0] in self._plusminus:
473
518
            whence = PROGRESS_CUR
474
519
            delta = int(line)
475
 
        elif line == "push":
 
520
        elif line == self._push_sym:
476
521
            whence = PROGRESS_PUSH
477
522
            delta = None
478
 
        elif line == "pop":
 
523
        elif line == self._pop_sym:
479
524
            whence = PROGRESS_POP
480
525
            delta = None
481
526
        else:
485
530
 
486
531
    def _handleTags(self, offset, line):
487
532
        """Process a tags command."""
488
 
        tags = line[offset:].split()
 
533
        tags = line[offset:].decode('utf8').split()
489
534
        new_tags, gone_tags = tags_to_new_gone(tags)
490
535
        self.client.tags(new_tags, gone_tags)
491
536
 
494
539
        try:
495
540
            event_time = iso8601.parse_date(line[offset:-1])
496
541
        except TypeError:
497
 
            e = sys.exc_info()[1]
498
 
            raise TypeError("Failed to parse %r, got %r" % (line, e))
 
542
            raise TypeError(_u("Failed to parse %r, got %r")
 
543
                % (line, sys.exec_info[1]))
499
544
        self.client.time(event_time)
500
545
 
501
546
    def lineReceived(self, line):
538
583
 
539
584
    # Get a TestSuite or TestCase to run
540
585
    suite = make_suite()
541
 
    # Create a stream (any object with a 'write' method)
 
586
    # Create a stream (any object with a 'write' method). This should accept
 
587
    # bytes not strings: subunit is a byte orientated protocol.
542
588
    stream = file('tests.log', 'wb')
543
589
    # Create a subunit result object which will output to the stream
544
590
    result = subunit.TestProtocolClient(stream)
555
601
        testresult.TestResult.__init__(self)
556
602
        self._stream = stream
557
603
        _make_stream_binary(stream)
 
604
        self._progress_fmt = _b("progress: ")
 
605
        self._bytes_eol = _b("\n")
 
606
        self._progress_plus = _b("+")
 
607
        self._progress_push = _b("push")
 
608
        self._progress_pop = _b("pop")
 
609
        self._empty_bytes = _b("")
 
610
        self._start_simple = _b(" [\n")
 
611
        self._end_simple = _b("]\n")
558
612
 
559
613
    def addError(self, test, error=None, details=None):
560
614
        """Report an error in test test.
601
655
        """
602
656
        self._addOutcome("failure", test, error=error, details=details)
603
657
 
604
 
    def _addOutcome(self, outcome, test, error=None, details=None):
 
658
    def _addOutcome(self, outcome, test, error=None, details=None,
 
659
        error_permitted=True):
605
660
        """Report a failure in test test.
606
661
 
607
662
        Only one of error and details should be provided: conceptually there
615
670
            exc_info tuple.
616
671
        :param details: New Testing-in-python drafted API; a dict from string
617
672
            to subunit.Content objects.
618
 
        """
619
 
        self._stream.write("%s: %s" % (outcome, test.id()))
620
 
        if error is None and details is None:
621
 
            raise ValueError
 
673
        :param error_permitted: If True then one and only one of error or
 
674
            details must be supplied. If False then error must not be supplied
 
675
            and details is still optional.  """
 
676
        self._stream.write(_b("%s: %s" % (outcome, test.id())))
 
677
        if error_permitted:
 
678
            if error is None and details is None:
 
679
                raise ValueError
 
680
        else:
 
681
            if error is not None:
 
682
                raise ValueError
622
683
        if error is not None:
623
 
            self._stream.write(" [\n")
 
684
            self._stream.write(self._start_simple)
624
685
            # XXX: this needs to be made much stricter, along the lines of
625
686
            # Martin[gz]'s work in testtools. Perhaps subunit can use that?
626
687
            for line in self._exc_info_to_unicode(error, test).splitlines():
627
688
                self._stream.write(("%s\n" % line).encode('utf8'))
 
689
        elif details is not None:
 
690
            self._write_details(details)
628
691
        else:
629
 
            self._write_details(details)
630
 
        self._stream.write("]\n")
 
692
            self._stream.write(_b("\n"))
 
693
        if details is not None or error is not None:
 
694
            self._stream.write(self._end_simple)
631
695
 
632
696
    def addSkip(self, test, reason=None, details=None):
633
697
        """Report a skipped test."""
634
698
        if reason is None:
635
699
            self._addOutcome("skip", test, error=None, details=details)
636
700
        else:
637
 
            self._stream.write("skip: %s [\n" % test.id())
638
 
            self._stream.write("%s\n" % reason)
639
 
            self._stream.write("]\n")
 
701
            self._stream.write(_b("skip: %s [\n" % test.id()))
 
702
            self._stream.write(_b("%s\n" % reason))
 
703
            self._stream.write(self._end_simple)
640
704
 
641
705
    def addSuccess(self, test, details=None):
642
706
        """Report a success in a test."""
643
 
        self._stream.write("successful: %s" % test.id())
644
 
        if not details:
645
 
            self._stream.write("\n")
646
 
        else:
647
 
            self._write_details(details)
648
 
            self._stream.write("]\n")
649
 
    addUnexpectedSuccess = addSuccess
 
707
        self._addOutcome("successful", test, details=details, error_permitted=False)
 
708
 
 
709
    def addUnexpectedSuccess(self, test, details=None):
 
710
        """Report an unexpected success in test test.
 
711
 
 
712
        Details can optionally be provided: conceptually there
 
713
        are two separate methods:
 
714
            addError(self, test)
 
715
            addError(self, test, details)
 
716
 
 
717
        :param details: New Testing-in-python drafted API; a dict from string
 
718
            to subunit.Content objects.
 
719
        """
 
720
        self._addOutcome("uxsuccess", test, details=details,
 
721
            error_permitted=False)
650
722
 
651
723
    def startTest(self, test):
652
724
        """Mark a test as starting its test run."""
653
725
        super(TestProtocolClient, self).startTest(test)
654
 
        self._stream.write("test: %s\n" % test.id())
 
726
        self._stream.write(_b("test: %s\n" % test.id()))
655
727
        self._stream.flush()
656
728
 
657
729
    def stopTest(self, test):
669
741
            PROGRESS_POP.
670
742
        """
671
743
        if whence == PROGRESS_CUR and offset > -1:
672
 
            prefix = "+"
 
744
            prefix = self._progress_plus
 
745
            offset = _b(str(offset))
673
746
        elif whence == PROGRESS_PUSH:
674
 
            prefix = ""
675
 
            offset = "push"
 
747
            prefix = self._empty_bytes
 
748
            offset = self._progress_push
676
749
        elif whence == PROGRESS_POP:
677
 
            prefix = ""
678
 
            offset = "pop"
 
750
            prefix = self._empty_bytes
 
751
            offset = self._progress_pop
679
752
        else:
680
 
            prefix = ""
681
 
        self._stream.write("progress: %s%s\n" % (prefix, offset))
 
753
            prefix = self._empty_bytes
 
754
            offset = _b(str(offset))
 
755
        self._stream.write(self._progress_fmt + prefix + offset +
 
756
            self._bytes_eol)
682
757
 
683
758
    def time(self, a_datetime):
684
759
        """Inform the client of the time.
686
761
        ":param datetime: A datetime.datetime object.
687
762
        """
688
763
        time = a_datetime.astimezone(iso8601.Utc())
689
 
        self._stream.write("time: %04d-%02d-%02d %02d:%02d:%02d.%06dZ\n" % (
 
764
        self._stream.write(_b("time: %04d-%02d-%02d %02d:%02d:%02d.%06dZ\n" % (
690
765
            time.year, time.month, time.day, time.hour, time.minute,
691
 
            time.second, time.microsecond))
 
766
            time.second, time.microsecond)))
692
767
 
693
768
    def _write_details(self, details):
694
769
        """Output details to the stream.
695
770
 
696
771
        :param details: An extended details dict for a test outcome.
697
772
        """
698
 
        self._stream.write(" [ multipart\n")
699
 
        for name, content in sorted(details.iteritems()):
700
 
            self._stream.write("Content-Type: %s/%s" %
701
 
                (content.content_type.type, content.content_type.subtype))
 
773
        self._stream.write(_b(" [ multipart\n"))
 
774
        for name, content in sorted(details.items()):
 
775
            self._stream.write(_b("Content-Type: %s/%s" %
 
776
                (content.content_type.type, content.content_type.subtype)))
702
777
            parameters = content.content_type.parameters
703
778
            if parameters:
704
 
                self._stream.write(";")
 
779
                self._stream.write(_b(";"))
705
780
                param_strs = []
706
 
                for param, value in parameters.iteritems():
 
781
                for param, value in parameters.items():
707
782
                    param_strs.append("%s=%s" % (param, value))
708
 
                self._stream.write(",".join(param_strs))
709
 
            self._stream.write("\n%s\n" % name)
 
783
                self._stream.write(_b(",".join(param_strs)))
 
784
            self._stream.write(_b("\n%s\n" % name))
710
785
            encoder = chunked.Encoder(self._stream)
711
 
            map(encoder.write, content.iter_bytes())
 
786
            list(map(encoder.write, content.iter_bytes()))
712
787
            encoder.close()
713
788
 
714
789
    def done(self):
799
874
 
800
875
    def _run(self, result):
801
876
        protocol = TestProtocolServer(result)
802
 
        output = subprocess.Popen(self.script, shell=True,
803
 
            stdout=subprocess.PIPE).communicate()[0]
804
 
        protocol.readFrom(StringIO(output.decode('utf-8')))
 
877
        process = subprocess.Popen(self.script, shell=True,
 
878
            stdout=subprocess.PIPE)
 
879
        _make_stream_binary(process.stdout)
 
880
        output = process.communicate()[0]
 
881
        protocol.readFrom(BytesIO(output))
805
882
 
806
883
 
807
884
class IsolatedTestCase(unittest.TestCase):
850
927
        # at this point, sys.stdin is redirected, now we want
851
928
        # to filter it to escape ]'s.
852
929
        ### XXX: test and write that bit.
853
 
 
854
 
        result = TestProtocolClient(sys.stdout)
 
930
        stream = os.fdopen(1, 'wb')
 
931
        result = TestProtocolClient(stream)
855
932
        klass.run(self, result)
856
 
        sys.stdout.flush()
 
933
        stream.flush()
857
934
        sys.stderr.flush()
858
935
        # exit HARD, exit NOW.
859
936
        os._exit(0)
863
940
        os.close(c2pwrite)
864
941
        # hookup a protocol engine
865
942
        protocol = TestProtocolServer(result)
866
 
        protocol.readFrom(os.fdopen(c2pread, 'rU'))
 
943
        fileobj = os.fdopen(c2pread, 'rb')
 
944
        protocol.readFrom(fileobj)
867
945
        os.waitpid(pid, 0)
868
946
        # TODO return code evaluation.
869
947
    return result
1058
1136
        _make_stream_binary(stream)
1059
1137
        self._passthrough = passthrough
1060
1138
        self._forward = forward
1061
 
        _make_stream_binary(forward)
1062
1139
 
1063
1140
    def __call__(self, result=None):
1064
1141
        return self.run(result)
1134
1211
    if formatter:
1135
1212
        return os.popen(formatter, "w")
1136
1213
    else:
1137
 
        return sys.stdout
 
1214
        stream = sys.stdout
 
1215
        if sys.version_info > (3, 0):
 
1216
            stream = stream.buffer
 
1217
        return stream
 
1218
 
 
1219
 
 
1220
if sys.version_info > (3, 0):
 
1221
    from io import UnsupportedOperation as _NoFilenoError
 
1222
else:
 
1223
    _NoFilenoError = AttributeError
 
1224
 
 
1225
def read_test_list(path):
 
1226
    """Read a list of test ids from a file on disk.
 
1227
 
 
1228
    :param path: Path to the file
 
1229
    :return: Sequence of test ids
 
1230
    """
 
1231
    f = open(path, 'rb')
 
1232
    try:
 
1233
        return [l.rstrip("\n") for l in f.readlines()]
 
1234
    finally:
 
1235
        f.close()
1138
1236
 
1139
1237
 
1140
1238
def _make_stream_binary(stream):
1141
1239
    """Ensure that a stream will be binary safe. See _make_binary_on_windows."""
1142
 
    if getattr(stream, 'fileno', None) is not None:
1143
 
        _make_binary_on_windows(stream.fileno())
 
1240
    try:
 
1241
        fileno = stream.fileno()
 
1242
    except _NoFilenoError:
 
1243
        return
 
1244
    _make_binary_on_windows(fileno)
1144
1245
 
1145
1246
def _make_binary_on_windows(fileno):
1146
1247
    """Win32 mangles \r\n to \n and that breaks streams. See bug lp:505078."""