1 # Python module for parsing and generating the Subunit protocol
3 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 __all__
= ['parse_results']
23 import subunit
.iso8601
25 from testtools
import content
, content_type
27 VALID_RESULTS
= ['success', 'successful', 'failure', 'fail', 'skip', 'knownfail', 'error', 'xfail', 'skip-testsuite', 'testsuite-failure', 'testsuite-xfail', 'testsuite-success', 'testsuite-error', 'uxsuccess', 'testsuite-uxsuccess']
29 class TestsuiteEnabledTestResult(testtools
.testresult
.TestResult
):
31 def start_testsuite(self
, name
):
32 raise NotImplementedError(self
.start_testsuite
)
35 def parse_results(msg_ops
, statistics
, fh
):
43 parts
= l
.split(None, 1)
44 if not len(parts
) == 2 or not l
.startswith(parts
[0]):
47 command
= parts
[0].rstrip(":")
49 if command
in ("test", "testing"):
50 msg_ops
.control_msg(l
)
52 test
= subunit
.RemotedTestCase(name
)
53 if name
in open_tests
:
54 msg_ops
.addError(open_tests
.pop(name
), subunit
.RemoteError(u
"Test already running"))
55 msg_ops
.startTest(test
)
56 open_tests
[name
] = test
57 elif command
== "time":
58 msg_ops
.control_msg(l
)
60 dt
= subunit
.iso8601
.parse_date(arg
.rstrip("\n"))
62 print "Unable to parse time line: %s" % arg
.rstrip("\n")
65 elif command
in VALID_RESULTS
:
66 msg_ops
.control_msg(l
)
68 grp
= re
.match("(.*?)( \[)?([ \t]*)( multipart)?\n", arg
)
69 (testname
, hasreason
) = (grp
.group(1), grp
.group(2))
72 # reason may be specified in next lines
78 msg_ops
.control_msg(l
)
85 remote_error
= subunit
.RemoteError(reason
.decode("utf-8"))
88 statistics
['TESTS_ERROR']+=1
89 msg_ops
.addError(subunit
.RemotedTestCase(testname
), subunit
.RemoteError(u
"reason (%s) interrupted" % result
))
93 remote_error
= subunit
.RemoteError(u
"No reason specified")
94 if result
in ("success", "successful"):
96 test
= open_tests
.pop(testname
)
98 statistics
['TESTS_ERROR']+=1
100 msg_ops
.addError(subunit
.RemotedTestCase(testname
), subunit
.RemoteError(u
"Test was never started"))
102 statistics
['TESTS_EXPECTED_OK']+=1
103 msg_ops
.addSuccess(test
)
104 elif result
in ("xfail", "knownfail"):
106 test
= open_tests
.pop(testname
)
108 statistics
['TESTS_ERROR']+=1
110 msg_ops
.addError(subunit
.RemotedTestCase(testname
), subunit
.RemoteError(u
"Test was never started"))
112 statistics
['TESTS_EXPECTED_FAIL']+=1
113 msg_ops
.addExpectedFailure(test
, remote_error
)
114 elif result
in ("uxsuccess", ):
116 test
= open_tests
.pop(testname
)
118 statistics
['TESTS_ERROR']+=1
120 msg_ops
.addError(subunit
.RemotedTestCase(testname
), subunit
.RemoteError(u
"Test was never started"))
122 statistics
['TESTS_UNEXPECTED_OK']+=1
123 msg_ops
.addUnexpectedSuccess(test
, remote_error
)
125 elif result
in ("failure", "fail"):
127 test
= open_tests
.pop(testname
)
129 statistics
['TESTS_ERROR']+=1
131 msg_ops
.addError(subunit
.RemotedTestCase(testname
), subunit
.RemoteError(u
"Test was never started"))
133 statistics
['TESTS_UNEXPECTED_FAIL']+=1
135 msg_ops
.addFailure(test
, remote_error
)
136 elif result
== "skip":
137 statistics
['TESTS_SKIP']+=1
138 # Allow tests to be skipped without prior announcement of test
140 test
= open_tests
.pop(testname
)
142 test
= subunit
.RemotedTestCase(testname
)
143 msg_ops
.addSkip(test
, reason
)
144 elif result
== "error":
145 statistics
['TESTS_ERROR']+=1
148 test
= open_tests
.pop(testname
)
150 test
= subunit
.RemotedTestCase(testname
)
151 msg_ops
.addError(test
, remote_error
)
152 elif result
== "skip-testsuite":
153 msg_ops
.skip_testsuite(testname
)
154 elif result
== "testsuite-success":
155 msg_ops
.end_testsuite(testname
, "success", reason
)
156 elif result
== "testsuite-failure":
157 msg_ops
.end_testsuite(testname
, "failure", reason
)
159 elif result
== "testsuite-xfail":
160 msg_ops
.end_testsuite(testname
, "xfail", reason
)
161 elif result
== "testsuite-uxsuccess":
162 msg_ops
.end_testsuite(testname
, "uxsuccess", reason
)
164 elif result
== "testsuite-error":
165 msg_ops
.end_testsuite(testname
, "error", reason
)
168 raise AssertionError("Recognized but unhandled result %r" %
170 elif command
== "testsuite":
171 msg_ops
.start_testsuite(arg
.strip())
172 elif command
== "progress":
175 msg_ops
.progress(None, subunit
.PROGRESS_POP
)
177 msg_ops
.progress(None, subunit
.PROGRESS_PUSH
)
179 msg_ops
.progress(int(arg
), subunit
.PROGRESS_CUR
)
181 msg_ops
.progress(int(arg
), subunit
.PROGRESS_SET
)
183 msg_ops
.output_msg(l
)
186 test
= subunit
.RemotedTestCase(open_tests
.popitem()[1])
187 msg_ops
.addError(test
, subunit
.RemoteError(u
"was started but never finished!"))
188 statistics
['TESTS_ERROR']+=1
194 class SubunitOps(subunit
.TestProtocolClient
,TestsuiteEnabledTestResult
):
196 # The following are Samba extensions:
197 def start_testsuite(self
, name
):
198 self
._stream
.write("testsuite: %s\n" % name
)
200 def skip_testsuite(self
, name
, reason
=None):
202 self
._stream
.write("skip-testsuite: %s [\n%s\n]\n" % (name
, reason
))
204 self
._stream
.write("skip-testsuite: %s\n" % name
)
206 def end_testsuite(self
, name
, result
, reason
=None):
208 self
._stream
.write("testsuite-%s: %s [\n%s\n]\n" % (result
, name
, reason
))
210 self
._stream
.write("testsuite-%s: %s\n" % (result
, name
))
212 def output_msg(self
, msg
):
213 self
._stream
.write(msg
)
216 def read_test_regexes(name
):
222 if l
== "" or l
[0] == "#":
225 (regex
, reason
) = l
.split("#", 1)
226 ret
[regex
.strip()] = reason
.strip()
234 def find_in_list(regexes
, fullname
):
235 for regex
, reason
in regexes
.iteritems():
236 if re
.match(regex
, fullname
):
243 class ImmediateFail(Exception):
244 """Raised to abort immediately."""
247 super(ImmediateFail
, self
).__init
__("test failed and fail_immediately set")
250 class FilterOps(testtools
.testresult
.TestResult
):
252 def control_msg(self
, msg
):
253 pass # We regenerate control messages, so ignore this
255 def time(self
, time
):
258 def progress(self
, delta
, whence
):
259 self
._ops
.progress(delta
, whence
)
261 def output_msg(self
, msg
):
262 if self
.output
is None:
263 sys
.stdout
.write(msg
)
267 def startTest(self
, test
):
268 self
.seen_output
= True
269 test
= self
._add
_prefix
(test
)
270 if self
.strip_ok_output
:
273 self
._ops
.startTest(test
)
275 def _add_prefix(self
, test
):
278 if self
.prefix
is not None:
280 if self
.suffix
is not None:
283 return subunit
.RemotedTestCase(prefix
+ test
.id() + suffix
)
285 def addError(self
, test
, details
=None):
286 test
= self
._add
_prefix
(test
)
289 self
._ops
.addError(test
, details
)
291 if self
.fail_immediately
:
292 raise ImmediateFail()
294 def addSkip(self
, test
, details
=None):
295 self
.seen_output
= True
296 test
= self
._add
_prefix
(test
)
297 self
._ops
.addSkip(test
, details
)
300 def addExpectedFailure(self
, test
, details
=None):
301 test
= self
._add
_prefix
(test
)
302 self
._ops
.addExpectedFailure(test
, details
)
305 def addUnexpectedSuccess(self
, test
, details
=None):
306 test
= self
._add
_prefix
(test
)
307 self
.uxsuccess_added
+=1
308 self
.total_uxsuccess
+=1
309 self
._ops
.addUnexpectedSuccess(test
, details
)
311 self
._ops
.output_msg(self
.output
)
313 if self
.fail_immediately
:
314 raise ImmediateFail()
316 def addFailure(self
, test
, details
=None):
317 test
= self
._add
_prefix
(test
)
318 xfail_reason
= find_in_list(self
.expected_failures
, test
.id())
319 if xfail_reason
is None:
320 xfail_reason
= find_in_list(self
.flapping
, test
.id())
321 if xfail_reason
is not None:
324 if details
is not None:
325 details
= subunit
.RemoteError(unicode(details
[1]) + xfail_reason
.decode("utf-8"))
327 details
= subunit
.RemoteError(xfail_reason
.decode("utf-8"))
328 self
._ops
.addExpectedFailure(test
, details
)
332 self
._ops
.addFailure(test
, details
)
334 self
._ops
.output_msg(self
.output
)
335 if self
.fail_immediately
:
336 raise ImmediateFail()
339 def addSuccess(self
, test
, details
=None):
340 test
= self
._add
_prefix
(test
)
341 xfail_reason
= find_in_list(self
.expected_failures
, test
.id())
342 if xfail_reason
is not None:
343 self
.uxsuccess_added
+= 1
344 self
.total_uxsuccess
+= 1
347 details
['reason'] = content
.Content(
348 content_type
.ContentType("text", "plain",
349 {"charset": "utf8"}), lambda: xfail_reason
)
350 self
._ops
.addUnexpectedSuccess(test
, details
)
352 self
._ops
.output_msg(self
.output
)
353 if self
.fail_immediately
:
354 raise ImmediateFail()
356 self
._ops
.addSuccess(test
, details
)
359 def skip_testsuite(self
, name
, reason
=None):
360 self
._ops
.skip_testsuite(name
, reason
)
362 def start_testsuite(self
, name
):
363 self
._ops
.start_testsuite(name
)
367 self
.uxsuccess_added
= 0
369 def end_testsuite(self
, name
, result
, reason
=None):
372 if self
.xfail_added
> 0:
374 if self
.fail_added
> 0 or self
.error_added
> 0 or self
.uxsuccess_added
> 0:
377 if xfail
and result
in ("fail", "failure"):
380 if self
.uxsuccess_added
> 0 and result
!= "uxsuccess":
383 reason
= "Subunit/Filter Reason"
384 reason
+= "\n uxsuccess[%d]" % self
.uxsuccess_added
386 if self
.fail_added
> 0 and result
!= "failure":
389 reason
= "Subunit/Filter Reason"
390 reason
+= "\n failures[%d]" % self
.fail_added
392 if self
.error_added
> 0 and result
!= "error":
395 reason
= "Subunit/Filter Reason"
396 reason
+= "\n errors[%d]" % self
.error_added
398 self
._ops
.end_testsuite(name
, result
, reason
)
399 if result
not in ("success", "xfail"):
401 self
._ops
.output_msg(self
.output
)
402 if self
.fail_immediately
:
403 raise ImmediateFail()
406 def __init__(self
, out
, prefix
=None, suffix
=None, expected_failures
=None,
407 strip_ok_output
=False, fail_immediately
=False,
410 self
.seen_output
= False
414 if expected_failures
is not None:
415 self
.expected_failures
= expected_failures
417 self
.expected_failures
= {}
418 if flapping
is not None:
419 self
.flapping
= flapping
422 self
.strip_ok_output
= strip_ok_output
425 self
.uxsuccess_added
= 0
429 self
.total_uxsuccess
= 0
431 self
.fail_immediately
= fail_immediately
434 class PlainFormatter(TestsuiteEnabledTestResult
):
436 def __init__(self
, verbose
, immediate
, statistics
,
438 super(PlainFormatter
, self
).__init
__()
439 self
.verbose
= verbose
440 self
.immediate
= immediate
441 self
.statistics
= statistics
442 self
.start_time
= None
443 self
.test_output
= {}
444 self
.suitesfailed
= []
449 self
._progress
_level
= 0
450 self
.totalsuites
= totaltests
451 self
.last_time
= None
454 def _format_time(delta
):
455 minutes
, seconds
= divmod(delta
.seconds
, 60)
456 hours
, minutes
= divmod(minutes
, 60)
461 ret
+= "%dm" % minutes
462 ret
+= "%ds" % seconds
465 def progress(self
, offset
, whence
):
466 if whence
== subunit
.PROGRESS_POP
:
467 self
._progress
_level
-= 1
468 elif whence
== subunit
.PROGRESS_PUSH
:
469 self
._progress
_level
+= 1
470 elif whence
== subunit
.PROGRESS_SET
:
471 if self
._progress
_level
== 0:
472 self
.totalsuites
= offset
473 elif whence
== subunit
.PROGRESS_CUR
:
474 raise NotImplementedError
477 if self
.start_time
is None:
481 def start_testsuite(self
, name
):
486 self
.test_output
[name
] = ""
488 total_tests
= (self
.statistics
['TESTS_EXPECTED_OK'] +
489 self
.statistics
['TESTS_EXPECTED_FAIL'] +
490 self
.statistics
['TESTS_ERROR'] +
491 self
.statistics
['TESTS_UNEXPECTED_FAIL'] +
492 self
.statistics
['TESTS_UNEXPECTED_OK'])
494 out
= "[%d(%d)" % (self
.index
, total_tests
)
495 if self
.totalsuites
is not None:
496 out
+= "/%d" % self
.totalsuites
497 if self
.start_time
is not None:
498 out
+= " at " + self
._format
_time
(self
.last_time
- self
.start_time
)
499 if self
.suitesfailed
:
500 out
+= ", %d errors" % (len(self
.suitesfailed
),)
503 sys
.stdout
.write(out
+ "\n")
505 sys
.stdout
.write(out
+ ": ")
507 def output_msg(self
, output
):
509 sys
.stdout
.write(output
)
510 elif self
.name
is not None:
511 self
.test_output
[self
.name
] += output
513 sys
.stdout
.write(output
)
515 def control_msg(self
, output
):
518 def end_testsuite(self
, name
, result
, reason
):
522 if not name
in self
.test_output
:
523 print "no output for name[%s]" % name
525 if result
in ("success", "xfail"):
528 self
.output_msg("ERROR: Testsuite[%s]\n" % name
)
529 if reason
is not None:
530 self
.output_msg("REASON: %s\n" % (reason
,))
531 self
.suitesfailed
.append(name
)
532 if self
.immediate
and not self
.verbose
and name
in self
.test_output
:
533 out
+= self
.test_output
[name
]
536 if not self
.immediate
:
540 out
+= " " + result
.upper() + "\n"
542 sys
.stdout
.write(out
)
544 def startTest(self
, test
):
547 def addSuccess(self
, test
):
548 self
.end_test(test
.id(), "success", False)
550 def addError(self
, test
, details
=None):
551 self
.end_test(test
.id(), "error", True, details
)
553 def addFailure(self
, test
, details
=None):
554 self
.end_test(test
.id(), "failure", True, details
)
556 def addSkip(self
, test
, details
=None):
557 self
.end_test(test
.id(), "skip", False, details
)
559 def addExpectedFailure(self
, test
, details
=None):
560 self
.end_test(test
.id(), "xfail", False, details
)
562 def addUnexpectedSuccess(self
, test
, details
=None):
563 self
.end_test(test
.id(), "uxsuccess", True, details
)
565 def end_test(self
, testname
, result
, unexpected
, details
=None):
567 self
.test_output
[self
.name
] = ""
568 if not self
.immediate
:
573 'success': '.'}.get(result
, "?(%s)" % result
))
576 if not self
.name
in self
.test_output
:
577 self
.test_output
[self
.name
] = ""
579 self
.test_output
[self
.name
] += "UNEXPECTED(%s): %s\n" % (result
, testname
)
580 if details
is not None:
581 self
.test_output
[self
.name
] += "REASON: %s\n" % (unicode(details
[1]).encode("utf-8").strip(),)
583 if self
.immediate
and not self
.verbose
:
584 sys
.stdout
.write(self
.test_output
[self
.name
])
585 self
.test_output
[self
.name
] = ""
587 if not self
.immediate
:
592 'success': 'S'}.get(result
, "?"))
594 def write_summary(self
, path
):
597 if self
.suitesfailed
:
598 f
.write("= Failed tests =\n")
600 for suite
in self
.suitesfailed
:
601 f
.write("== %s ==\n" % suite
)
602 if suite
in self
.test_output
:
603 f
.write(self
.test_output
[suite
]+"\n\n")
607 if not self
.immediate
and not self
.verbose
:
608 for suite
in self
.suitesfailed
:
610 print "FAIL: %s" % suite
611 if suite
in self
.test_output
:
612 print self
.test_output
[suite
]
615 f
.write("= Skipped tests =\n")
616 for reason
in self
.skips
.keys():
617 f
.write(reason
+ "\n")
618 for name
in self
.skips
[reason
]:
619 f
.write("\t%s\n" % name
)
623 if (not self
.suitesfailed
and
624 not self
.statistics
['TESTS_UNEXPECTED_FAIL'] and
625 not self
.statistics
['TESTS_UNEXPECTED_OK'] and
626 not self
.statistics
['TESTS_ERROR']):
627 ok
= (self
.statistics
['TESTS_EXPECTED_OK'] +
628 self
.statistics
['TESTS_EXPECTED_FAIL'])
629 print "\nALL OK (%d tests in %d testsuites)" % (ok
, self
.suites_ok
)
631 print "\nFAILED (%d failures, %d errors and %d unexpected successes in %d testsuites)" % (
632 self
.statistics
['TESTS_UNEXPECTED_FAIL'],
633 self
.statistics
['TESTS_ERROR'],
634 self
.statistics
['TESTS_UNEXPECTED_OK'],
635 len(self
.suitesfailed
))
637 def skip_testsuite(self
, name
, reason
="UNKNOWN"):
638 self
.skips
.setdefault(reason
, []).append(name
)