terminal.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090
  1. # -*- coding: utf-8 -*-
  2. """ terminal reporting of the full testing process.
  3. This is a good source for looking at the various reporting hooks.
  4. """
  5. from __future__ import absolute_import
  6. from __future__ import division
  7. from __future__ import print_function
  8. import argparse
  9. import collections
  10. import platform
  11. import sys
  12. import time
  13. from functools import partial
  14. import attr
  15. import pluggy
  16. import py
  17. import six
  18. from more_itertools import collapse
  19. import pytest
  20. from _pytest import nodes
  21. from _pytest.main import EXIT_INTERRUPTED
  22. from _pytest.main import EXIT_NOTESTSCOLLECTED
  23. from _pytest.main import EXIT_OK
  24. from _pytest.main import EXIT_TESTSFAILED
  25. from _pytest.main import EXIT_USAGEERROR
  26. REPORT_COLLECTING_RESOLUTION = 0.5
  27. class MoreQuietAction(argparse.Action):
  28. """
  29. a modified copy of the argparse count action which counts down and updates
  30. the legacy quiet attribute at the same time
  31. used to unify verbosity handling
  32. """
  33. def __init__(self, option_strings, dest, default=None, required=False, help=None):
  34. super(MoreQuietAction, self).__init__(
  35. option_strings=option_strings,
  36. dest=dest,
  37. nargs=0,
  38. default=default,
  39. required=required,
  40. help=help,
  41. )
  42. def __call__(self, parser, namespace, values, option_string=None):
  43. new_count = getattr(namespace, self.dest, 0) - 1
  44. setattr(namespace, self.dest, new_count)
  45. # todo Deprecate config.quiet
  46. namespace.quiet = getattr(namespace, "quiet", 0) + 1
  47. def pytest_addoption(parser):
  48. group = parser.getgroup("terminal reporting", "reporting", after="general")
  49. group._addoption(
  50. "-v",
  51. "--verbose",
  52. action="count",
  53. default=0,
  54. dest="verbose",
  55. help="increase verbosity.",
  56. ),
  57. group._addoption(
  58. "-q",
  59. "--quiet",
  60. action=MoreQuietAction,
  61. default=0,
  62. dest="verbose",
  63. help="decrease verbosity.",
  64. ),
  65. group._addoption(
  66. "--verbosity", dest="verbose", type=int, default=0, help="set verbosity"
  67. )
  68. group._addoption(
  69. "-r",
  70. action="store",
  71. dest="reportchars",
  72. default="",
  73. metavar="chars",
  74. help="show extra test summary info as specified by chars: (f)ailed, "
  75. "(E)rror, (s)kipped, (x)failed, (X)passed, "
  76. "(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. "
  77. "Warnings are displayed at all times except when "
  78. "--disable-warnings is set.",
  79. )
  80. group._addoption(
  81. "--disable-warnings",
  82. "--disable-pytest-warnings",
  83. default=False,
  84. dest="disable_warnings",
  85. action="store_true",
  86. help="disable warnings summary",
  87. )
  88. group._addoption(
  89. "-l",
  90. "--showlocals",
  91. action="store_true",
  92. dest="showlocals",
  93. default=False,
  94. help="show locals in tracebacks (disabled by default).",
  95. )
  96. group._addoption(
  97. "--tb",
  98. metavar="style",
  99. action="store",
  100. dest="tbstyle",
  101. default="auto",
  102. choices=["auto", "long", "short", "no", "line", "native"],
  103. help="traceback print mode (auto/long/short/line/native/no).",
  104. )
  105. group._addoption(
  106. "--show-capture",
  107. action="store",
  108. dest="showcapture",
  109. choices=["no", "stdout", "stderr", "log", "all"],
  110. default="all",
  111. help="Controls how captured stdout/stderr/log is shown on failed tests. "
  112. "Default is 'all'.",
  113. )
  114. group._addoption(
  115. "--fulltrace",
  116. "--full-trace",
  117. action="store_true",
  118. default=False,
  119. help="don't cut any tracebacks (default is to cut).",
  120. )
  121. group._addoption(
  122. "--color",
  123. metavar="color",
  124. action="store",
  125. dest="color",
  126. default="auto",
  127. choices=["yes", "no", "auto"],
  128. help="color terminal output (yes/no/auto).",
  129. )
  130. parser.addini(
  131. "console_output_style",
  132. help='console output: "classic", or with additional progress information ("progress" (percentage) | "count").',
  133. default="progress",
  134. )
  135. def pytest_configure(config):
  136. reporter = TerminalReporter(config, sys.stdout)
  137. config.pluginmanager.register(reporter, "terminalreporter")
  138. if config.option.debug or config.option.traceconfig:
  139. def mywriter(tags, args):
  140. msg = " ".join(map(str, args))
  141. reporter.write_line("[traceconfig] " + msg)
  142. config.trace.root.setprocessor("pytest:config", mywriter)
  143. def getreportopt(config):
  144. reportopts = ""
  145. reportchars = config.option.reportchars
  146. if not config.option.disable_warnings and "w" not in reportchars:
  147. reportchars += "w"
  148. elif config.option.disable_warnings and "w" in reportchars:
  149. reportchars = reportchars.replace("w", "")
  150. aliases = {"F", "S"}
  151. for char in reportchars:
  152. # handle old aliases
  153. if char in aliases:
  154. char = char.lower()
  155. if char == "a":
  156. reportopts = "sxXwEf"
  157. elif char == "A":
  158. reportopts = "PpsxXwEf"
  159. break
  160. elif char not in reportopts:
  161. reportopts += char
  162. return reportopts
  163. @pytest.hookimpl(trylast=True) # after _pytest.runner
  164. def pytest_report_teststatus(report):
  165. letter = "F"
  166. if report.passed:
  167. letter = "."
  168. elif report.skipped:
  169. letter = "s"
  170. outcome = report.outcome
  171. if report.when in ("collect", "setup", "teardown") and outcome == "failed":
  172. outcome = "error"
  173. letter = "E"
  174. return outcome, letter, outcome.upper()
  175. @attr.s
  176. class WarningReport(object):
  177. """
  178. Simple structure to hold warnings information captured by ``pytest_warning_captured``.
  179. :ivar str message: user friendly message about the warning
  180. :ivar str|None nodeid: node id that generated the warning (see ``get_location``).
  181. :ivar tuple|py.path.local fslocation:
  182. file system location of the source of the warning (see ``get_location``).
  183. """
  184. message = attr.ib()
  185. nodeid = attr.ib(default=None)
  186. fslocation = attr.ib(default=None)
  187. count_towards_summary = True
  188. def get_location(self, config):
  189. """
  190. Returns the more user-friendly information about the location
  191. of a warning, or None.
  192. """
  193. if self.nodeid:
  194. return self.nodeid
  195. if self.fslocation:
  196. if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2:
  197. filename, linenum = self.fslocation[:2]
  198. relpath = py.path.local(filename).relto(config.invocation_dir)
  199. if not relpath:
  200. relpath = str(filename)
  201. return "%s:%s" % (relpath, linenum)
  202. else:
  203. return str(self.fslocation)
  204. return None
  205. class TerminalReporter(object):
  206. def __init__(self, config, file=None):
  207. import _pytest.config
  208. self.config = config
  209. self._numcollected = 0
  210. self._session = None
  211. self._showfspath = None
  212. self.stats = {}
  213. self.startdir = config.invocation_dir
  214. if file is None:
  215. file = sys.stdout
  216. self._tw = _pytest.config.create_terminal_writer(config, file)
  217. # self.writer will be deprecated in pytest-3.4
  218. self.writer = self._tw
  219. self._screen_width = self._tw.fullwidth
  220. self.currentfspath = None
  221. self.reportchars = getreportopt(config)
  222. self.hasmarkup = self._tw.hasmarkup
  223. self.isatty = file.isatty()
  224. self._progress_nodeids_reported = set()
  225. self._show_progress_info = self._determine_show_progress_info()
  226. self._collect_report_last_write = None
  227. def _determine_show_progress_info(self):
  228. """Return True if we should display progress information based on the current config"""
  229. # do not show progress if we are not capturing output (#3038)
  230. if self.config.getoption("capture", "no") == "no":
  231. return False
  232. # do not show progress if we are showing fixture setup/teardown
  233. if self.config.getoption("setupshow", False):
  234. return False
  235. cfg = self.config.getini("console_output_style")
  236. if cfg in ("progress", "count"):
  237. return cfg
  238. return False
  239. @property
  240. def verbosity(self):
  241. return self.config.option.verbose
  242. @property
  243. def showheader(self):
  244. return self.verbosity >= 0
  245. @property
  246. def showfspath(self):
  247. if self._showfspath is None:
  248. return self.verbosity >= 0
  249. return self._showfspath
  250. @showfspath.setter
  251. def showfspath(self, value):
  252. self._showfspath = value
  253. @property
  254. def showlongtestinfo(self):
  255. return self.verbosity > 0
  256. def hasopt(self, char):
  257. char = {"xfailed": "x", "skipped": "s"}.get(char, char)
  258. return char in self.reportchars
  259. def write_fspath_result(self, nodeid, res, **markup):
  260. fspath = self.config.rootdir.join(nodeid.split("::")[0])
  261. # NOTE: explicitly check for None to work around py bug, and for less
  262. # overhead in general (https://github.com/pytest-dev/py/pull/207).
  263. if self.currentfspath is None or fspath != self.currentfspath:
  264. if self.currentfspath is not None and self._show_progress_info:
  265. self._write_progress_information_filling_space()
  266. self.currentfspath = fspath
  267. fspath = self.startdir.bestrelpath(fspath)
  268. self._tw.line()
  269. self._tw.write(fspath + " ")
  270. self._tw.write(res, **markup)
  271. def write_ensure_prefix(self, prefix, extra="", **kwargs):
  272. if self.currentfspath != prefix:
  273. self._tw.line()
  274. self.currentfspath = prefix
  275. self._tw.write(prefix)
  276. if extra:
  277. self._tw.write(extra, **kwargs)
  278. self.currentfspath = -2
  279. def ensure_newline(self):
  280. if self.currentfspath:
  281. self._tw.line()
  282. self.currentfspath = None
  283. def write(self, content, **markup):
  284. self._tw.write(content, **markup)
  285. def write_line(self, line, **markup):
  286. if not isinstance(line, six.text_type):
  287. line = six.text_type(line, errors="replace")
  288. self.ensure_newline()
  289. self._tw.line(line, **markup)
  290. def rewrite(self, line, **markup):
  291. """
  292. Rewinds the terminal cursor to the beginning and writes the given line.
  293. :kwarg erase: if True, will also add spaces until the full terminal width to ensure
  294. previous lines are properly erased.
  295. The rest of the keyword arguments are markup instructions.
  296. """
  297. erase = markup.pop("erase", False)
  298. if erase:
  299. fill_count = self._tw.fullwidth - len(line) - 1
  300. fill = " " * fill_count
  301. else:
  302. fill = ""
  303. line = str(line)
  304. self._tw.write("\r" + line + fill, **markup)
  305. def write_sep(self, sep, title=None, **markup):
  306. self.ensure_newline()
  307. self._tw.sep(sep, title, **markup)
  308. def section(self, title, sep="=", **kw):
  309. self._tw.sep(sep, title, **kw)
  310. def line(self, msg, **kw):
  311. self._tw.line(msg, **kw)
  312. def pytest_internalerror(self, excrepr):
  313. for line in six.text_type(excrepr).split("\n"):
  314. self.write_line("INTERNALERROR> " + line)
  315. return 1
  316. def pytest_warning_captured(self, warning_message, item):
  317. # from _pytest.nodes import get_fslocation_from_item
  318. from _pytest.warnings import warning_record_to_str
  319. warnings = self.stats.setdefault("warnings", [])
  320. fslocation = warning_message.filename, warning_message.lineno
  321. message = warning_record_to_str(warning_message)
  322. nodeid = item.nodeid if item is not None else ""
  323. warning_report = WarningReport(
  324. fslocation=fslocation, message=message, nodeid=nodeid
  325. )
  326. warnings.append(warning_report)
  327. def pytest_plugin_registered(self, plugin):
  328. if self.config.option.traceconfig:
  329. msg = "PLUGIN registered: %s" % (plugin,)
  330. # XXX this event may happen during setup/teardown time
  331. # which unfortunately captures our output here
  332. # which garbles our output if we use self.write_line
  333. self.write_line(msg)
  334. def pytest_deselected(self, items):
  335. self.stats.setdefault("deselected", []).extend(items)
  336. def pytest_runtest_logstart(self, nodeid, location):
  337. # ensure that the path is printed before the
  338. # 1st test of a module starts running
  339. if self.showlongtestinfo:
  340. line = self._locationline(nodeid, *location)
  341. self.write_ensure_prefix(line, "")
  342. elif self.showfspath:
  343. fsid = nodeid.split("::")[0]
  344. self.write_fspath_result(fsid, "")
  345. def pytest_runtest_logreport(self, report):
  346. self._tests_ran = True
  347. rep = report
  348. res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
  349. category, letter, word = res
  350. if isinstance(word, tuple):
  351. word, markup = word
  352. else:
  353. markup = None
  354. self.stats.setdefault(category, []).append(rep)
  355. if not letter and not word:
  356. # probably passed setup/teardown
  357. return
  358. running_xdist = hasattr(rep, "node")
  359. if markup is None:
  360. was_xfail = hasattr(report, "wasxfail")
  361. if rep.passed and not was_xfail:
  362. markup = {"green": True}
  363. elif rep.passed and was_xfail:
  364. markup = {"yellow": True}
  365. elif rep.failed:
  366. markup = {"red": True}
  367. elif rep.skipped:
  368. markup = {"yellow": True}
  369. else:
  370. markup = {}
  371. if self.verbosity <= 0:
  372. if not running_xdist and self.showfspath:
  373. self.write_fspath_result(rep.nodeid, letter, **markup)
  374. else:
  375. self._tw.write(letter, **markup)
  376. else:
  377. self._progress_nodeids_reported.add(rep.nodeid)
  378. line = self._locationline(rep.nodeid, *rep.location)
  379. if not running_xdist:
  380. self.write_ensure_prefix(line, word, **markup)
  381. if self._show_progress_info:
  382. self._write_progress_information_filling_space()
  383. else:
  384. self.ensure_newline()
  385. self._tw.write("[%s]" % rep.node.gateway.id)
  386. if self._show_progress_info:
  387. self._tw.write(
  388. self._get_progress_information_message() + " ", cyan=True
  389. )
  390. else:
  391. self._tw.write(" ")
  392. self._tw.write(word, **markup)
  393. self._tw.write(" " + line)
  394. self.currentfspath = -2
  395. def pytest_runtest_logfinish(self, nodeid):
  396. if self.verbosity <= 0 and self._show_progress_info:
  397. if self._show_progress_info == "count":
  398. num_tests = self._session.testscollected
  399. progress_length = len(" [{}/{}]".format(str(num_tests), str(num_tests)))
  400. else:
  401. progress_length = len(" [100%]")
  402. self._progress_nodeids_reported.add(nodeid)
  403. is_last_item = (
  404. len(self._progress_nodeids_reported) == self._session.testscollected
  405. )
  406. if is_last_item:
  407. self._write_progress_information_filling_space()
  408. else:
  409. w = self._width_of_current_line
  410. past_edge = w + progress_length + 1 >= self._screen_width
  411. if past_edge:
  412. msg = self._get_progress_information_message()
  413. self._tw.write(msg + "\n", cyan=True)
  414. def _get_progress_information_message(self):
  415. collected = self._session.testscollected
  416. if self._show_progress_info == "count":
  417. if collected:
  418. progress = self._progress_nodeids_reported
  419. counter_format = "{{:{}d}}".format(len(str(collected)))
  420. format_string = " [{}/{{}}]".format(counter_format)
  421. return format_string.format(len(progress), collected)
  422. return " [ {} / {} ]".format(collected, collected)
  423. else:
  424. if collected:
  425. progress = len(self._progress_nodeids_reported) * 100 // collected
  426. return " [{:3d}%]".format(progress)
  427. return " [100%]"
  428. def _write_progress_information_filling_space(self):
  429. msg = self._get_progress_information_message()
  430. w = self._width_of_current_line
  431. fill = self._tw.fullwidth - w - 1
  432. self.write(msg.rjust(fill), cyan=True)
  433. @property
  434. def _width_of_current_line(self):
  435. """Return the width of current line, using the superior implementation of py-1.6 when available"""
  436. try:
  437. return self._tw.width_of_current_line
  438. except AttributeError:
  439. # py < 1.6.0
  440. return self._tw.chars_on_current_line
  441. def pytest_collection(self):
  442. if self.isatty:
  443. if self.config.option.verbose >= 0:
  444. self.write("collecting ... ", bold=True)
  445. self._collect_report_last_write = time.time()
  446. elif self.config.option.verbose >= 1:
  447. self.write("collecting ... ", bold=True)
  448. def pytest_collectreport(self, report):
  449. if report.failed:
  450. self.stats.setdefault("error", []).append(report)
  451. elif report.skipped:
  452. self.stats.setdefault("skipped", []).append(report)
  453. items = [x for x in report.result if isinstance(x, pytest.Item)]
  454. self._numcollected += len(items)
  455. if self.isatty:
  456. self.report_collect()
  457. def report_collect(self, final=False):
  458. if self.config.option.verbose < 0:
  459. return
  460. if not final:
  461. # Only write "collecting" report every 0.5s.
  462. t = time.time()
  463. if (
  464. self._collect_report_last_write is not None
  465. and self._collect_report_last_write > t - REPORT_COLLECTING_RESOLUTION
  466. ):
  467. return
  468. self._collect_report_last_write = t
  469. errors = len(self.stats.get("error", []))
  470. skipped = len(self.stats.get("skipped", []))
  471. deselected = len(self.stats.get("deselected", []))
  472. selected = self._numcollected - errors - skipped - deselected
  473. if final:
  474. line = "collected "
  475. else:
  476. line = "collecting "
  477. line += (
  478. str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
  479. )
  480. if errors:
  481. line += " / %d errors" % errors
  482. if deselected:
  483. line += " / %d deselected" % deselected
  484. if skipped:
  485. line += " / %d skipped" % skipped
  486. if self._numcollected > selected > 0:
  487. line += " / %d selected" % selected
  488. if self.isatty:
  489. self.rewrite(line, bold=True, erase=True)
  490. if final:
  491. self.write("\n")
  492. else:
  493. self.write_line(line)
  494. @pytest.hookimpl(trylast=True)
  495. def pytest_sessionstart(self, session):
  496. self._session = session
  497. self._sessionstarttime = time.time()
  498. if not self.showheader:
  499. return
  500. self.write_sep("=", "test session starts", bold=True)
  501. verinfo = platform.python_version()
  502. msg = "platform %s -- Python %s" % (sys.platform, verinfo)
  503. if hasattr(sys, "pypy_version_info"):
  504. verinfo = ".".join(map(str, sys.pypy_version_info[:3]))
  505. msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3])
  506. msg += ", pytest-%s, py-%s, pluggy-%s" % (
  507. pytest.__version__,
  508. py.__version__,
  509. pluggy.__version__,
  510. )
  511. if (
  512. self.verbosity > 0
  513. or self.config.option.debug
  514. or getattr(self.config.option, "pastebin", None)
  515. ):
  516. msg += " -- " + str(sys.executable)
  517. self.write_line(msg)
  518. lines = self.config.hook.pytest_report_header(
  519. config=self.config, startdir=self.startdir
  520. )
  521. self._write_report_lines_from_hooks(lines)
  522. def _write_report_lines_from_hooks(self, lines):
  523. lines.reverse()
  524. for line in collapse(lines):
  525. self.write_line(line)
  526. def pytest_report_header(self, config):
  527. line = "rootdir: %s" % config.rootdir
  528. if config.inifile:
  529. line += ", inifile: " + config.rootdir.bestrelpath(config.inifile)
  530. testpaths = config.getini("testpaths")
  531. if testpaths and config.args == testpaths:
  532. rel_paths = [config.rootdir.bestrelpath(x) for x in testpaths]
  533. line += ", testpaths: {}".format(", ".join(rel_paths))
  534. result = [line]
  535. plugininfo = config.pluginmanager.list_plugin_distinfo()
  536. if plugininfo:
  537. result.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo)))
  538. return result
  539. def pytest_collection_finish(self, session):
  540. self.report_collect(True)
  541. if self.config.getoption("collectonly"):
  542. self._printcollecteditems(session.items)
  543. lines = self.config.hook.pytest_report_collectionfinish(
  544. config=self.config, startdir=self.startdir, items=session.items
  545. )
  546. self._write_report_lines_from_hooks(lines)
  547. if self.config.getoption("collectonly"):
  548. if self.stats.get("failed"):
  549. self._tw.sep("!", "collection failures")
  550. for rep in self.stats.get("failed"):
  551. rep.toterminal(self._tw)
  552. def _printcollecteditems(self, items):
  553. # to print out items and their parent collectors
  554. # we take care to leave out Instances aka ()
  555. # because later versions are going to get rid of them anyway
  556. if self.config.option.verbose < 0:
  557. if self.config.option.verbose < -1:
  558. counts = {}
  559. for item in items:
  560. name = item.nodeid.split("::", 1)[0]
  561. counts[name] = counts.get(name, 0) + 1
  562. for name, count in sorted(counts.items()):
  563. self._tw.line("%s: %d" % (name, count))
  564. else:
  565. for item in items:
  566. self._tw.line(item.nodeid)
  567. return
  568. stack = []
  569. indent = ""
  570. for item in items:
  571. needed_collectors = item.listchain()[1:] # strip root node
  572. while stack:
  573. if stack == needed_collectors[: len(stack)]:
  574. break
  575. stack.pop()
  576. for col in needed_collectors[len(stack) :]:
  577. stack.append(col)
  578. if col.name == "()": # Skip Instances.
  579. continue
  580. indent = (len(stack) - 1) * " "
  581. self._tw.line("%s%s" % (indent, col))
  582. if self.config.option.verbose >= 1:
  583. if hasattr(col, "_obj") and col._obj.__doc__:
  584. for line in col._obj.__doc__.strip().splitlines():
  585. self._tw.line("%s%s" % (indent + " ", line.strip()))
  586. @pytest.hookimpl(hookwrapper=True)
  587. def pytest_sessionfinish(self, exitstatus):
  588. outcome = yield
  589. outcome.get_result()
  590. self._tw.line("")
  591. summary_exit_codes = (
  592. EXIT_OK,
  593. EXIT_TESTSFAILED,
  594. EXIT_INTERRUPTED,
  595. EXIT_USAGEERROR,
  596. EXIT_NOTESTSCOLLECTED,
  597. )
  598. if exitstatus in summary_exit_codes:
  599. self.config.hook.pytest_terminal_summary(
  600. terminalreporter=self, exitstatus=exitstatus, config=self.config
  601. )
  602. if exitstatus == EXIT_INTERRUPTED:
  603. self._report_keyboardinterrupt()
  604. del self._keyboardinterrupt_memo
  605. self.summary_stats()
  606. @pytest.hookimpl(hookwrapper=True)
  607. def pytest_terminal_summary(self):
  608. self.summary_errors()
  609. self.summary_failures()
  610. self.summary_warnings()
  611. self.summary_passes()
  612. yield
  613. self.short_test_summary()
  614. # Display any extra warnings from teardown here (if any).
  615. self.summary_warnings()
  616. def pytest_keyboard_interrupt(self, excinfo):
  617. self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
  618. def pytest_unconfigure(self):
  619. if hasattr(self, "_keyboardinterrupt_memo"):
  620. self._report_keyboardinterrupt()
  621. def _report_keyboardinterrupt(self):
  622. excrepr = self._keyboardinterrupt_memo
  623. msg = excrepr.reprcrash.message
  624. self.write_sep("!", msg)
  625. if "KeyboardInterrupt" in msg:
  626. if self.config.option.fulltrace:
  627. excrepr.toterminal(self._tw)
  628. else:
  629. excrepr.reprcrash.toterminal(self._tw)
  630. self._tw.line(
  631. "(to show a full traceback on KeyboardInterrupt use --fulltrace)",
  632. yellow=True,
  633. )
  634. def _locationline(self, nodeid, fspath, lineno, domain):
  635. def mkrel(nodeid):
  636. line = self.config.cwd_relative_nodeid(nodeid)
  637. if domain and line.endswith(domain):
  638. line = line[: -len(domain)]
  639. values = domain.split("[")
  640. values[0] = values[0].replace(".", "::") # don't replace '.' in params
  641. line += "[".join(values)
  642. return line
  643. # collect_fspath comes from testid which has a "/"-normalized path
  644. if fspath:
  645. res = mkrel(nodeid)
  646. if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
  647. "\\", nodes.SEP
  648. ):
  649. res += " <- " + self.startdir.bestrelpath(fspath)
  650. else:
  651. res = "[location]"
  652. return res + " "
  653. def _getfailureheadline(self, rep):
  654. head_line = rep.head_line
  655. if head_line:
  656. return head_line
  657. return "test session" # XXX?
  658. def _getcrashline(self, rep):
  659. try:
  660. return str(rep.longrepr.reprcrash)
  661. except AttributeError:
  662. try:
  663. return str(rep.longrepr)[:50]
  664. except AttributeError:
  665. return ""
  666. #
  667. # summaries for sessionfinish
  668. #
  669. def getreports(self, name):
  670. values = []
  671. for x in self.stats.get(name, []):
  672. if not hasattr(x, "_pdbshown"):
  673. values.append(x)
  674. return values
  675. def summary_warnings(self):
  676. if self.hasopt("w"):
  677. all_warnings = self.stats.get("warnings")
  678. if not all_warnings:
  679. return
  680. final = hasattr(self, "_already_displayed_warnings")
  681. if final:
  682. warning_reports = all_warnings[self._already_displayed_warnings :]
  683. else:
  684. warning_reports = all_warnings
  685. self._already_displayed_warnings = len(warning_reports)
  686. if not warning_reports:
  687. return
  688. reports_grouped_by_message = collections.OrderedDict()
  689. for wr in warning_reports:
  690. reports_grouped_by_message.setdefault(wr.message, []).append(wr)
  691. title = "warnings summary (final)" if final else "warnings summary"
  692. self.write_sep("=", title, yellow=True, bold=False)
  693. for message, warning_reports in reports_grouped_by_message.items():
  694. has_any_location = False
  695. for w in warning_reports:
  696. location = w.get_location(self.config)
  697. if location:
  698. self._tw.line(str(location))
  699. has_any_location = True
  700. if has_any_location:
  701. lines = message.splitlines()
  702. indented = "\n".join(" " + x for x in lines)
  703. message = indented.rstrip()
  704. else:
  705. message = message.rstrip()
  706. self._tw.line(message)
  707. self._tw.line()
  708. self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html")
  709. def summary_passes(self):
  710. if self.config.option.tbstyle != "no":
  711. if self.hasopt("P"):
  712. reports = self.getreports("passed")
  713. if not reports:
  714. return
  715. self.write_sep("=", "PASSES")
  716. for rep in reports:
  717. if rep.sections:
  718. msg = self._getfailureheadline(rep)
  719. self.write_sep("_", msg, green=True, bold=True)
  720. self._outrep_summary(rep)
  721. def print_teardown_sections(self, rep):
  722. showcapture = self.config.option.showcapture
  723. if showcapture == "no":
  724. return
  725. for secname, content in rep.sections:
  726. if showcapture != "all" and showcapture not in secname:
  727. continue
  728. if "teardown" in secname:
  729. self._tw.sep("-", secname)
  730. if content[-1:] == "\n":
  731. content = content[:-1]
  732. self._tw.line(content)
  733. def summary_failures(self):
  734. if self.config.option.tbstyle != "no":
  735. reports = self.getreports("failed")
  736. if not reports:
  737. return
  738. self.write_sep("=", "FAILURES")
  739. if self.config.option.tbstyle == "line":
  740. for rep in reports:
  741. line = self._getcrashline(rep)
  742. self.write_line(line)
  743. else:
  744. teardown_sections = {}
  745. for report in self.getreports(""):
  746. if report.when == "teardown":
  747. teardown_sections.setdefault(report.nodeid, []).append(report)
  748. for rep in reports:
  749. msg = self._getfailureheadline(rep)
  750. self.write_sep("_", msg, red=True, bold=True)
  751. self._outrep_summary(rep)
  752. for report in teardown_sections.get(rep.nodeid, []):
  753. self.print_teardown_sections(report)
  754. def summary_errors(self):
  755. if self.config.option.tbstyle != "no":
  756. reports = self.getreports("error")
  757. if not reports:
  758. return
  759. self.write_sep("=", "ERRORS")
  760. for rep in self.stats["error"]:
  761. msg = self._getfailureheadline(rep)
  762. if rep.when == "collect":
  763. msg = "ERROR collecting " + msg
  764. else:
  765. msg = "ERROR at %s of %s" % (rep.when, msg)
  766. self.write_sep("_", msg, red=True, bold=True)
  767. self._outrep_summary(rep)
  768. def _outrep_summary(self, rep):
  769. rep.toterminal(self._tw)
  770. showcapture = self.config.option.showcapture
  771. if showcapture == "no":
  772. return
  773. for secname, content in rep.sections:
  774. if showcapture != "all" and showcapture not in secname:
  775. continue
  776. self._tw.sep("-", secname)
  777. if content[-1:] == "\n":
  778. content = content[:-1]
  779. self._tw.line(content)
  780. def summary_stats(self):
  781. session_duration = time.time() - self._sessionstarttime
  782. (line, color) = build_summary_stats_line(self.stats)
  783. msg = "%s in %.2f seconds" % (line, session_duration)
  784. markup = {color: True, "bold": True}
  785. if self.verbosity >= 0:
  786. self.write_sep("=", msg, **markup)
  787. if self.verbosity == -1:
  788. self.write_line(msg, **markup)
  789. def short_test_summary(self):
  790. if not self.reportchars:
  791. return
  792. def show_simple(stat, lines):
  793. failed = self.stats.get(stat, [])
  794. if not failed:
  795. return
  796. termwidth = self.writer.fullwidth
  797. config = self.config
  798. for rep in failed:
  799. line = _get_line_with_reprcrash_message(config, rep, termwidth)
  800. lines.append(line)
  801. def show_xfailed(lines):
  802. xfailed = self.stats.get("xfailed", [])
  803. for rep in xfailed:
  804. verbose_word = rep._get_verbose_word(self.config)
  805. pos = _get_pos(self.config, rep)
  806. lines.append("%s %s" % (verbose_word, pos))
  807. reason = rep.wasxfail
  808. if reason:
  809. lines.append(" " + str(reason))
  810. def show_xpassed(lines):
  811. xpassed = self.stats.get("xpassed", [])
  812. for rep in xpassed:
  813. verbose_word = rep._get_verbose_word(self.config)
  814. pos = _get_pos(self.config, rep)
  815. reason = rep.wasxfail
  816. lines.append("%s %s %s" % (verbose_word, pos, reason))
  817. def show_skipped(lines):
  818. skipped = self.stats.get("skipped", [])
  819. fskips = _folded_skips(skipped) if skipped else []
  820. if not fskips:
  821. return
  822. verbose_word = skipped[0]._get_verbose_word(self.config)
  823. for num, fspath, lineno, reason in fskips:
  824. if reason.startswith("Skipped: "):
  825. reason = reason[9:]
  826. if lineno is not None:
  827. lines.append(
  828. "%s [%d] %s:%d: %s"
  829. % (verbose_word, num, fspath, lineno + 1, reason)
  830. )
  831. else:
  832. lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason))
  833. REPORTCHAR_ACTIONS = {
  834. "x": show_xfailed,
  835. "X": show_xpassed,
  836. "f": partial(show_simple, "failed"),
  837. "s": show_skipped,
  838. "p": partial(show_simple, "passed"),
  839. "E": partial(show_simple, "error"),
  840. }
  841. lines = []
  842. for char in self.reportchars:
  843. action = REPORTCHAR_ACTIONS.get(char)
  844. if action: # skipping e.g. "P" (passed with output) here.
  845. action(lines)
  846. if lines:
  847. self.write_sep("=", "short test summary info")
  848. for line in lines:
  849. self.write_line(line)
  850. def _get_pos(config, rep):
  851. nodeid = config.cwd_relative_nodeid(rep.nodeid)
  852. return nodeid
  853. def _get_line_with_reprcrash_message(config, rep, termwidth):
  854. """Get summary line for a report, trying to add reprcrash message."""
  855. from wcwidth import wcswidth
  856. verbose_word = rep._get_verbose_word(config)
  857. pos = _get_pos(config, rep)
  858. line = "%s %s" % (verbose_word, pos)
  859. len_line = wcswidth(line)
  860. ellipsis, len_ellipsis = "...", 3
  861. if len_line > termwidth - len_ellipsis:
  862. # No space for an additional message.
  863. return line
  864. try:
  865. msg = rep.longrepr.reprcrash.message
  866. except AttributeError:
  867. pass
  868. else:
  869. # Only use the first line.
  870. i = msg.find("\n")
  871. if i != -1:
  872. msg = msg[:i]
  873. len_msg = wcswidth(msg)
  874. sep, len_sep = " - ", 3
  875. max_len_msg = termwidth - len_line - len_sep
  876. if max_len_msg >= len_ellipsis:
  877. if len_msg > max_len_msg:
  878. max_len_msg -= len_ellipsis
  879. msg = msg[:max_len_msg]
  880. while wcswidth(msg) > max_len_msg:
  881. msg = msg[:-1]
  882. if six.PY2:
  883. # on python 2 systems with narrow unicode compilation, trying to
  884. # get a single character out of a multi-byte unicode character such as
  885. # u'😄' will result in a High Surrogate (U+D83D) character, which is
  886. # rendered as u'�'; in this case we just strip that character out as it
  887. # serves no purpose being rendered
  888. try:
  889. surrogate = six.unichr(0xD83D)
  890. msg = msg.rstrip(surrogate)
  891. except ValueError: # pragma: no cover
  892. # Jython cannot represent this lone surrogate at all (#5256):
  893. # ValueError: unichr() arg is a lone surrogate in range
  894. # (0xD800, 0xDFFF) (Jython UTF-16 encoding)
  895. # ignore this case as it shouldn't appear in the string anyway
  896. pass
  897. msg += ellipsis
  898. line += sep + msg
  899. return line
  900. def _folded_skips(skipped):
  901. d = {}
  902. for event in skipped:
  903. key = event.longrepr
  904. assert len(key) == 3, (event, key)
  905. keywords = getattr(event, "keywords", {})
  906. # folding reports with global pytestmark variable
  907. # this is workaround, because for now we cannot identify the scope of a skip marker
  908. # TODO: revisit after marks scope would be fixed
  909. if (
  910. event.when == "setup"
  911. and "skip" in keywords
  912. and "pytestmark" not in keywords
  913. ):
  914. key = (key[0], None, key[2])
  915. d.setdefault(key, []).append(event)
  916. values = []
  917. for key, events in d.items():
  918. values.append((len(events),) + key)
  919. return values
  920. def build_summary_stats_line(stats):
  921. known_types = (
  922. "failed passed skipped deselected xfailed xpassed warnings error".split()
  923. )
  924. unknown_type_seen = False
  925. for found_type in stats:
  926. if found_type not in known_types:
  927. if found_type: # setup/teardown reports have an empty key, ignore them
  928. known_types.append(found_type)
  929. unknown_type_seen = True
  930. parts = []
  931. for key in known_types:
  932. reports = stats.get(key, None)
  933. if reports:
  934. count = sum(
  935. 1 for rep in reports if getattr(rep, "count_towards_summary", True)
  936. )
  937. parts.append("%d %s" % (count, key))
  938. if parts:
  939. line = ", ".join(parts)
  940. else:
  941. line = "no tests ran"
  942. if "failed" in stats or "error" in stats:
  943. color = "red"
  944. elif "warnings" in stats or unknown_type_seen:
  945. color = "yellow"
  946. elif "passed" in stats:
  947. color = "green"
  948. else:
  949. color = "yellow"
  950. return line, color
  951. def _plugin_nameversions(plugininfo):
  952. values = []
  953. for plugin, dist in plugininfo:
  954. # gets us name and version!
  955. name = "{dist.project_name}-{dist.version}".format(dist=dist)
  956. # questionable convenience, but it keeps things short
  957. if name.startswith("pytest-"):
  958. name = name[7:]
  959. # we decided to print python package names
  960. # they can have more than one plugin
  961. if name not in values:
  962. values.append(name)
  963. return values