PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/_pytest/terminal.py

https://bitbucket.org/dac_io/pypy
Python | 473 lines | 460 code | 6 blank | 7 comment | 17 complexity | 1003caae3539b55d7015f52441af51e5 MD5 | raw file
  1. """ terminal reporting of the full testing process.
  2. This is a good source for looking at the various reporting hooks.
  3. """
  4. import pytest, py
  5. import sys
  6. import os
  7. def pytest_addoption(parser):
  8. group = parser.getgroup("terminal reporting", "reporting", after="general")
  9. group._addoption('-v', '--verbose', action="count",
  10. dest="verbose", default=0, help="increase verbosity."),
  11. group._addoption('-q', '--quiet', action="count",
  12. dest="quiet", default=0, help="decreate verbosity."),
  13. group._addoption('-r',
  14. action="store", dest="reportchars", default=None, metavar="chars",
  15. help="show extra test summary info as specified by chars (f)ailed, "
  16. "(E)error, (s)skipped, (x)failed, (X)passed.")
  17. group._addoption('-l', '--showlocals',
  18. action="store_true", dest="showlocals", default=False,
  19. help="show locals in tracebacks (disabled by default).")
  20. group._addoption('--report',
  21. action="store", dest="report", default=None, metavar="opts",
  22. help="(deprecated, use -r)")
  23. group._addoption('--tb', metavar="style",
  24. action="store", dest="tbstyle", default='long',
  25. type="choice", choices=['long', 'short', 'no', 'line', 'native'],
  26. help="traceback print mode (long/short/line/native/no).")
  27. group._addoption('--fulltrace',
  28. action="store_true", dest="fulltrace", default=False,
  29. help="don't cut any tracebacks (default is to cut).")
  30. def pytest_configure(config):
  31. config.option.verbose -= config.option.quiet
  32. # we try hard to make printing resilient against
  33. # later changes on FD level.
  34. stdout = py.std.sys.stdout
  35. if hasattr(os, 'dup') and hasattr(stdout, 'fileno'):
  36. try:
  37. newfd = os.dup(stdout.fileno())
  38. #print "got newfd", newfd
  39. except ValueError:
  40. pass
  41. else:
  42. stdout = os.fdopen(newfd, stdout.mode, 1)
  43. config._cleanup.append(lambda: stdout.close())
  44. reporter = TerminalReporter(config, stdout)
  45. config.pluginmanager.register(reporter, 'terminalreporter')
  46. if config.option.debug or config.option.traceconfig:
  47. def mywriter(tags, args):
  48. msg = " ".join(map(str, args))
  49. reporter.write_line("[traceconfig] " + msg)
  50. config.trace.root.setprocessor("pytest:config", mywriter)
  51. def getreportopt(config):
  52. reportopts = ""
  53. optvalue = config.option.report
  54. if optvalue:
  55. py.builtin.print_("DEPRECATED: use -r instead of --report option.",
  56. file=py.std.sys.stderr)
  57. if optvalue:
  58. for setting in optvalue.split(","):
  59. setting = setting.strip()
  60. if setting == "skipped":
  61. reportopts += "s"
  62. elif setting == "xfailed":
  63. reportopts += "x"
  64. reportchars = config.option.reportchars
  65. if reportchars:
  66. for char in reportchars:
  67. if char not in reportopts:
  68. reportopts += char
  69. return reportopts
  70. def pytest_report_teststatus(report):
  71. if report.passed:
  72. letter = "."
  73. elif report.skipped:
  74. letter = "s"
  75. elif report.failed:
  76. letter = "F"
  77. if report.when != "call":
  78. letter = "f"
  79. return report.outcome, letter, report.outcome.upper()
  80. class TerminalReporter:
  81. def __init__(self, config, file=None):
  82. self.config = config
  83. self.verbosity = self.config.option.verbose
  84. self.showheader = self.verbosity >= 0
  85. self.showfspath = self.verbosity >= 0
  86. self.showlongtestinfo = self.verbosity > 0
  87. self._numcollected = 0
  88. self.stats = {}
  89. self.curdir = py.path.local()
  90. if file is None:
  91. file = py.std.sys.stdout
  92. self._tw = py.io.TerminalWriter(file)
  93. self.currentfspath = None
  94. self.reportchars = getreportopt(config)
  95. self.hasmarkup = self._tw.hasmarkup
  96. def hasopt(self, char):
  97. char = {'xfailed': 'x', 'skipped': 's'}.get(char,char)
  98. return char in self.reportchars
  99. def write_fspath_result(self, fspath, res):
  100. if fspath != self.currentfspath:
  101. self.currentfspath = fspath
  102. #fspath = self.curdir.bestrelpath(fspath)
  103. self._tw.line()
  104. #relpath = self.curdir.bestrelpath(fspath)
  105. self._tw.write(fspath + " ")
  106. self._tw.write(res)
  107. def write_ensure_prefix(self, prefix, extra="", **kwargs):
  108. if self.currentfspath != prefix:
  109. self._tw.line()
  110. self.currentfspath = prefix
  111. self._tw.write(prefix)
  112. if extra:
  113. self._tw.write(extra, **kwargs)
  114. self.currentfspath = -2
  115. def ensure_newline(self):
  116. if self.currentfspath:
  117. self._tw.line()
  118. self.currentfspath = None
  119. def write(self, content, **markup):
  120. self._tw.write(content, **markup)
  121. def write_line(self, line, **markup):
  122. line = str(line)
  123. self.ensure_newline()
  124. self._tw.line(line, **markup)
  125. def rewrite(self, line, **markup):
  126. line = str(line)
  127. self._tw.write("\r" + line, **markup)
  128. def write_sep(self, sep, title=None, **markup):
  129. self.ensure_newline()
  130. self._tw.sep(sep, title, **markup)
  131. def pytest_internalerror(self, excrepr):
  132. for line in str(excrepr).split("\n"):
  133. self.write_line("INTERNALERROR> " + line)
  134. return 1
  135. def pytest_plugin_registered(self, plugin):
  136. if self.config.option.traceconfig:
  137. msg = "PLUGIN registered: %s" %(plugin,)
  138. # XXX this event may happen during setup/teardown time
  139. # which unfortunately captures our output here
  140. # which garbles our output if we use self.write_line
  141. self.write_line(msg)
  142. def pytest_deselected(self, items):
  143. self.stats.setdefault('deselected', []).extend(items)
  144. def pytest_runtest_logstart(self, nodeid, location):
  145. # ensure that the path is printed before the
  146. # 1st test of a module starts running
  147. fspath = nodeid.split("::")[0]
  148. if self.showlongtestinfo:
  149. line = self._locationline(fspath, *location)
  150. self.write_ensure_prefix(line, "")
  151. elif self.showfspath:
  152. self.write_fspath_result(fspath, "")
  153. def pytest_runtest_logreport(self, report):
  154. rep = report
  155. res = self.config.hook.pytest_report_teststatus(report=rep)
  156. cat, letter, word = res
  157. self.stats.setdefault(cat, []).append(rep)
  158. if not letter and not word:
  159. # probably passed setup/teardown
  160. return
  161. if self.verbosity <= 0:
  162. if not hasattr(rep, 'node') and self.showfspath:
  163. self.write_fspath_result(rep.fspath, letter)
  164. else:
  165. self._tw.write(letter)
  166. else:
  167. if isinstance(word, tuple):
  168. word, markup = word
  169. else:
  170. if rep.passed:
  171. markup = {'green':True}
  172. elif rep.failed:
  173. markup = {'red':True}
  174. elif rep.skipped:
  175. markup = {'yellow':True}
  176. line = self._locationline(str(rep.fspath), *rep.location)
  177. if not hasattr(rep, 'node'):
  178. self.write_ensure_prefix(line, word, **markup)
  179. #self._tw.write(word, **markup)
  180. else:
  181. self.ensure_newline()
  182. if hasattr(rep, 'node'):
  183. self._tw.write("[%s] " % rep.node.gateway.id)
  184. self._tw.write(word, **markup)
  185. self._tw.write(" " + line)
  186. self.currentfspath = -2
  187. def pytest_collection(self):
  188. if not self.hasmarkup:
  189. self.write("collecting ... ", bold=True)
  190. def pytest_collectreport(self, report):
  191. if report.failed:
  192. self.stats.setdefault("error", []).append(report)
  193. elif report.skipped:
  194. self.stats.setdefault("skipped", []).append(report)
  195. items = [x for x in report.result if isinstance(x, pytest.Item)]
  196. self._numcollected += len(items)
  197. if self.hasmarkup:
  198. #self.write_fspath_result(report.fspath, 'E')
  199. self.report_collect()
  200. def report_collect(self, final=False):
  201. errors = len(self.stats.get('error', []))
  202. skipped = len(self.stats.get('skipped', []))
  203. if final:
  204. line = "collected "
  205. else:
  206. line = "collecting "
  207. line += str(self._numcollected) + " items"
  208. if errors:
  209. line += " / %d errors" % errors
  210. if skipped:
  211. line += " / %d skipped" % skipped
  212. if self.hasmarkup:
  213. if final:
  214. line += " \n"
  215. self.rewrite(line, bold=True)
  216. else:
  217. self.write_line(line)
  218. def pytest_collection_modifyitems(self):
  219. self.report_collect(True)
  220. def pytest_sessionstart(self, session):
  221. self._sessionstarttime = py.std.time.time()
  222. if not self.showheader:
  223. return
  224. self.write_sep("=", "test session starts", bold=True)
  225. verinfo = ".".join(map(str, sys.version_info[:3]))
  226. msg = "platform %s -- Python %s" % (sys.platform, verinfo)
  227. if hasattr(sys, 'pypy_version_info'):
  228. verinfo = ".".join(map(str, sys.pypy_version_info[:3]))
  229. msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3])
  230. msg += " -- pytest-%s" % (py.test.__version__)
  231. if self.verbosity > 0 or self.config.option.debug or \
  232. getattr(self.config.option, 'pastebin', None):
  233. msg += " -- " + str(sys.executable)
  234. self.write_line(msg)
  235. lines = self.config.hook.pytest_report_header(config=self.config)
  236. lines.reverse()
  237. for line in flatten(lines):
  238. self.write_line(line)
  239. def pytest_collection_finish(self, session):
  240. if self.config.option.collectonly:
  241. self._printcollecteditems(session.items)
  242. if self.stats.get('failed'):
  243. self._tw.sep("!", "collection failures")
  244. for rep in self.stats.get('failed'):
  245. rep.toterminal(self._tw)
  246. return 1
  247. return 0
  248. if not self.showheader:
  249. return
  250. #for i, testarg in enumerate(self.config.args):
  251. # self.write_line("test path %d: %s" %(i+1, testarg))
  252. def _printcollecteditems(self, items):
  253. # to print out items and their parent collectors
  254. # we take care to leave out Instances aka ()
  255. # because later versions are going to get rid of them anyway
  256. if self.config.option.verbose < 0:
  257. if self.config.option.verbose < -1:
  258. counts = {}
  259. for item in items:
  260. name = item.nodeid.split('::', 1)[0]
  261. counts[name] = counts.get(name, 0) + 1
  262. for name, count in sorted(counts.items()):
  263. self._tw.line("%s: %d" % (name, count))
  264. else:
  265. for item in items:
  266. nodeid = item.nodeid
  267. nodeid = nodeid.replace("::()::", "::")
  268. self._tw.line(nodeid)
  269. return
  270. stack = []
  271. indent = ""
  272. for item in items:
  273. needed_collectors = item.listchain()[1:] # strip root node
  274. while stack:
  275. if stack == needed_collectors[:len(stack)]:
  276. break
  277. stack.pop()
  278. for col in needed_collectors[len(stack):]:
  279. stack.append(col)
  280. #if col.name == "()":
  281. # continue
  282. indent = (len(stack)-1) * " "
  283. self._tw.line("%s%s" %(indent, col))
  284. def pytest_sessionfinish(self, exitstatus, __multicall__):
  285. __multicall__.execute()
  286. self._tw.line("")
  287. if exitstatus in (0, 1, 2):
  288. self.summary_errors()
  289. self.summary_failures()
  290. self.config.hook.pytest_terminal_summary(terminalreporter=self)
  291. if exitstatus == 2:
  292. self._report_keyboardinterrupt()
  293. del self._keyboardinterrupt_memo
  294. self.summary_deselected()
  295. self.summary_stats()
  296. def pytest_keyboard_interrupt(self, excinfo):
  297. self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
  298. def pytest_unconfigure(self):
  299. if hasattr(self, '_keyboardinterrupt_memo'):
  300. self._report_keyboardinterrupt()
  301. def _report_keyboardinterrupt(self):
  302. excrepr = self._keyboardinterrupt_memo
  303. msg = excrepr.reprcrash.message
  304. self.write_sep("!", msg)
  305. if "KeyboardInterrupt" in msg:
  306. if self.config.option.fulltrace:
  307. excrepr.toterminal(self._tw)
  308. else:
  309. excrepr.reprcrash.toterminal(self._tw)
  310. def _locationline(self, collect_fspath, fspath, lineno, domain):
  311. # collect_fspath comes from testid which has a "/"-normalized path
  312. if fspath and fspath.replace("\\", "/") != collect_fspath:
  313. fspath = "%s <- %s" % (collect_fspath, fspath)
  314. if fspath:
  315. line = str(fspath)
  316. if lineno is not None:
  317. lineno += 1
  318. line += ":" + str(lineno)
  319. if domain:
  320. line += ": " + str(domain)
  321. else:
  322. line = "[location]"
  323. return line + " "
  324. def _getfailureheadline(self, rep):
  325. if hasattr(rep, 'location'):
  326. fspath, lineno, domain = rep.location
  327. return domain
  328. else:
  329. return "test session" # XXX?
  330. def _getcrashline(self, rep):
  331. try:
  332. return str(rep.longrepr.reprcrash)
  333. except AttributeError:
  334. try:
  335. return str(rep.longrepr)[:50]
  336. except AttributeError:
  337. return ""
  338. #
  339. # summaries for sessionfinish
  340. #
  341. def getreports(self, name):
  342. l = []
  343. for x in self.stats.get(name, []):
  344. if not hasattr(x, '_pdbshown'):
  345. l.append(x)
  346. return l
  347. def summary_failures(self):
  348. if self.config.option.tbstyle != "no":
  349. reports = self.getreports('failed')
  350. if not reports:
  351. return
  352. self.write_sep("=", "FAILURES")
  353. for rep in reports:
  354. if self.config.option.tbstyle == "line":
  355. line = self._getcrashline(rep)
  356. self.write_line(line)
  357. else:
  358. msg = self._getfailureheadline(rep)
  359. self.write_sep("_", msg)
  360. self._outrep_summary(rep)
  361. def summary_errors(self):
  362. if self.config.option.tbstyle != "no":
  363. reports = self.getreports('error')
  364. if not reports:
  365. return
  366. self.write_sep("=", "ERRORS")
  367. for rep in self.stats['error']:
  368. msg = self._getfailureheadline(rep)
  369. if not hasattr(rep, 'when'):
  370. # collect
  371. msg = "ERROR collecting " + msg
  372. elif rep.when == "setup":
  373. msg = "ERROR at setup of " + msg
  374. elif rep.when == "teardown":
  375. msg = "ERROR at teardown of " + msg
  376. self.write_sep("_", msg)
  377. self._outrep_summary(rep)
  378. def _outrep_summary(self, rep):
  379. rep.toterminal(self._tw)
  380. for secname, content in rep.sections:
  381. self._tw.sep("-", secname)
  382. if content[-1:] == "\n":
  383. content = content[:-1]
  384. self._tw.line(content)
  385. def summary_stats(self):
  386. session_duration = py.std.time.time() - self._sessionstarttime
  387. keys = "failed passed skipped deselected".split()
  388. for key in self.stats.keys():
  389. if key not in keys:
  390. keys.append(key)
  391. parts = []
  392. for key in keys:
  393. if key: # setup/teardown reports have an empty key, ignore them
  394. val = self.stats.get(key, None)
  395. if val:
  396. parts.append("%d %s" %(len(val), key))
  397. line = ", ".join(parts)
  398. # XXX coloring
  399. msg = "%s in %.2f seconds" %(line, session_duration)
  400. if self.verbosity >= 0:
  401. self.write_sep("=", msg, bold=True)
  402. else:
  403. self.write_line(msg, bold=True)
  404. def summary_deselected(self):
  405. if 'deselected' in self.stats:
  406. l = []
  407. k = self.config.option.keyword
  408. if k:
  409. l.append("-k%s" % k)
  410. m = self.config.option.markexpr
  411. if m:
  412. l.append("-m %r" % m)
  413. self.write_sep("=", "%d tests deselected by %r" %(
  414. len(self.stats['deselected']), " ".join(l)), bold=True)
  415. def repr_pythonversion(v=None):
  416. if v is None:
  417. v = sys.version_info
  418. try:
  419. return "%s.%s.%s-%s-%s" % v
  420. except (TypeError, ValueError):
  421. return str(v)
  422. def flatten(l):
  423. for x in l:
  424. if isinstance(x, (list, tuple)):
  425. for y in flatten(x):
  426. yield y
  427. else:
  428. yield x