2 # subunit: extensions to Python unittest to get test results from subprocesses.
3 # Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
5 # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
6 # license at the users choice. A copy of both licenses are available in the
7 # project source as Apache-2.0 and BSD. You may not use this file except in
8 # compliance with one of these two licences.
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # license you chose for the specific language governing permissions and
14 # limitations under that license.
17 """TestResult helper classes used to by subunit."""
27 # NOT a TestResult, because we are implementing the interface, not inheriting
29 class TestResultDecorator(object):
30 """General pass-through decorator.
32 This provides a base that other TestResults can inherit from to
33 gain basic forwarding functionality. It also takes care of
34 handling the case where the target doesn't support newer methods
35 or features by degrading them.
38 def __init__(self
, decorated
):
39 """Create a TestResultDecorator forwarding to decorated."""
40 # Make every decorator degrade gracefully.
41 self
.decorated
= testtools
.ExtendedToOriginalDecorator(decorated
)
43 def startTest(self
, test
):
44 return self
.decorated
.startTest(test
)
46 def startTestRun(self
):
47 return self
.decorated
.startTestRun()
49 def stopTest(self
, test
):
50 return self
.decorated
.stopTest(test
)
52 def stopTestRun(self
):
53 return self
.decorated
.stopTestRun()
55 def addError(self
, test
, err
=None, details
=None):
56 return self
.decorated
.addError(test
, err
, details
=details
)
58 def addFailure(self
, test
, err
=None, details
=None):
59 return self
.decorated
.addFailure(test
, err
, details
=details
)
61 def addSuccess(self
, test
, details
=None):
62 return self
.decorated
.addSuccess(test
, details
=details
)
64 def addSkip(self
, test
, reason
=None, details
=None):
65 return self
.decorated
.addSkip(test
, reason
, details
=details
)
67 def addExpectedFailure(self
, test
, err
=None, details
=None):
68 return self
.decorated
.addExpectedFailure(test
, err
, details
=details
)
70 def addUnexpectedSuccess(self
, test
, details
=None):
71 return self
.decorated
.addUnexpectedSuccess(test
, details
=details
)
73 def progress(self
, offset
, whence
):
74 return self
.decorated
.progress(offset
, whence
)
76 def wasSuccessful(self
):
77 return self
.decorated
.wasSuccessful()
81 return self
.decorated
.shouldStop
84 return self
.decorated
.stop()
86 def tags(self
, new_tags
, gone_tags
):
87 return self
.decorated
.time(new_tags
, gone_tags
)
89 def time(self
, a_datetime
):
90 return self
.decorated
.time(a_datetime
)
93 class HookedTestResultDecorator(TestResultDecorator
):
94 """A TestResult which calls a hook on every event."""
96 def __init__(self
, decorated
):
97 self
.super = super(HookedTestResultDecorator
, self
)
98 self
.super.__init
__(decorated
)
100 def startTest(self
, test
):
102 return self
.super.startTest(test
)
104 def startTestRun(self
):
106 return self
.super.startTestRun()
108 def stopTest(self
, test
):
110 return self
.super.stopTest(test
)
112 def stopTestRun(self
):
114 return self
.super.stopTestRun()
116 def addError(self
, test
, err
=None, details
=None):
118 return self
.super.addError(test
, err
, details
=details
)
120 def addFailure(self
, test
, err
=None, details
=None):
122 return self
.super.addFailure(test
, err
, details
=details
)
124 def addSuccess(self
, test
, details
=None):
126 return self
.super.addSuccess(test
, details
=details
)
128 def addSkip(self
, test
, reason
=None, details
=None):
130 return self
.super.addSkip(test
, reason
, details
=details
)
132 def addExpectedFailure(self
, test
, err
=None, details
=None):
134 return self
.super.addExpectedFailure(test
, err
, details
=details
)
136 def addUnexpectedSuccess(self
, test
, details
=None):
138 return self
.super.addUnexpectedSuccess(test
, details
=details
)
140 def progress(self
, offset
, whence
):
142 return self
.super.progress(offset
, whence
)
144 def wasSuccessful(self
):
146 return self
.super.wasSuccessful()
149 def shouldStop(self
):
151 return self
.super.shouldStop
155 return self
.super.stop()
157 def time(self
, a_datetime
):
159 return self
.super.time(a_datetime
)
162 class AutoTimingTestResultDecorator(HookedTestResultDecorator
):
163 """Decorate a TestResult to add time events to a test run.
165 By default this will cause a time event before every test event,
166 but if explicit time data is being provided by the test run, then
167 this decorator will turn itself off to prevent causing confusion.
170 def __init__(self
, decorated
):
172 super(AutoTimingTestResultDecorator
, self
).__init
__(decorated
)
174 def _before_event(self
):
178 time
= datetime
.datetime
.utcnow().replace(tzinfo
=iso8601
.Utc())
179 self
.decorated
.time(time
)
181 def progress(self
, offset
, whence
):
182 return self
.decorated
.progress(offset
, whence
)
185 def shouldStop(self
):
186 return self
.decorated
.shouldStop
188 def time(self
, a_datetime
):
189 """Provide a timestamp for the current test activity.
191 :param a_datetime: If None, automatically add timestamps before every
192 event (this is the default behaviour if time() is not called at
193 all). If not None, pass the provided time onto the decorated
194 result object and disable automatic timestamps.
196 self
._time
= a_datetime
197 return self
.decorated
.time(a_datetime
)
200 class TestResultFilter(TestResultDecorator
):
201 """A pyunit TestResult interface implementation which filters tests.
203 Tests that pass the filter are handed on to another TestResult instance
204 for further processing/reporting. To obtain the filtered results,
205 the other instance must be interrogated.
207 :ivar result: The result that tests are passed to after filtering.
208 :ivar filter_predicate: The callback run to decide whether to pass
212 def __init__(self
, result
, filter_error
=False, filter_failure
=False,
213 filter_success
=True, filter_skip
=False,
214 filter_predicate
=None):
215 """Create a FilterResult object filtering to result.
217 :param filter_error: Filter out errors.
218 :param filter_failure: Filter out failures.
219 :param filter_success: Filter out successful tests.
220 :param filter_skip: Filter out skipped tests.
221 :param filter_predicate: A callable taking (test, outcome, err,
222 details) and returning True if the result should be passed
223 through. err and details may be none if no error or extra
224 metadata is available. outcome is the name of the outcome such
225 as 'success' or 'failure'.
227 TestResultDecorator
.__init
__(self
, result
)
228 self
._filter
_error
= filter_error
229 self
._filter
_failure
= filter_failure
230 self
._filter
_success
= filter_success
231 self
._filter
_skip
= filter_skip
232 if filter_predicate
is None:
233 filter_predicate
= lambda test
, outcome
, err
, details
: True
234 self
.filter_predicate
= filter_predicate
235 # The current test (for filtering tags)
236 self
._current
_test
= None
237 # Has the current test been filtered (for outputting test tags)
238 self
._current
_test
_filtered
= None
239 # The (new, gone) tags for the current test.
240 self
._current
_test
_tags
= None
242 def addError(self
, test
, err
=None, details
=None):
243 if (not self
._filter
_error
and
244 self
.filter_predicate(test
, 'error', err
, details
)):
245 self
.decorated
.startTest(test
)
246 self
.decorated
.addError(test
, err
, details
=details
)
250 def addFailure(self
, test
, err
=None, details
=None):
251 if (not self
._filter
_failure
and
252 self
.filter_predicate(test
, 'failure', err
, details
)):
253 self
.decorated
.startTest(test
)
254 self
.decorated
.addFailure(test
, err
, details
=details
)
258 def addSkip(self
, test
, reason
=None, details
=None):
259 if (not self
._filter
_skip
and
260 self
.filter_predicate(test
, 'skip', reason
, details
)):
261 self
.decorated
.startTest(test
)
262 self
.decorated
.addSkip(test
, reason
, details
=details
)
266 def addSuccess(self
, test
, details
=None):
267 if (not self
._filter
_success
and
268 self
.filter_predicate(test
, 'success', None, details
)):
269 self
.decorated
.startTest(test
)
270 self
.decorated
.addSuccess(test
, details
=details
)
274 def addExpectedFailure(self
, test
, err
=None, details
=None):
275 if self
.filter_predicate(test
, 'expectedfailure', err
, details
):
276 self
.decorated
.startTest(test
)
277 return self
.decorated
.addExpectedFailure(test
, err
,
282 def addUnexpectedSuccess(self
, test
, details
=None):
283 self
.decorated
.startTest(test
)
284 return self
.decorated
.addUnexpectedSuccess(test
, details
=details
)
287 self
._current
_test
_filtered
= True
289 def startTest(self
, test
):
292 Not directly passed to the client, but used for handling of tags
295 self
._current
_test
= test
296 self
._current
_test
_filtered
= False
297 self
._current
_test
_tags
= set(), set()
299 def stopTest(self
, test
):
302 Not directly passed to the client, but used for handling of tags
305 if not self
._current
_test
_filtered
:
306 # Tags to output for this test.
307 if self
._current
_test
_tags
[0] or self
._current
_test
_tags
[1]:
308 self
.decorated
.tags(*self
._current
_test
_tags
)
309 self
.decorated
.stopTest(test
)
310 self
._current
_test
= None
311 self
._current
_test
_filtered
= None
312 self
._current
_test
_tags
= None
314 def tags(self
, new_tags
, gone_tags
):
315 """Handle tag instructions.
317 Adds and removes tags as appropriate. If a test is currently running,
318 tags are not affected for subsequent tests.
320 :param new_tags: Tags to add,
321 :param gone_tags: Tags to remove.
323 if self
._current
_test
is not None:
324 # gather the tags until the test stops.
325 self
._current
_test
_tags
[0].update(new_tags
)
326 self
._current
_test
_tags
[0].difference_update(gone_tags
)
327 self
._current
_test
_tags
[1].update(gone_tags
)
328 self
._current
_test
_tags
[1].difference_update(new_tags
)
329 return self
.decorated
.tags(new_tags
, gone_tags
)
331 def id_to_orig_id(self
, id):
332 if id.startswith("subunit.RemotedTestCase."):
333 return id[len("subunit.RemotedTestCase."):]