Commit | Line | Data |
---|---|---|
b85894a3 MJ |
1 | # Copyright (c) 2016, Matt Layman |
2 | ||
3 | import os | |
4 | from unittest import TextTestResult, TextTestRunner | |
5 | from unittest.runner import _WritelnDecorator | |
6 | import sys | |
7 | ||
8 | from tap import formatter | |
9 | from tap.i18n import _ | |
10 | from tap.tracker import Tracker | |
11 | ||
12 | ||
13 | class TAPTestResult(TextTestResult): | |
14 | ||
15 | FORMAT = None | |
16 | ||
17 | def __init__(self, stream, descriptions, verbosity): | |
18 | super(TAPTestResult, self).__init__(stream, descriptions, verbosity) | |
19 | ||
20 | def stopTestRun(self): | |
21 | """Once the test run is complete, generate each of the TAP files.""" | |
22 | super(TAPTestResult, self).stopTestRun() | |
23 | self.tracker.generate_tap_reports() | |
24 | ||
25 | def addError(self, test, err): | |
26 | super(TAPTestResult, self).addError(test, err) | |
27 | diagnostics = formatter.format_exception(err) | |
28 | self.tracker.add_not_ok( | |
29 | self._cls_name(test), self._description(test), | |
30 | diagnostics=diagnostics) | |
31 | ||
32 | def addFailure(self, test, err): | |
33 | super(TAPTestResult, self).addFailure(test, err) | |
34 | diagnostics = formatter.format_exception(err) | |
35 | self.tracker.add_not_ok( | |
36 | self._cls_name(test), self._description(test), | |
37 | diagnostics=diagnostics) | |
38 | ||
39 | def addSuccess(self, test): | |
40 | super(TAPTestResult, self).addSuccess(test) | |
41 | self.tracker.add_ok(self._cls_name(test), self._description(test)) | |
42 | ||
43 | def addSkip(self, test, reason): | |
44 | super(TAPTestResult, self).addSkip(test, reason) | |
45 | self.tracker.add_skip( | |
46 | self._cls_name(test), self._description(test), reason) | |
47 | ||
48 | def addExpectedFailure(self, test, err): | |
49 | super(TAPTestResult, self).addExpectedFailure(test, err) | |
50 | diagnostics = formatter.format_exception(err) | |
51 | self.tracker.add_not_ok( | |
52 | self._cls_name(test), self._description(test), | |
53 | _('(expected failure)'), diagnostics=diagnostics) | |
54 | ||
55 | def addUnexpectedSuccess(self, test): | |
56 | super(TAPTestResult, self).addUnexpectedSuccess(test) | |
57 | self.tracker.add_ok(self._cls_name(test), self._description(test), | |
58 | _('(unexpected success)')) | |
59 | ||
60 | def _cls_name(self, test): | |
61 | return test.__class__.__name__ | |
62 | ||
63 | def _description(self, test): | |
64 | if self.FORMAT: | |
65 | try: | |
66 | return self.FORMAT.format( | |
67 | method_name=str(test), | |
68 | short_description=test.shortDescription() or '') | |
69 | except KeyError: | |
70 | sys.exit(_( | |
71 | 'Bad format string: {format}\n' | |
72 | 'Replacement options are: {{short_description}} and ' | |
73 | '{{method_name}}').format(format=self.FORMAT)) | |
74 | ||
75 | return test.shortDescription() or str(test) | |
76 | ||
77 | ||
78 | # TODO: 2016-7-30 mblayman - Since the 2.6 signature is no longer relevant, | |
79 | # check the possibility of removing the module level scope. | |
80 | ||
81 | # Module level state stinks, but this is the only way to keep compatibility | |
82 | # with Python 2.6. The best place for the tracker is as an instance variable | |
83 | # on the runner, but __init__ is so different that it is not easy to create | |
84 | # a runner that satisfies every supported Python version. | |
85 | _tracker = Tracker() | |
86 | ||
87 | ||
88 | class TAPTestRunner(TextTestRunner): | |
89 | """A test runner that will behave exactly like TextTestRunner and will | |
90 | additionally generate TAP files for each test case""" | |
91 | ||
92 | resultclass = TAPTestResult | |
93 | ||
94 | def set_stream(self, streaming): | |
95 | """Set the streaming boolean option to stream TAP directly to stdout. | |
96 | ||
97 | The test runner default output will be suppressed in favor of TAP. | |
98 | """ | |
99 | self.stream = _WritelnDecorator(open(os.devnull, 'w')) | |
100 | _tracker.streaming = streaming | |
101 | _tracker.stream = sys.stdout | |
102 | ||
103 | def _makeResult(self): | |
104 | result = self.resultclass( | |
105 | self.stream, self.descriptions, self.verbosity) | |
106 | result.tracker = _tracker | |
107 | return result | |
108 | ||
109 | @classmethod | |
110 | def set_outdir(cls, outdir): | |
111 | """Set the output directory so that TAP files are written to the | |
112 | specified outdir location. | |
113 | """ | |
114 | # Blame the lack of unittest extensibility for this hacky method. | |
115 | _tracker.outdir = outdir | |
116 | ||
117 | @classmethod | |
118 | def set_combined(cls, combined): | |
119 | """Set the tracker to use a single output file.""" | |
120 | _tracker.combined = combined | |
121 | ||
122 | @classmethod | |
123 | def set_header(cls, header): | |
124 | """Set the header display flag.""" | |
125 | _tracker.header = header | |
126 | ||
127 | @classmethod | |
128 | def set_format(cls, fmt): | |
129 | """Set the format of each test line. | |
130 | ||
131 | The format string can use: | |
132 | * {method_name}: The test method name | |
133 | * {short_description}: The test's docstring short description | |
134 | """ | |
135 | TAPTestResult.FORMAT = fmt |