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."""
26 # NOT a TestResult, because we are implementing the interface, not inheriting
28 class TestResultDecorator(object):
29 """General pass-through decorator.
31 This provides a base that other TestResults can inherit from to
32 gain basic forwarding functionality. It also takes care of
33 handling the case where the target doesn't support newer methods
34 or features by degrading them.
37 def __init__(self, decorated):
38 """Create a TestResultDecorator forwarding to decorated."""
39 self.decorated = decorated
41 def _call_maybe(self, method_name, fallback, *params):
42 """Call method_name on self.decorated, if present.
44 This is used to guard newer methods which older pythons do not
45 support. While newer clients won't call these methods if they don't
46 exist, they do exist on the decorator, and thus the decorator has to be
47 the one to filter them out.
49 :param method_name: The name of the method to call.
50 :param fallback: If not None, the fallback to call to handle downgrading
51 this method. Otherwise when method_name is not available, no
52 exception is raised and None is returned.
53 :param *params: Parameters to pass to method_name.
54 :return: The result of self.decorated.method_name(*params), if it
55 exists, and None otherwise.
57 method = getattr(self.decorated, method_name, None)
59 if fallback is not None:
60 return fallback(*params)
62 return method(*params)
64 def startTest(self, test):
65 return self.decorated.startTest(test)
67 def startTestRun(self):
68 return self._call_maybe("startTestRun", None)
70 def stopTest(self, test):
71 return self.decorated.stopTest(test)
73 def stopTestRun(self):
74 return self._call_maybe("stopTestRun", None)
76 def addError(self, test, err):
77 return self.decorated.addError(test, err)
79 def addFailure(self, test, err):
80 return self.decorated.addFailure(test, err)
82 def addSuccess(self, test):
83 return self.decorated.addSuccess(test)
85 def addSkip(self, test, reason):
86 return self._call_maybe("addSkip", self._degrade_skip, test, reason)
88 def _degrade_skip(self, test, reason):
89 return self.decorated.addSuccess(test)
91 def addExpectedFailure(self, test, err):
92 return self._call_maybe("addExpectedFailure",
93 self.decorated.addFailure, test, err)
95 def addUnexpectedSuccess(self, test):
96 return self._call_maybe("addUnexpectedSuccess",
97 self.decorated.addSuccess, test)
99 def progress(self, offset, whence):
100 return self._call_maybe("progress", None, offset, whence)
102 def wasSuccessful(self):
103 return self.decorated.wasSuccessful()
106 def shouldStop(self):
107 return self.decorated.shouldStop
110 return self.decorated.stop()
112 def time(self, a_datetime):
113 return self._call_maybe("time", None, a_datetime)
116 class HookedTestResultDecorator(TestResultDecorator):
117 """A TestResult which calls a hook on every event."""
119 def __init__(self, decorated):
120 self.super = super(HookedTestResultDecorator, self)
121 self.super.__init__(decorated)
123 def startTest(self, test):
125 return self.super.startTest(test)
127 def startTestRun(self):
129 return self.super.startTestRun()
131 def stopTest(self, test):
133 return self.super.stopTest(test)
135 def stopTestRun(self):
137 return self.super.stopTestRun()
139 def addError(self, test, err):
141 return self.super.addError(test, err)
143 def addFailure(self, test, err):
145 return self.super.addFailure(test, err)
147 def addSuccess(self, test):
149 return self.super.addSuccess(test)
151 def addSkip(self, test, reason):
153 return self.super.addSkip(test, reason)
155 def addExpectedFailure(self, test, err):
157 return self.super.addExpectedFailure(test, err)
159 def addUnexpectedSuccess(self, test):
161 return self.super.addUnexpectedSuccess(test)
163 def progress(self, offset, whence):
165 return self.super.progress(offset, whence)
167 def wasSuccessful(self):
169 return self.super.wasSuccessful()
172 def shouldStop(self):
174 return self.super.shouldStop
178 return self.super.stop()
180 def time(self, a_datetime):
182 return self.super.time(a_datetime)
185 class AutoTimingTestResultDecorator(HookedTestResultDecorator):
186 """Decorate a TestResult to add time events to a test run.
188 By default this will cause a time event before every test event,
189 but if explicit time data is being provided by the test run, then
190 this decorator will turn itself off to prevent causing confusion.
193 def __init__(self, decorated):
195 super(AutoTimingTestResultDecorator, self).__init__(decorated)
197 def _before_event(self):
201 time = datetime.datetime.utcnow().replace(tzinfo=iso8601.Utc())
202 self._call_maybe("time", None, time)
204 def progress(self, offset, whence):
205 return self._call_maybe("progress", None, offset, whence)
208 def shouldStop(self):
209 return self.decorated.shouldStop
211 def time(self, a_datetime):
212 """Provide a timestamp for the current test activity.
214 :param a_datetime: If None, automatically add timestamps before every
215 event (this is the default behaviour if time() is not called at
216 all). If not None, pass the provided time onto the decorated
217 result object and disable automatic timestamps.
219 self._time = a_datetime
220 return self._call_maybe("time", None, a_datetime)
223 class ExtendedToOriginalDecorator(object):
224 """Permit new TestResult API code to degrade gracefully with old results.
226 This decorates an existing TestResult and converts missing outcomes
227 such as addSkip to older outcomes such as addSuccess. It also supports
228 the extended details protocol. In all cases the most recent protocol
229 is attempted first, and fallbacks only occur when the decorated result
230 does not support the newer style of calling.
233 def __init__(self, decorated):
234 self.decorated = decorated
236 def addError(self, test, err=None, details=None):
237 self._check_args(err, details)
238 if details is not None:
240 return self.decorated.addError(test, details=details)
243 err = self._details_to_exc_info(details)
244 return self.decorated.addError(test, err)
246 def addExpectedFailure(self, test, err=None, details=None):
247 self._check_args(err, details)
248 addExpectedFailure = getattr(self.decorated, 'addExpectedFailure', None)
249 if addExpectedFailure is None:
250 return self.addSuccess(test)
251 if details is not None:
253 return addExpectedFailure(test, details=details)
256 err = self._details_to_exc_info(details)
257 return addExpectedFailure(test, err)
259 def addFailure(self, test, err=None, details=None):
260 self._check_args(err, details)
261 if details is not None:
263 return self.decorated.addFailure(test, details=details)
266 err = self._details_to_exc_info(details)
267 return self.decorated.addFailure(test, err)
269 def addSkip(self, test, reason=None, details=None):
270 self._check_args(reason, details)
271 addSkip = getattr(self.decorated, 'addSkip', None)
273 return self.decorated.addSuccess(test)
274 if details is not None:
276 return addSkip(test, details=details)
279 reason = self._details_to_str(details)
280 return addSkip(test, reason)
282 def addUnexpectedSuccess(self, test, details=None):
283 outcome = getattr(self.decorated, 'addUnexpectedSuccess', None)
285 return self.decorated.addSuccess(test)
286 if details is not None:
288 return outcome(test, details=details)
293 def addSuccess(self, test, details=None):
294 if details is not None:
296 return self.decorated.addSuccess(test, details=details)
299 return self.decorated.addSuccess(test)
301 def _check_args(self, err, details):
305 if details is not None:
308 raise ValueError("Must pass only one of err '%s' and details '%s"
311 def _details_to_exc_info(self, details):
312 """Convert a details dict to an exc_info tuple."""
313 return subunit.RemoteError(self._details_to_str(details))
315 def _details_to_str(self, details):
316 """Convert a details dict to a string."""
318 # sorted is for testing, may want to remove that and use a dict
319 # subclass with defined order for iteritems instead.
320 for key, content in sorted(details.iteritems()):
321 if content.content_type.type != 'text':
322 lines.append('Binary content: %s\n' % key)
324 lines.append('Text attachment: %s\n' % key)
325 lines.append('------------\n')
326 lines.extend(content.iter_bytes())
327 if not lines[-1].endswith('\n'):
329 lines.append('------------\n')
330 return ''.join(lines)
332 def startTest(self, test):
333 return self.decorated.startTest(test)
335 def startTestRun(self):
337 return self.decorated.startTestRun()
338 except AttributeError:
341 def stopTest(self, test):
342 return self.decorated.stopTest(test)
344 def stopTestRun(self):
346 return self.decorated.stopTestRun()
347 except AttributeError: