Change the ExtendedToOriginal decorator to fallback xfails as success.
[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         self.decorated = decorated
40
41     def _call_maybe(self, method_name, fallback, *params):
42         """Call method_name on self.decorated, if present.
43         
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.
48
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.
56         """
57         method = getattr(self.decorated, method_name, None)
58         if method is None:
59             if fallback is not None:
60                 return fallback(*params)
61             return
62         return method(*params)
63
64     def startTest(self, test):
65         return self.decorated.startTest(test)
66
67     def startTestRun(self):
68         return self._call_maybe("startTestRun", None)
69
70     def stopTest(self, test):
71         return self.decorated.stopTest(test)
72
73     def stopTestRun(self):
74         return self._call_maybe("stopTestRun", None)
75
76     def addError(self, test, err):
77         return self.decorated.addError(test, err)
78
79     def addFailure(self, test, err):
80         return self.decorated.addFailure(test, err)
81
82     def addSuccess(self, test):
83         return self.decorated.addSuccess(test)
84
85     def addSkip(self, test, reason):
86         return self._call_maybe("addSkip", self._degrade_skip, test, reason)
87
88     def _degrade_skip(self, test, reason):
89         return self.decorated.addSuccess(test)
90
91     def addExpectedFailure(self, test, err):
92         return self._call_maybe("addExpectedFailure",
93             self.decorated.addFailure, test, err)
94
95     def addUnexpectedSuccess(self, test):
96         return self._call_maybe("addUnexpectedSuccess",
97             self.decorated.addSuccess, test)
98
99     def progress(self, offset, whence):
100         return self._call_maybe("progress", None, offset, whence)
101
102     def wasSuccessful(self):
103         return self.decorated.wasSuccessful()
104
105     @property
106     def shouldStop(self):
107         return self.decorated.shouldStop
108
109     def stop(self):
110         return self.decorated.stop()
111
112     def time(self, a_datetime):
113         return self._call_maybe("time", None, a_datetime)
114
115
116 class HookedTestResultDecorator(TestResultDecorator):
117     """A TestResult which calls a hook on every event."""
118
119     def __init__(self, decorated):
120         self.super = super(HookedTestResultDecorator, self)
121         self.super.__init__(decorated)
122
123     def startTest(self, test):
124         self._before_event()
125         return self.super.startTest(test)
126
127     def startTestRun(self):
128         self._before_event()
129         return self.super.startTestRun()
130
131     def stopTest(self, test):
132         self._before_event()
133         return self.super.stopTest(test)
134
135     def stopTestRun(self):
136         self._before_event()
137         return self.super.stopTestRun()
138
139     def addError(self, test, err):
140         self._before_event()
141         return self.super.addError(test, err)
142
143     def addFailure(self, test, err):
144         self._before_event()
145         return self.super.addFailure(test, err)
146
147     def addSuccess(self, test):
148         self._before_event()
149         return self.super.addSuccess(test)
150
151     def addSkip(self, test, reason):
152         self._before_event()
153         return self.super.addSkip(test, reason)
154
155     def addExpectedFailure(self, test, err):
156         self._before_event()
157         return self.super.addExpectedFailure(test, err)
158
159     def addUnexpectedSuccess(self, test):
160         self._before_event()
161         return self.super.addUnexpectedSuccess(test)
162
163     def progress(self, offset, whence):
164         self._before_event()
165         return self.super.progress(offset, whence)
166
167     def wasSuccessful(self):
168         self._before_event()
169         return self.super.wasSuccessful()
170
171     @property
172     def shouldStop(self):
173         self._before_event()
174         return self.super.shouldStop
175
176     def stop(self):
177         self._before_event()
178         return self.super.stop()
179
180     def time(self, a_datetime):
181         self._before_event()
182         return self.super.time(a_datetime)
183
184
185 class AutoTimingTestResultDecorator(HookedTestResultDecorator):
186     """Decorate a TestResult to add time events to a test run.
187
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.
191     """
192
193     def __init__(self, decorated):
194         self._time = None
195         super(AutoTimingTestResultDecorator, self).__init__(decorated)
196
197     def _before_event(self):
198         time = self._time
199         if time is not None:
200             return
201         time = datetime.datetime.utcnow().replace(tzinfo=iso8601.Utc())
202         self._call_maybe("time", None, time)
203
204     def progress(self, offset, whence):
205         return self._call_maybe("progress", None, offset, whence)
206
207     @property
208     def shouldStop(self):
209         return self.decorated.shouldStop
210
211     def time(self, a_datetime):
212         """Provide a timestamp for the current test activity.
213
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.
218         """
219         self._time = a_datetime
220         return self._call_maybe("time", None, a_datetime)
221
222
223 class ExtendedToOriginalDecorator(object):
224     """Permit new TestResult API code to degrade gracefully with old results.
225
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.
231     """
232
233     def __init__(self, decorated):
234         self.decorated = decorated
235
236     def addError(self, test, err=None, details=None):
237         self._check_args(err, details)
238         if details is not None:
239             try:
240                 return self.decorated.addError(test, details=details)
241             except TypeError, e:
242                 # have to convert
243                 err = self._details_to_exc_info(details)
244         return self.decorated.addError(test, err)
245
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:
252             try:
253                 return addExpectedFailure(test, details=details)
254             except TypeError, e:
255                 # have to convert
256                 err = self._details_to_exc_info(details)
257         return addExpectedFailure(test, err)
258
259     def addFailure(self, test, err=None, details=None):
260         self._check_args(err, details)
261         if details is not None:
262             try:
263                 return self.decorated.addFailure(test, details=details)
264             except TypeError, e:
265                 # have to convert
266                 err = self._details_to_exc_info(details)
267         return self.decorated.addFailure(test, err)
268
269     def addSkip(self, test, reason=None, details=None):
270         self._check_args(reason, details)
271         addSkip = getattr(self.decorated, 'addSkip', None)
272         if addSkip is None:
273             return self.decorated.addSuccess(test)
274         if details is not None:
275             try:
276                 return addSkip(test, details=details)
277             except TypeError, e:
278                 # have to convert
279                 reason = self._details_to_str(details)
280         return addSkip(test, reason)
281
282     def addUnexpectedSuccess(self, test, details=None):
283         outcome = getattr(self.decorated, 'addUnexpectedSuccess', None)
284         if outcome is None:
285             return self.decorated.addSuccess(test)
286         if details is not None:
287             try:
288                 return outcome(test, details=details)
289             except TypeError, e:
290                 pass
291         return outcome(test)
292
293     def addSuccess(self, test, details=None):
294         if details is not None:
295             try:
296                 return self.decorated.addSuccess(test, details=details)
297             except TypeError, e:
298                 pass
299         return self.decorated.addSuccess(test)
300
301     def _check_args(self, err, details):
302         param_count = 0
303         if err is not None:
304             param_count += 1
305         if details is not None:
306             param_count += 1
307         if param_count != 1:
308             raise ValueError("Must pass only one of err '%s' and details '%s"
309                 % (err, details))
310
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))
314
315     def _details_to_str(self, details):
316         """Convert a details dict to a string."""
317         lines = []
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)
323                 continue
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'):
328                 lines.append('\n')
329             lines.append('------------\n')
330         return ''.join(lines)
331
332     def startTest(self, test):
333         return self.decorated.startTest(test)
334
335     def startTestRun(self):
336         try:
337             return self.decorated.startTestRun()
338         except AttributeError:
339             return
340
341     def stopTest(self, test):
342         return self.decorated.stopTest(test)
343
344     def stopTestRun(self):
345         try:
346             return self.decorated.stopTestRun()
347         except AttributeError:
348             return