diff --git a/tests/run-tests.py b/tests/run-tests.py --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -57,6 +57,7 @@ import re import threading import killdaemons as killmod import Queue as queue +from xml.dom import minidom import unittest processlock = threading.Lock() @@ -190,6 +191,8 @@ def getparser(): " (implies --keep-tmpdir)") parser.add_option("-v", "--verbose", action="store_true", help="output verbose messages") + parser.add_option("--xunit", type="string", + help="record xunit results at specified path") parser.add_option("--view", type="string", help="external diff viewer") parser.add_option("--with-hg", type="string", @@ -304,6 +307,20 @@ def vlog(*msg): return log(*msg) +# Bytes that break XML even in a CDATA block: control characters 0-31 +# sans \t, \n and \r +CDATA_EVIL = re.compile(r"[\000-\010\013\014\016-\037]") + +def cdatasafe(data): + """Make a string safe to include in a CDATA block. + + Certain control characters are illegal in a CDATA block, and + there's no way to include a ]]> in a CDATA either. This function + replaces illegal bytes with ? and adds a space between the ]] so + that it won't break the CDATA block. + """ + return CDATA_EVIL.sub('?', data).replace(']]>', '] ]>') + def log(*msg): """Log something to stdout. @@ -1085,6 +1102,9 @@ class TestResult(unittest._TextTestResul self.times = [] self._started = {} self._stopped = {} + # Data stored for the benefit of generating xunit reports. + self.successes = [] + self.faildata = {} def addFailure(self, test, reason): self.failures.append((test, reason)) @@ -1099,9 +1119,12 @@ class TestResult(unittest._TextTestResul self.stream.write('!') iolock.release() - def addError(self, *args, **kwargs): - super(TestResult, self).addError(*args, **kwargs) + def addSuccess(self, test): + super(TestResult, self).addSuccess(test) + self.successes.append(test) + def addError(self, test, err): + super(TestResult, self).addError(test, err) if self._options.first: self.stop() @@ -1141,6 +1164,8 @@ class TestResult(unittest._TextTestResul """Record a mismatch in test output for a particular test.""" accepted = False + failed = False + lines = [] iolock.acquire() if self._options.nodiff: @@ -1169,7 +1194,8 @@ class TestResult(unittest._TextTestResul else: rename(test.errpath, '%s.out' % test.path) accepted = True - + if not accepted and not failed: + self.faildata[test.name] = ''.join(lines) iolock.release() return accepted @@ -1344,6 +1370,35 @@ class TextTestRunner(unittest.TextTestRu for test, msg in result.errors: self.stream.writeln('Errored %s: %s' % (test.name, msg)) + if self._runner.options.xunit: + xuf = open(self._runner.options.xunit, 'wb') + try: + timesd = dict( + (test, real) for test, cuser, csys, real in result.times) + doc = minidom.Document() + s = doc.createElement('testsuite') + s.setAttribute('name', 'run-tests') + s.setAttribute('tests', str(result.testsRun)) + s.setAttribute('errors', "0") # TODO + s.setAttribute('failures', str(failed)) + s.setAttribute('skipped', str(skipped + ignored)) + doc.appendChild(s) + for tc in result.successes: + t = doc.createElement('testcase') + t.setAttribute('name', tc.name) + t.setAttribute('time', '%.3f' % timesd[tc.name]) + s.appendChild(t) + for tc, err in sorted(result.faildata.iteritems()): + t = doc.createElement('testcase') + t.setAttribute('name', tc) + t.setAttribute('time', '%.3f' % timesd[tc]) + cd = doc.createCDATASection(cdatasafe(err)) + t.appendChild(cd) + s.appendChild(t) + xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8')) + finally: + xuf.close() + self._runner._checkhglib('Tested') self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.' diff --git a/tests/test-run-tests.t b/tests/test-run-tests.t --- a/tests/test-run-tests.t +++ b/tests/test-run-tests.t @@ -48,6 +48,39 @@ failing test # Ran 2 tests, 0 skipped, 0 warned, 1 failed. python hash seed: * (glob) [1] +test --xunit support + $ $TESTDIR/run-tests.py --with-hg=`which hg` --xunit=xunit.xml + + --- $TESTTMP/test-failure.t + +++ $TESTTMP/test-failure.t.err + @@ -1,4 +1,4 @@ + $ echo babar + - rataxes + + babar + This is a noop statement so that + this test is still more bytes than success. + + ERROR: test-failure.t output changed + !. + Failed test-failure.t: output changed + # Ran 2 tests, 0 skipped, 0 warned, 1 failed. + python hash seed: * (glob) + [1] + $ cat xunit.xml + + + (glob) + (glob) + + test for --retest ==================== @@ -291,6 +324,18 @@ Skips Skipped test-skip.t: irrelevant # Ran 1 tests, 2 skipped, 0 warned, 0 failed. +Skips with xml + $ $TESTDIR/run-tests.py --with-hg=`which hg` --keyword xyzzy \ + > --xunit=xunit.xml + i.s + Skipped test-skip.t: irrelevant + # Ran 1 tests, 2 skipped, 0 warned, 0 failed. + $ cat xunit.xml + + + (glob) + + Missing skips or blacklisted skips don't count as executed: $ echo test-failure.t > blacklist $ $TESTDIR/run-tests.py --with-hg=`which hg` --blacklist=blacklist \