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