PageRenderTime 49ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/_pytest/junitxml.py

https://bitbucket.org/pwaller/pypy
Python | 220 lines | 197 code | 13 blank | 10 comment | 12 complexity | 3ec9b0896953cc766182d970ef022833 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. class Junit(py.xml.Namespace):
  23. pass
  24. # We need to get the subset of the invalid unicode ranges according to
  25. # XML 1.0 which are valid in this python build. Hence we calculate
  26. # this dynamically instead of hardcoding it. The spec range of valid
  27. # chars is: Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
  28. # | [#x10000-#x10FFFF]
  29. _legal_chars = (0x09, 0x0A, 0x0d)
  30. _legal_ranges = (
  31. (0x20, 0xD7FF),
  32. (0xE000, 0xFFFD),
  33. (0x10000, 0x10FFFF),
  34. )
  35. _legal_xml_re = [unicode("%s-%s") % (unichr(low), unichr(high))
  36. for (low, high) in _legal_ranges
  37. if low < sys.maxunicode]
  38. _legal_xml_re = [unichr(x) for x in _legal_chars] + _legal_xml_re
  39. illegal_xml_re = re.compile(unicode('[^%s]') %
  40. unicode('').join(_legal_xml_re))
  41. del _legal_chars
  42. del _legal_ranges
  43. del _legal_xml_re
  44. def bin_xml_escape(arg):
  45. def repl(matchobj):
  46. i = ord(matchobj.group())
  47. if i <= 0xFF:
  48. return unicode('#x%02X') % i
  49. else:
  50. return unicode('#x%04X') % i
  51. return illegal_xml_re.sub(repl, py.xml.escape(arg))
  52. def pytest_addoption(parser):
  53. group = parser.getgroup("terminal reporting")
  54. group.addoption('--junitxml', action="store", dest="xmlpath",
  55. metavar="path", default=None,
  56. help="create junit-xml style report file at given path.")
  57. group.addoption('--junitprefix', action="store", dest="junitprefix",
  58. metavar="str", default=None,
  59. help="prepend prefix to classnames in junit-xml output")
  60. def pytest_configure(config):
  61. xmlpath = config.option.xmlpath
  62. if xmlpath:
  63. config._xml = LogXML(xmlpath, config.option.junitprefix)
  64. config.pluginmanager.register(config._xml)
  65. def pytest_unconfigure(config):
  66. xml = getattr(config, '_xml', None)
  67. if xml:
  68. del config._xml
  69. config.pluginmanager.unregister(xml)
  70. class LogXML(object):
  71. def __init__(self, logfile, prefix):
  72. logfile = os.path.expanduser(os.path.expandvars(logfile))
  73. self.logfile = os.path.normpath(logfile)
  74. self.prefix = prefix
  75. self.tests = []
  76. self.passed = self.skipped = 0
  77. self.failed = self.errors = 0
  78. def _opentestcase(self, report):
  79. names = report.nodeid.split("::")
  80. names[0] = names[0].replace("/", '.')
  81. names = [x.replace(".py", "") for x in names if x != "()"]
  82. classnames = names[:-1]
  83. if self.prefix:
  84. classnames.insert(0, self.prefix)
  85. self.tests.append(Junit.testcase(
  86. classname=".".join(classnames),
  87. name=names[-1],
  88. time=getattr(report, 'duration', 0)
  89. ))
  90. def append(self, obj):
  91. self.tests[-1].append(obj)
  92. def append_pass(self, report):
  93. self.passed += 1
  94. def append_failure(self, report):
  95. #msg = str(report.longrepr.reprtraceback.extraline)
  96. if "xfail" in report.keywords:
  97. self.append(
  98. Junit.skipped(message="xfail-marked test passes unexpectedly"))
  99. self.skipped += 1
  100. else:
  101. sec = dict(report.sections)
  102. fail = Junit.failure(message="test failure")
  103. fail.append(str(report.longrepr))
  104. self.append(fail)
  105. for name in ('out', 'err'):
  106. content = sec.get("Captured std%s" % name)
  107. if content:
  108. tag = getattr(Junit, 'system-'+name)
  109. self.append(tag(bin_xml_escape(content)))
  110. self.failed += 1
  111. def append_collect_failure(self, report):
  112. #msg = str(report.longrepr.reprtraceback.extraline)
  113. self.append(Junit.failure(str(report.longrepr),
  114. message="collection failure"))
  115. self.errors += 1
  116. def append_collect_skipped(self, report):
  117. #msg = str(report.longrepr.reprtraceback.extraline)
  118. self.append(Junit.skipped(str(report.longrepr),
  119. message="collection skipped"))
  120. self.skipped += 1
  121. def append_error(self, report):
  122. self.append(Junit.error(str(report.longrepr),
  123. message="test setup failure"))
  124. self.errors += 1
  125. def append_skipped(self, report):
  126. if "xfail" in report.keywords:
  127. self.append(Junit.skipped(str(report.keywords['xfail']),
  128. message="expected test failure"))
  129. else:
  130. filename, lineno, skipreason = report.longrepr
  131. if skipreason.startswith("Skipped: "):
  132. skipreason = skipreason[9:]
  133. self.append(
  134. Junit.skipped("%s:%s: %s" % report.longrepr,
  135. type="pytest.skip",
  136. message=skipreason
  137. ))
  138. self.skipped += 1
  139. def pytest_runtest_logreport(self, report):
  140. if report.passed:
  141. if report.when == "call": # ignore setup/teardown
  142. self._opentestcase(report)
  143. self.append_pass(report)
  144. elif report.failed:
  145. self._opentestcase(report)
  146. if report.when != "call":
  147. self.append_error(report)
  148. else:
  149. self.append_failure(report)
  150. elif report.skipped:
  151. self._opentestcase(report)
  152. self.append_skipped(report)
  153. def pytest_collectreport(self, report):
  154. if not report.passed:
  155. self._opentestcase(report)
  156. if report.failed:
  157. self.append_collect_failure(report)
  158. else:
  159. self.append_collect_skipped(report)
  160. def pytest_internalerror(self, excrepr):
  161. self.errors += 1
  162. data = py.xml.escape(excrepr)
  163. self.tests.append(
  164. Junit.testcase(
  165. Junit.error(data, message="internal error"),
  166. classname="pytest",
  167. name="internal"))
  168. def pytest_sessionstart(self, session):
  169. self.suite_start_time = time.time()
  170. def pytest_sessionfinish(self, session, exitstatus, __multicall__):
  171. if py.std.sys.version_info[0] < 3:
  172. logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8')
  173. else:
  174. logfile = open(self.logfile, 'w', encoding='utf-8')
  175. suite_stop_time = time.time()
  176. suite_time_delta = suite_stop_time - self.suite_start_time
  177. numtests = self.passed + self.failed
  178. logfile.write('<?xml version="1.0" encoding="utf-8"?>')
  179. logfile.write(Junit.testsuite(
  180. self.tests,
  181. name="",
  182. errors=self.errors,
  183. failures=self.failed,
  184. skips=self.skipped,
  185. tests=numtests,
  186. time="%.3f" % suite_time_delta,
  187. ).unicode(indent=0))
  188. logfile.close()
  189. def pytest_terminal_summary(self, terminalreporter):
  190. terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile))