PageRenderTime 44ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 1ms

/_pytest/junitxml.py

https://bitbucket.org/evelyn559/pypy
Python | 221 lines | 189 code | 19 blank | 13 comment | 27 complexity | b23bbf1e36d31f275d38bc9bf9c89646 MD5 | raw file
  1. """ report test results in JUnit-XML format, for use with Hudson and build integration servers.
  2. Based on initial code from Ross Lawley.
  3. """
  4. import py
  5. import os
  6. import re
  7. import sys
  8. import time
  9. # Python 2.X and 3.X compatibility
  10. try:
  11. unichr(65)
  12. except NameError:
  13. unichr = chr
  14. try:
  15. unicode('A')
  16. except NameError:
  17. unicode = str
  18. try:
  19. long(1)
  20. except NameError:
  21. long = int
  22. # We need to get the subset of the invalid unicode ranges according to
  23. # XML 1.0 which are valid in this python build. Hence we calculate
  24. # this dynamically instead of hardcoding it. The spec range of valid
  25. # chars is: Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
  26. # | [#x10000-#x10FFFF]
  27. _illegal_unichrs = [(0x00, 0x08), (0x0B, 0x0C), (0x0E, 0x19),
  28. (0xD800, 0xDFFF), (0xFDD0, 0xFFFF)]
  29. _illegal_ranges = [unicode("%s-%s") % (unichr(low), unichr(high))
  30. for (low, high) in _illegal_unichrs
  31. if low < sys.maxunicode]
  32. illegal_xml_re = re.compile(unicode('[%s]') %
  33. unicode('').join(_illegal_ranges))
  34. del _illegal_unichrs
  35. del _illegal_ranges
  36. def pytest_addoption(parser):
  37. group = parser.getgroup("terminal reporting")
  38. group.addoption('--junitxml', action="store", dest="xmlpath",
  39. metavar="path", default=None,
  40. help="create junit-xml style report file at given path.")
  41. group.addoption('--junitprefix', action="store", dest="junitprefix",
  42. metavar="str", default=None,
  43. help="prepend prefix to classnames in junit-xml output")
  44. def pytest_configure(config):
  45. xmlpath = config.option.xmlpath
  46. if xmlpath:
  47. config._xml = LogXML(xmlpath, config.option.junitprefix)
  48. config.pluginmanager.register(config._xml)
  49. def pytest_unconfigure(config):
  50. xml = getattr(config, '_xml', None)
  51. if xml:
  52. del config._xml
  53. config.pluginmanager.unregister(xml)
  54. class LogXML(object):
  55. def __init__(self, logfile, prefix):
  56. logfile = os.path.expanduser(os.path.expandvars(logfile))
  57. self.logfile = os.path.normpath(logfile)
  58. self.prefix = prefix
  59. self.test_logs = []
  60. self.passed = self.skipped = 0
  61. self.failed = self.errors = 0
  62. self._durations = {}
  63. def _opentestcase(self, report):
  64. names = report.nodeid.split("::")
  65. names[0] = names[0].replace("/", '.')
  66. names = tuple(names)
  67. d = {'time': self._durations.pop(report.nodeid, "0")}
  68. names = [x.replace(".py", "") for x in names if x != "()"]
  69. classnames = names[:-1]
  70. if self.prefix:
  71. classnames.insert(0, self.prefix)
  72. d['classname'] = ".".join(classnames)
  73. d['name'] = py.xml.escape(names[-1])
  74. attrs = ['%s="%s"' % item for item in sorted(d.items())]
  75. self.test_logs.append("\n<testcase %s>" % " ".join(attrs))
  76. def _closetestcase(self):
  77. self.test_logs.append("</testcase>")
  78. def appendlog(self, fmt, *args):
  79. def repl(matchobj):
  80. i = ord(matchobj.group())
  81. if i <= 0xFF:
  82. return unicode('#x%02X') % i
  83. else:
  84. return unicode('#x%04X') % i
  85. args = tuple([illegal_xml_re.sub(repl, py.xml.escape(arg))
  86. for arg in args])
  87. self.test_logs.append(fmt % args)
  88. def append_pass(self, report):
  89. self.passed += 1
  90. self._opentestcase(report)
  91. self._closetestcase()
  92. def append_failure(self, report):
  93. self._opentestcase(report)
  94. #msg = str(report.longrepr.reprtraceback.extraline)
  95. if "xfail" in report.keywords:
  96. self.appendlog(
  97. '<skipped message="xfail-marked test passes unexpectedly"/>')
  98. self.skipped += 1
  99. else:
  100. self.appendlog('<failure message="test failure">%s</failure>',
  101. report.longrepr)
  102. self.failed += 1
  103. self._closetestcase()
  104. def append_collect_failure(self, report):
  105. self._opentestcase(report)
  106. #msg = str(report.longrepr.reprtraceback.extraline)
  107. self.appendlog('<failure message="collection failure">%s</failure>',
  108. report.longrepr)
  109. self._closetestcase()
  110. self.errors += 1
  111. def append_collect_skipped(self, report):
  112. self._opentestcase(report)
  113. #msg = str(report.longrepr.reprtraceback.extraline)
  114. self.appendlog('<skipped message="collection skipped">%s</skipped>',
  115. report.longrepr)
  116. self._closetestcase()
  117. self.skipped += 1
  118. def append_error(self, report):
  119. self._opentestcase(report)
  120. self.appendlog('<error message="test setup failure">%s</error>',
  121. report.longrepr)
  122. self._closetestcase()
  123. self.errors += 1
  124. def append_skipped(self, report):
  125. self._opentestcase(report)
  126. if "xfail" in report.keywords:
  127. self.appendlog(
  128. '<skipped message="expected test failure">%s</skipped>',
  129. report.keywords['xfail'])
  130. else:
  131. filename, lineno, skipreason = report.longrepr
  132. if skipreason.startswith("Skipped: "):
  133. skipreason = skipreason[9:]
  134. self.appendlog('<skipped type="pytest.skip" '
  135. 'message="%s">%s</skipped>',
  136. skipreason, "%s:%s: %s" % report.longrepr,
  137. )
  138. self._closetestcase()
  139. self.skipped += 1
  140. def pytest_runtest_logreport(self, report):
  141. if report.passed:
  142. self.append_pass(report)
  143. elif report.failed:
  144. if report.when != "call":
  145. self.append_error(report)
  146. else:
  147. self.append_failure(report)
  148. elif report.skipped:
  149. self.append_skipped(report)
  150. def pytest_runtest_call(self, item, __multicall__):
  151. start = time.time()
  152. try:
  153. return __multicall__.execute()
  154. finally:
  155. self._durations[item.nodeid] = time.time() - start
  156. def pytest_collectreport(self, report):
  157. if not report.passed:
  158. if report.failed:
  159. self.append_collect_failure(report)
  160. else:
  161. self.append_collect_skipped(report)
  162. def pytest_internalerror(self, excrepr):
  163. self.errors += 1
  164. data = py.xml.escape(excrepr)
  165. self.test_logs.append(
  166. '\n<testcase classname="pytest" name="internal">'
  167. ' <error message="internal error">'
  168. '%s</error></testcase>' % data)
  169. def pytest_sessionstart(self, session):
  170. self.suite_start_time = time.time()
  171. def pytest_sessionfinish(self, session, exitstatus, __multicall__):
  172. if py.std.sys.version_info[0] < 3:
  173. logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8')
  174. else:
  175. logfile = open(self.logfile, 'w', encoding='utf-8')
  176. suite_stop_time = time.time()
  177. suite_time_delta = suite_stop_time - self.suite_start_time
  178. numtests = self.passed + self.failed
  179. logfile.write('<?xml version="1.0" encoding="utf-8"?>')
  180. logfile.write('<testsuite ')
  181. logfile.write('name="" ')
  182. logfile.write('errors="%i" ' % self.errors)
  183. logfile.write('failures="%i" ' % self.failed)
  184. logfile.write('skips="%i" ' % self.skipped)
  185. logfile.write('tests="%i" ' % numtests)
  186. logfile.write('time="%.3f"' % suite_time_delta)
  187. logfile.write(' >')
  188. logfile.writelines(self.test_logs)
  189. logfile.write('</testsuite>')
  190. logfile.close()
  191. def pytest_terminal_summary(self, terminalreporter):
  192. terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile))