Move transport decoration fallback to use the ExtendedToOriginal fallback.
[third_party/subunit] / python / subunit / test_results.py
1 #
2 #  subunit: extensions to Python unittest to get test results from subprocesses.
3 #  Copyright (C) 2009  Robert Collins <robertc@robertcollins.net>
4 #
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.
9 #  
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.
15 #
16
17 """TestResult helper classes used to by subunit."""
18
19 import datetime
20
21 import iso8601
22
23 import subunit
24
25
26 # NOT a TestResult, because we are implementing the interface, not inheriting
27 # it.
28 class TestResultDecorator(object):
29     """General pass-through decorator.
30
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.
35     """
36
37     def __init__(self, decorated):
38         """Create a TestResultDecorator forwarding to decorated."""
39         # Make every decorator degrade gracefully.
40         self.decorated = ExtendedToOriginalDecorator(decorated)
41
42     def startTest(self, test):
43         return self.decorated.startTest(test)
44
45     def startTestRun(self):
46         return self.decorated.startTestRun()
47
48     def stopTest(self, test):
49         return self.decorated.stopTest(test)
50
51     def stopTestRun(self):
52         return self.decorated.stopTestRun()
53
54     def addError(self, test, err):
55         return self.decorated.addError(test, err)
56
57     def addFailure(self, test, err):
58         return self.decorated.addFailure(test, err)
59
60     def addSuccess(self, test):
61         return self.decorated.addSuccess(test)
62
63     def addSkip(self, test, reason):
64         return self.decorated.addSkip(test, reason)
65
66     def addExpectedFailure(self, test, err):
67         return self.decorated.addExpectedFailure(test, err)
68
69     def addUnexpectedSuccess(self, test):
70         return self.decorated.addUnexpectedSuccess(test)
71
72     def progress(self, offset, whence):
73         return self.decorated.progress(offset, whence)
74
75     def wasSuccessful(self):
76         return self.decorated.wasSuccessful()
77
78     @property
79     def shouldStop(self):
80         return self.decorated.shouldStop
81
82     def stop(self):
83         return self.decorated.stop()
84
85     def tags(self, gone_tags, new_tags):
86         return self.decorated.time(gone_tags, new_tags)
87
88     def time(self, a_datetime):
89         return self.decorated.time(a_datetime)
90
91
92 class HookedTestResultDecorator(TestResultDecorator):
93     """A TestResult which calls a hook on every event."""
94
95     def __init__(self, decorated):
96         self.super = super(HookedTestResultDecorator, self)
97         self.super.__init__(decorated)
98
99     def startTest(self, test):
100         self._before_event()
101         return self.super.startTest(test)
102
103     def startTestRun(self):
104         self._before_event()
105         return self.super.startTestRun()
106
107     def stopTest(self, test):
108         self._before_event()
109         return self.super.stopTest(test)
110
111     def stopTestRun(self):
112         self._before_event()
113         return self.super.stopTestRun()
114
115     def addError(self, test, err):
116         self._before_event()
117         return self.super.addError(test, err)
118
119     def addFailure(self, test, err):
120         self._before_event()
121         return self.super.addFailure(test, err)
122
123     def addSuccess(self, test):
124         self._before_event()
125         return self.super.addSuccess(test)
126
127     def addSkip(self, test, reason):
128         self._before_event()
129         return self.super.addSkip(test, reason)
130
131     def addExpectedFailure(self, test, err):
132         self._before_event()
133         return self.super.addExpectedFailure(test, err)
134
135     def addUnexpectedSuccess(self, test):
136         self._before_event()
137         return self.super.addUnexpectedSuccess(test)
138
139     def progress(self, offset, whence):
140         self._before_event()
141         return self.super.progress(offset, whence)
142
143     def wasSuccessful(self):
144         self._before_event()
145         return self.super.wasSuccessful()
146
147     @property
148     def shouldStop(self):
149         self._before_event()
150         return self.super.shouldStop
151
152     def stop(self):
153         self._before_event()
154         return self.super.stop()
155
156     def time(self, a_datetime):
157         self._before_event()
158         return self.super.time(a_datetime)
159
160
161 class AutoTimingTestResultDecorator(HookedTestResultDecorator):
162     """Decorate a TestResult to add time events to a test run.
163
164     By default this will cause a time event before every test event,
165     but if explicit time data is being provided by the test run, then
166     this decorator will turn itself off to prevent causing confusion.
167     """
168
169     def __init__(self, decorated):
170         self._time = None
171         super(AutoTimingTestResultDecorator, self).__init__(decorated)
172
173     def _before_event(self):
174         time = self._time
175         if time is not None:
176             return
177         time = datetime.datetime.utcnow().replace(tzinfo=iso8601.Utc())
178         self.decorated.time(time)
179
180     def progress(self, offset, whence):
181         return self.decorated.progress(offset, whence)
182
183     @property
184     def shouldStop(self):
185         return self.decorated.shouldStop
186
187     def time(self, a_datetime):
188         """Provide a timestamp for the current test activity.
189
190         :param a_datetime: If None, automatically add timestamps before every
191             event (this is the default behaviour if time() is not called at
192             all).  If not None, pass the provided time onto the decorated
193             result object and disable automatic timestamps.
194         """
195         self._time = a_datetime
196         return self.decorated.time(a_datetime)
197
198
199 class ExtendedToOriginalDecorator(object):
200     """Permit new TestResult API code to degrade gracefully with old results.
201
202     This decorates an existing TestResult and converts missing outcomes
203     such as addSkip to older outcomes such as addSuccess. It also supports
204     the extended details protocol. In all cases the most recent protocol
205     is attempted first, and fallbacks only occur when the decorated result
206     does not support the newer style of calling.
207     """
208
209     def __init__(self, decorated):
210         self.decorated = decorated
211
212     def addError(self, test, err=None, details=None):
213         self._check_args(err, details)
214         if details is not None:
215             try:
216                 return self.decorated.addError(test, details=details)
217             except TypeError, e:
218                 # have to convert
219                 err = self._details_to_exc_info(details)
220         return self.decorated.addError(test, err)
221
222     def addExpectedFailure(self, test, err=None, details=None):
223         self._check_args(err, details)
224         addExpectedFailure = getattr(self.decorated, 'addExpectedFailure', None)
225         if addExpectedFailure is None:
226             return self.addSuccess(test)
227         if details is not None:
228             try:
229                 return addExpectedFailure(test, details=details)
230             except TypeError, e:
231                 # have to convert
232                 err = self._details_to_exc_info(details)
233         return addExpectedFailure(test, err)
234
235     def addFailure(self, test, err=None, details=None):
236         self._check_args(err, details)
237         if details is not None:
238             try:
239                 return self.decorated.addFailure(test, details=details)
240             except TypeError, e:
241                 # have to convert
242                 err = self._details_to_exc_info(details)
243         return self.decorated.addFailure(test, err)
244
245     def addSkip(self, test, reason=None, details=None):
246         self._check_args(reason, details)
247         addSkip = getattr(self.decorated, 'addSkip', None)
248         if addSkip is None:
249             return self.decorated.addSuccess(test)
250         if details is not None:
251             try:
252                 return addSkip(test, details=details)
253             except TypeError, e:
254                 # have to convert
255                 reason = self._details_to_str(details)
256         return addSkip(test, reason)
257
258     def addUnexpectedSuccess(self, test, details=None):
259         outcome = getattr(self.decorated, 'addUnexpectedSuccess', None)
260         if outcome is None:
261             return self.decorated.addSuccess(test)
262         if details is not None:
263             try:
264                 return outcome(test, details=details)
265             except TypeError, e:
266                 pass
267         return outcome(test)
268
269     def addSuccess(self, test, details=None):
270         if details is not None:
271             try:
272                 return self.decorated.addSuccess(test, details=details)
273             except TypeError, e:
274                 pass
275         return self.decorated.addSuccess(test)
276
277     def _check_args(self, err, details):
278         param_count = 0
279         if err is not None:
280             param_count += 1
281         if details is not None:
282             param_count += 1
283         if param_count != 1:
284             raise ValueError("Must pass only one of err '%s' and details '%s"
285                 % (err, details))
286
287     def _details_to_exc_info(self, details):
288         """Convert a details dict to an exc_info tuple."""
289         return subunit.RemoteError(self._details_to_str(details))
290
291     def _details_to_str(self, details):
292         """Convert a details dict to a string."""
293         lines = []
294         # sorted is for testing, may want to remove that and use a dict
295         # subclass with defined order for iteritems instead.
296         for key, content in sorted(details.iteritems()):
297             if content.content_type.type != 'text':
298                 lines.append('Binary content: %s\n' % key)
299                 continue
300             lines.append('Text attachment: %s\n' % key)
301             lines.append('------------\n')
302             lines.extend(content.iter_bytes())
303             if not lines[-1].endswith('\n'):
304                 lines.append('\n')
305             lines.append('------------\n')
306         return ''.join(lines)
307
308     def progress(self, offset, whence):
309         method = getattr(self.decorated, 'progress', None)
310         if method is None:
311             return
312         return method(offset, whence)
313
314     @property
315     def shouldStop(self):
316         return self.decorated.shouldStop
317
318     def startTest(self, test):
319         return self.decorated.startTest(test)
320
321     def startTestRun(self):
322         try:
323             return self.decorated.startTestRun()
324         except AttributeError:
325             return
326
327     def stop(self):
328         return self.decorated.stop()
329
330     def stopTest(self, test):
331         return self.decorated.stopTest(test)
332
333     def stopTestRun(self):
334         try:
335             return self.decorated.stopTestRun()
336         except AttributeError:
337             return
338
339     def tags(self, new_tags, gone_tags):
340         method = getattr(self.decorated, 'tags', None)
341         if method is None:
342             return
343         return method(new_tags, gone_tags)
344
345     def time(self, a_datetime):
346         method = getattr(self.decorated, 'time', None)
347         if method is None:
348             return
349         return method(a_datetime)
350
351     def wasSuccessful(self):
352         return self.decorated.wasSuccessful()
353