terminal.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. """ terminal reporting of the full testing process.
  2. This is a good source for looking at the various reporting hooks.
  3. """
  4. from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
  5. EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
  6. import pytest
  7. import py
  8. import sys
  9. import time
  10. import platform
  11. import _pytest._pluggy as pluggy
  12. def pytest_addoption(parser):
  13. group = parser.getgroup("terminal reporting", "reporting", after="general")
  14. group._addoption('-v', '--verbose', action="count",
  15. dest="verbose", default=0, help="increase verbosity."),
  16. group._addoption('-q', '--quiet', action="count",
  17. dest="quiet", default=0, help="decrease verbosity."),
  18. group._addoption('-r',
  19. action="store", dest="reportchars", default=None, metavar="chars",
  20. help="show extra test summary info as specified by chars (f)ailed, "
  21. "(E)error, (s)skipped, (x)failed, (X)passed (w)pytest-warnings "
  22. "(p)passed, (P)passed with output, (a)all except pP.")
  23. group._addoption('-l', '--showlocals',
  24. action="store_true", dest="showlocals", default=False,
  25. help="show locals in tracebacks (disabled by default).")
  26. group._addoption('--report',
  27. action="store", dest="report", default=None, metavar="opts",
  28. help="(deprecated, use -r)")
  29. group._addoption('--tb', metavar="style",
  30. action="store", dest="tbstyle", default='auto',
  31. choices=['auto', 'long', 'short', 'no', 'line', 'native'],
  32. help="traceback print mode (auto/long/short/line/native/no).")
  33. group._addoption('--fulltrace', '--full-trace',
  34. action="store_true", default=False,
  35. help="don't cut any tracebacks (default is to cut).")
  36. group._addoption('--color', metavar="color",
  37. action="store", dest="color", default='auto',
  38. choices=['yes', 'no', 'auto'],
  39. help="color terminal output (yes/no/auto).")
  40. def pytest_configure(config):
  41. config.option.verbose -= config.option.quiet
  42. reporter = TerminalReporter(config, sys.stdout)
  43. config.pluginmanager.register(reporter, 'terminalreporter')
  44. if config.option.debug or config.option.traceconfig:
  45. def mywriter(tags, args):
  46. msg = " ".join(map(str, args))
  47. reporter.write_line("[traceconfig] " + msg)
  48. config.trace.root.setprocessor("pytest:config", mywriter)
  49. def getreportopt(config):
  50. reportopts = ""
  51. optvalue = config.option.report
  52. if optvalue:
  53. py.builtin.print_("DEPRECATED: use -r instead of --report option.",
  54. file=sys.stderr)
  55. if optvalue:
  56. for setting in optvalue.split(","):
  57. setting = setting.strip()
  58. if setting == "skipped":
  59. reportopts += "s"
  60. elif setting == "xfailed":
  61. reportopts += "x"
  62. reportchars = config.option.reportchars
  63. if reportchars:
  64. for char in reportchars:
  65. if char not in reportopts and char != 'a':
  66. reportopts += char
  67. elif char == 'a':
  68. reportopts = 'fEsxXw'
  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 WarningReport:
  81. def __init__(self, code, message, nodeid=None, fslocation=None):
  82. self.code = code
  83. self.message = message
  84. self.nodeid = nodeid
  85. self.fslocation = fslocation
  86. class TerminalReporter:
  87. def __init__(self, config, file=None):
  88. import _pytest.config
  89. self.config = config
  90. self.verbosity = self.config.option.verbose
  91. self.showheader = self.verbosity >= 0
  92. self.showfspath = self.verbosity >= 0
  93. self.showlongtestinfo = self.verbosity > 0
  94. self._numcollected = 0
  95. self.stats = {}
  96. self.startdir = py.path.local()
  97. if file is None:
  98. file = sys.stdout
  99. self._tw = self.writer = _pytest.config.create_terminal_writer(config,
  100. file)
  101. self.currentfspath = None
  102. self.reportchars = getreportopt(config)
  103. self.hasmarkup = self._tw.hasmarkup
  104. self.isatty = file.isatty()
  105. def hasopt(self, char):
  106. char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
  107. return char in self.reportchars
  108. def write_fspath_result(self, nodeid, res):
  109. fspath = self.config.rootdir.join(nodeid.split("::")[0])
  110. if fspath != self.currentfspath:
  111. self.currentfspath = fspath
  112. fspath = self.startdir.bestrelpath(fspath)
  113. self._tw.line()
  114. self._tw.write(fspath + " ")
  115. self._tw.write(res)
  116. def write_ensure_prefix(self, prefix, extra="", **kwargs):
  117. if self.currentfspath != prefix:
  118. self._tw.line()
  119. self.currentfspath = prefix
  120. self._tw.write(prefix)
  121. if extra:
  122. self._tw.write(extra, **kwargs)
  123. self.currentfspath = -2
  124. def ensure_newline(self):
  125. if self.currentfspath:
  126. self._tw.line()
  127. self.currentfspath = None
  128. def write(self, content, **markup):
  129. self._tw.write(content, **markup)
  130. def write_line(self, line, **markup):
  131. if not py.builtin._istext(line):
  132. line = py.builtin.text(line, errors="replace")
  133. self.ensure_newline()
  134. self._tw.line(line, **markup)
  135. def rewrite(self, line, **markup):
  136. line = str(line)
  137. self._tw.write("\r" + line, **markup)
  138. def write_sep(self, sep, title=None, **markup):
  139. self.ensure_newline()
  140. self._tw.sep(sep, title, **markup)
  141. def section(self, title, sep="=", **kw):
  142. self._tw.sep(sep, title, **kw)
  143. def line(self, msg, **kw):
  144. self._tw.line(msg, **kw)
  145. def pytest_internalerror(self, excrepr):
  146. for line in py.builtin.text(excrepr).split("\n"):
  147. self.write_line("INTERNALERROR> " + line)
  148. return 1
  149. def pytest_logwarning(self, code, fslocation, message, nodeid):
  150. warnings = self.stats.setdefault("warnings", [])
  151. if isinstance(fslocation, tuple):
  152. fslocation = "%s:%d" % fslocation
  153. warning = WarningReport(code=code, fslocation=fslocation,
  154. message=message, nodeid=nodeid)
  155. warnings.append(warning)
  156. def pytest_plugin_registered(self, plugin):
  157. if self.config.option.traceconfig:
  158. msg = "PLUGIN registered: %s" % (plugin,)
  159. # XXX this event may happen during setup/teardown time
  160. # which unfortunately captures our output here
  161. # which garbles our output if we use self.write_line
  162. self.write_line(msg)
  163. def pytest_deselected(self, items):
  164. self.stats.setdefault('deselected', []).extend(items)
  165. def pytest_runtest_logstart(self, nodeid, location):
  166. # ensure that the path is printed before the
  167. # 1st test of a module starts running
  168. if self.showlongtestinfo:
  169. line = self._locationline(nodeid, *location)
  170. self.write_ensure_prefix(line, "")
  171. elif self.showfspath:
  172. fsid = nodeid.split("::")[0]
  173. self.write_fspath_result(fsid, "")
  174. def pytest_runtest_logreport(self, report):
  175. rep = report
  176. res = self.config.hook.pytest_report_teststatus(report=rep)
  177. cat, letter, word = res
  178. self.stats.setdefault(cat, []).append(rep)
  179. self._tests_ran = True
  180. if not letter and not word:
  181. # probably passed setup/teardown
  182. return
  183. if self.verbosity <= 0:
  184. if not hasattr(rep, 'node') and self.showfspath:
  185. self.write_fspath_result(rep.nodeid, letter)
  186. else:
  187. self._tw.write(letter)
  188. else:
  189. if isinstance(word, tuple):
  190. word, markup = word
  191. else:
  192. if rep.passed:
  193. markup = {'green':True}
  194. elif rep.failed:
  195. markup = {'red':True}
  196. elif rep.skipped:
  197. markup = {'yellow':True}
  198. line = self._locationline(rep.nodeid, *rep.location)
  199. if not hasattr(rep, 'node'):
  200. self.write_ensure_prefix(line, word, **markup)
  201. #self._tw.write(word, **markup)
  202. else:
  203. self.ensure_newline()
  204. if hasattr(rep, 'node'):
  205. self._tw.write("[%s] " % rep.node.gateway.id)
  206. self._tw.write(word, **markup)
  207. self._tw.write(" " + line)
  208. self.currentfspath = -2
  209. def pytest_collection(self):
  210. if not self.isatty and self.config.option.verbose >= 1:
  211. self.write("collecting ... ", bold=True)
  212. def pytest_collectreport(self, report):
  213. if report.failed:
  214. self.stats.setdefault("error", []).append(report)
  215. elif report.skipped:
  216. self.stats.setdefault("skipped", []).append(report)
  217. items = [x for x in report.result if isinstance(x, pytest.Item)]
  218. self._numcollected += len(items)
  219. if self.isatty:
  220. #self.write_fspath_result(report.nodeid, 'E')
  221. self.report_collect()
  222. def report_collect(self, final=False):
  223. if self.config.option.verbose < 0:
  224. return
  225. errors = len(self.stats.get('error', []))
  226. skipped = len(self.stats.get('skipped', []))
  227. if final:
  228. line = "collected "
  229. else:
  230. line = "collecting "
  231. line += str(self._numcollected) + " items"
  232. if errors:
  233. line += " / %d errors" % errors
  234. if skipped:
  235. line += " / %d skipped" % skipped
  236. if self.isatty:
  237. if final:
  238. line += " \n"
  239. self.rewrite(line, bold=True)
  240. else:
  241. self.write_line(line)
  242. def pytest_collection_modifyitems(self):
  243. self.report_collect(True)
  244. @pytest.hookimpl(trylast=True)
  245. def pytest_sessionstart(self, session):
  246. self._sessionstarttime = time.time()
  247. if not self.showheader:
  248. return
  249. self.write_sep("=", "test session starts", bold=True)
  250. verinfo = platform.python_version()
  251. msg = "platform %s -- Python %s" % (sys.platform, verinfo)
  252. if hasattr(sys, 'pypy_version_info'):
  253. verinfo = ".".join(map(str, sys.pypy_version_info[:3]))
  254. msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3])
  255. msg += ", pytest-%s, py-%s, pluggy-%s" % (
  256. pytest.__version__, py.__version__, pluggy.__version__)
  257. if self.verbosity > 0 or self.config.option.debug or \
  258. getattr(self.config.option, 'pastebin', None):
  259. msg += " -- " + str(sys.executable)
  260. self.write_line(msg)
  261. lines = self.config.hook.pytest_report_header(
  262. config=self.config, startdir=self.startdir)
  263. lines.reverse()
  264. for line in flatten(lines):
  265. self.write_line(line)
  266. def pytest_report_header(self, config):
  267. inifile = ""
  268. if config.inifile:
  269. inifile = config.rootdir.bestrelpath(config.inifile)
  270. lines = ["rootdir: %s, inifile: %s" %(config.rootdir, inifile)]
  271. plugininfo = config.pluginmanager.list_plugin_distinfo()
  272. if plugininfo:
  273. lines.append(
  274. "plugins: %s" % ", ".join(_plugin_nameversions(plugininfo)))
  275. return lines
  276. def pytest_collection_finish(self, session):
  277. if self.config.option.collectonly:
  278. self._printcollecteditems(session.items)
  279. if self.stats.get('failed'):
  280. self._tw.sep("!", "collection failures")
  281. for rep in self.stats.get('failed'):
  282. rep.toterminal(self._tw)
  283. return 1
  284. return 0
  285. if not self.showheader:
  286. return
  287. #for i, testarg in enumerate(self.config.args):
  288. # self.write_line("test path %d: %s" %(i+1, testarg))
  289. def _printcollecteditems(self, items):
  290. # to print out items and their parent collectors
  291. # we take care to leave out Instances aka ()
  292. # because later versions are going to get rid of them anyway
  293. if self.config.option.verbose < 0:
  294. if self.config.option.verbose < -1:
  295. counts = {}
  296. for item in items:
  297. name = item.nodeid.split('::', 1)[0]
  298. counts[name] = counts.get(name, 0) + 1
  299. for name, count in sorted(counts.items()):
  300. self._tw.line("%s: %d" % (name, count))
  301. else:
  302. for item in items:
  303. nodeid = item.nodeid
  304. nodeid = nodeid.replace("::()::", "::")
  305. self._tw.line(nodeid)
  306. return
  307. stack = []
  308. indent = ""
  309. for item in items:
  310. needed_collectors = item.listchain()[1:] # strip root node
  311. while stack:
  312. if stack == needed_collectors[:len(stack)]:
  313. break
  314. stack.pop()
  315. for col in needed_collectors[len(stack):]:
  316. stack.append(col)
  317. #if col.name == "()":
  318. # continue
  319. indent = (len(stack) - 1) * " "
  320. self._tw.line("%s%s" % (indent, col))
  321. @pytest.hookimpl(hookwrapper=True)
  322. def pytest_sessionfinish(self, exitstatus):
  323. outcome = yield
  324. outcome.get_result()
  325. self._tw.line("")
  326. summary_exit_codes = (
  327. EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, EXIT_USAGEERROR,
  328. EXIT_NOTESTSCOLLECTED)
  329. if exitstatus in summary_exit_codes:
  330. self.config.hook.pytest_terminal_summary(terminalreporter=self)
  331. self.summary_errors()
  332. self.summary_failures()
  333. self.summary_warnings()
  334. self.summary_passes()
  335. if exitstatus == EXIT_INTERRUPTED:
  336. self._report_keyboardinterrupt()
  337. del self._keyboardinterrupt_memo
  338. self.summary_deselected()
  339. self.summary_stats()
  340. def pytest_keyboard_interrupt(self, excinfo):
  341. self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
  342. def pytest_unconfigure(self):
  343. if hasattr(self, '_keyboardinterrupt_memo'):
  344. self._report_keyboardinterrupt()
  345. def _report_keyboardinterrupt(self):
  346. excrepr = self._keyboardinterrupt_memo
  347. msg = excrepr.reprcrash.message
  348. self.write_sep("!", msg)
  349. if "KeyboardInterrupt" in msg:
  350. if self.config.option.fulltrace:
  351. excrepr.toterminal(self._tw)
  352. else:
  353. self._tw.line("to show a full traceback on KeyboardInterrupt use --fulltrace", yellow=True)
  354. excrepr.reprcrash.toterminal(self._tw)
  355. def _locationline(self, nodeid, fspath, lineno, domain):
  356. def mkrel(nodeid):
  357. line = self.config.cwd_relative_nodeid(nodeid)
  358. if domain and line.endswith(domain):
  359. line = line[:-len(domain)]
  360. l = domain.split("[")
  361. l[0] = l[0].replace('.', '::') # don't replace '.' in params
  362. line += "[".join(l)
  363. return line
  364. # collect_fspath comes from testid which has a "/"-normalized path
  365. if fspath:
  366. res = mkrel(nodeid).replace("::()", "") # parens-normalization
  367. if nodeid.split("::")[0] != fspath.replace("\\", "/"):
  368. res += " <- " + self.startdir.bestrelpath(fspath)
  369. else:
  370. res = "[location]"
  371. return res + " "
  372. def _getfailureheadline(self, rep):
  373. if hasattr(rep, 'location'):
  374. fspath, lineno, domain = rep.location
  375. return domain
  376. else:
  377. return "test session" # XXX?
  378. def _getcrashline(self, rep):
  379. try:
  380. return str(rep.longrepr.reprcrash)
  381. except AttributeError:
  382. try:
  383. return str(rep.longrepr)[:50]
  384. except AttributeError:
  385. return ""
  386. #
  387. # summaries for sessionfinish
  388. #
  389. def getreports(self, name):
  390. l = []
  391. for x in self.stats.get(name, []):
  392. if not hasattr(x, '_pdbshown'):
  393. l.append(x)
  394. return l
  395. def summary_warnings(self):
  396. if self.hasopt("w"):
  397. warnings = self.stats.get("warnings")
  398. if not warnings:
  399. return
  400. self.write_sep("=", "pytest-warning summary")
  401. for w in warnings:
  402. self._tw.line("W%s %s %s" % (w.code,
  403. w.fslocation, w.message))
  404. def summary_passes(self):
  405. if self.config.option.tbstyle != "no":
  406. if self.hasopt("P"):
  407. reports = self.getreports('passed')
  408. if not reports:
  409. return
  410. self.write_sep("=", "PASSES")
  411. for rep in reports:
  412. msg = self._getfailureheadline(rep)
  413. self.write_sep("_", msg)
  414. self._outrep_summary(rep)
  415. def summary_failures(self):
  416. if self.config.option.tbstyle != "no":
  417. reports = self.getreports('failed')
  418. if not reports:
  419. return
  420. self.write_sep("=", "FAILURES")
  421. for rep in reports:
  422. if self.config.option.tbstyle == "line":
  423. line = self._getcrashline(rep)
  424. self.write_line(line)
  425. else:
  426. msg = self._getfailureheadline(rep)
  427. markup = {'red': True, 'bold': True}
  428. self.write_sep("_", msg, **markup)
  429. self._outrep_summary(rep)
  430. def summary_errors(self):
  431. if self.config.option.tbstyle != "no":
  432. reports = self.getreports('error')
  433. if not reports:
  434. return
  435. self.write_sep("=", "ERRORS")
  436. for rep in self.stats['error']:
  437. msg = self._getfailureheadline(rep)
  438. if not hasattr(rep, 'when'):
  439. # collect
  440. msg = "ERROR collecting " + msg
  441. elif rep.when == "setup":
  442. msg = "ERROR at setup of " + msg
  443. elif rep.when == "teardown":
  444. msg = "ERROR at teardown of " + msg
  445. self.write_sep("_", msg)
  446. self._outrep_summary(rep)
  447. def _outrep_summary(self, rep):
  448. rep.toterminal(self._tw)
  449. for secname, content in rep.sections:
  450. self._tw.sep("-", secname)
  451. if content[-1:] == "\n":
  452. content = content[:-1]
  453. self._tw.line(content)
  454. def summary_stats(self):
  455. session_duration = time.time() - self._sessionstarttime
  456. (line, color) = build_summary_stats_line(self.stats)
  457. msg = "%s in %.2f seconds" % (line, session_duration)
  458. markup = {color: True, 'bold': True}
  459. if self.verbosity >= 0:
  460. self.write_sep("=", msg, **markup)
  461. if self.verbosity == -1:
  462. self.write_line(msg, **markup)
  463. def summary_deselected(self):
  464. if 'deselected' in self.stats:
  465. l = []
  466. k = self.config.option.keyword
  467. if k:
  468. l.append("-k%s" % k)
  469. m = self.config.option.markexpr
  470. if m:
  471. l.append("-m %r" % m)
  472. if l:
  473. self.write_sep("=", "%d tests deselected by %r" % (
  474. len(self.stats['deselected']), " ".join(l)), bold=True)
  475. def repr_pythonversion(v=None):
  476. if v is None:
  477. v = sys.version_info
  478. try:
  479. return "%s.%s.%s-%s-%s" % v
  480. except (TypeError, ValueError):
  481. return str(v)
  482. def flatten(l):
  483. for x in l:
  484. if isinstance(x, (list, tuple)):
  485. for y in flatten(x):
  486. yield y
  487. else:
  488. yield x
  489. def build_summary_stats_line(stats):
  490. keys = ("failed passed skipped deselected "
  491. "xfailed xpassed warnings error").split()
  492. key_translation = {'warnings': 'pytest-warnings'}
  493. unknown_key_seen = False
  494. for key in stats.keys():
  495. if key not in keys:
  496. if key: # setup/teardown reports have an empty key, ignore them
  497. keys.append(key)
  498. unknown_key_seen = True
  499. parts = []
  500. for key in keys:
  501. val = stats.get(key, None)
  502. if val:
  503. key_name = key_translation.get(key, key)
  504. parts.append("%d %s" % (len(val), key_name))
  505. if parts:
  506. line = ", ".join(parts)
  507. else:
  508. line = "no tests ran"
  509. if 'failed' in stats or 'error' in stats:
  510. color = 'red'
  511. elif 'warnings' in stats or unknown_key_seen:
  512. color = 'yellow'
  513. elif 'passed' in stats:
  514. color = 'green'
  515. else:
  516. color = 'yellow'
  517. return (line, color)
  518. def _plugin_nameversions(plugininfo):
  519. l = []
  520. for plugin, dist in plugininfo:
  521. # gets us name and version!
  522. name = '{dist.project_name}-{dist.version}'.format(dist=dist)
  523. # questionable convenience, but it keeps things short
  524. if name.startswith("pytest-"):
  525. name = name[7:]
  526. # we decided to print python package names
  527. # they can have more than one plugin
  528. if name not in l:
  529. l.append(name)
  530. return l