terminal.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. """ terminal reporting of the full testing process.
  2. This is a good source for looking at the various reporting hooks.
  3. """
  4. from __future__ import absolute_import, division, print_function
  5. import itertools
  6. import platform
  7. import sys
  8. import time
  9. import pluggy
  10. import py
  11. import six
  12. from more_itertools import collapse
  13. import pytest
  14. from _pytest import nodes
  15. from _pytest.main import (
  16. EXIT_OK,
  17. EXIT_TESTSFAILED,
  18. EXIT_INTERRUPTED,
  19. EXIT_USAGEERROR,
  20. EXIT_NOTESTSCOLLECTED,
  21. )
  22. import argparse
  23. class MoreQuietAction(argparse.Action):
  24. """
  25. a modified copy of the argparse count action which counts down and updates
  26. the legacy quiet attribute at the same time
  27. used to unify verbosity handling
  28. """
  29. def __init__(self, option_strings, dest, default=None, required=False, help=None):
  30. super(MoreQuietAction, self).__init__(
  31. option_strings=option_strings,
  32. dest=dest,
  33. nargs=0,
  34. default=default,
  35. required=required,
  36. help=help,
  37. )
  38. def __call__(self, parser, namespace, values, option_string=None):
  39. new_count = getattr(namespace, self.dest, 0) - 1
  40. setattr(namespace, self.dest, new_count)
  41. # todo Deprecate config.quiet
  42. namespace.quiet = getattr(namespace, "quiet", 0) + 1
  43. def pytest_addoption(parser):
  44. group = parser.getgroup("terminal reporting", "reporting", after="general")
  45. group._addoption(
  46. "-v",
  47. "--verbose",
  48. action="count",
  49. default=0,
  50. dest="verbose",
  51. help="increase verbosity.",
  52. ),
  53. group._addoption(
  54. "-q",
  55. "--quiet",
  56. action=MoreQuietAction,
  57. default=0,
  58. dest="verbose",
  59. help="decrease verbosity.",
  60. ),
  61. group._addoption(
  62. "--verbosity", dest="verbose", type=int, default=0, help="set verbosity"
  63. )
  64. group._addoption(
  65. "-r",
  66. action="store",
  67. dest="reportchars",
  68. default="",
  69. metavar="chars",
  70. help="show extra test summary info as specified by chars (f)ailed, "
  71. "(E)error, (s)skipped, (x)failed, (X)passed, "
  72. "(p)passed, (P)passed with output, (a)all except pP. "
  73. "Warnings are displayed at all times except when "
  74. "--disable-warnings is set",
  75. )
  76. group._addoption(
  77. "--disable-warnings",
  78. "--disable-pytest-warnings",
  79. default=False,
  80. dest="disable_warnings",
  81. action="store_true",
  82. help="disable warnings summary",
  83. )
  84. group._addoption(
  85. "-l",
  86. "--showlocals",
  87. action="store_true",
  88. dest="showlocals",
  89. default=False,
  90. help="show locals in tracebacks (disabled by default).",
  91. )
  92. group._addoption(
  93. "--tb",
  94. metavar="style",
  95. action="store",
  96. dest="tbstyle",
  97. default="auto",
  98. choices=["auto", "long", "short", "no", "line", "native"],
  99. help="traceback print mode (auto/long/short/line/native/no).",
  100. )
  101. group._addoption(
  102. "--show-capture",
  103. action="store",
  104. dest="showcapture",
  105. choices=["no", "stdout", "stderr", "log", "all"],
  106. default="all",
  107. help="Controls how captured stdout/stderr/log is shown on failed tests. "
  108. "Default is 'all'.",
  109. )
  110. group._addoption(
  111. "--fulltrace",
  112. "--full-trace",
  113. action="store_true",
  114. default=False,
  115. help="don't cut any tracebacks (default is to cut).",
  116. )
  117. group._addoption(
  118. "--color",
  119. metavar="color",
  120. action="store",
  121. dest="color",
  122. default="auto",
  123. choices=["yes", "no", "auto"],
  124. help="color terminal output (yes/no/auto).",
  125. )
  126. parser.addini(
  127. "console_output_style",
  128. help="console output: classic or with additional progress information (classic|progress).",
  129. default="progress",
  130. )
  131. def pytest_configure(config):
  132. reporter = TerminalReporter(config, sys.stdout)
  133. config.pluginmanager.register(reporter, "terminalreporter")
  134. if config.option.debug or config.option.traceconfig:
  135. def mywriter(tags, args):
  136. msg = " ".join(map(str, args))
  137. reporter.write_line("[traceconfig] " + msg)
  138. config.trace.root.setprocessor("pytest:config", mywriter)
  139. def getreportopt(config):
  140. reportopts = ""
  141. reportchars = config.option.reportchars
  142. if not config.option.disable_warnings and "w" not in reportchars:
  143. reportchars += "w"
  144. elif config.option.disable_warnings and "w" in reportchars:
  145. reportchars = reportchars.replace("w", "")
  146. if reportchars:
  147. for char in reportchars:
  148. if char not in reportopts and char != "a":
  149. reportopts += char
  150. elif char == "a":
  151. reportopts = "fEsxXw"
  152. return reportopts
  153. def pytest_report_teststatus(report):
  154. if report.passed:
  155. letter = "."
  156. elif report.skipped:
  157. letter = "s"
  158. elif report.failed:
  159. letter = "F"
  160. if report.when != "call":
  161. letter = "f"
  162. return report.outcome, letter, report.outcome.upper()
  163. class WarningReport(object):
  164. """
  165. Simple structure to hold warnings information captured by ``pytest_logwarning``.
  166. """
  167. def __init__(self, code, message, nodeid=None, fslocation=None):
  168. """
  169. :param code: unused
  170. :param str message: user friendly message about the warning
  171. :param str|None nodeid: node id that generated the warning (see ``get_location``).
  172. :param tuple|py.path.local fslocation:
  173. file system location of the source of the warning (see ``get_location``).
  174. """
  175. self.code = code
  176. self.message = message
  177. self.nodeid = nodeid
  178. self.fslocation = fslocation
  179. def get_location(self, config):
  180. """
  181. Returns the more user-friendly information about the location
  182. of a warning, or None.
  183. """
  184. if self.nodeid:
  185. return self.nodeid
  186. if self.fslocation:
  187. if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2:
  188. filename, linenum = self.fslocation[:2]
  189. relpath = py.path.local(filename).relto(config.invocation_dir)
  190. return "%s:%s" % (relpath, linenum)
  191. else:
  192. return str(self.fslocation)
  193. return None
  194. class TerminalReporter(object):
  195. def __init__(self, config, file=None):
  196. import _pytest.config
  197. self.config = config
  198. self.verbosity = self.config.option.verbose
  199. self.showheader = self.verbosity >= 0
  200. self.showfspath = self.verbosity >= 0
  201. self.showlongtestinfo = self.verbosity > 0
  202. self._numcollected = 0
  203. self._session = None
  204. self.stats = {}
  205. self.startdir = py.path.local()
  206. if file is None:
  207. file = sys.stdout
  208. self._tw = _pytest.config.create_terminal_writer(config, file)
  209. # self.writer will be deprecated in pytest-3.4
  210. self.writer = self._tw
  211. self._screen_width = self._tw.fullwidth
  212. self.currentfspath = None
  213. self.reportchars = getreportopt(config)
  214. self.hasmarkup = self._tw.hasmarkup
  215. self.isatty = file.isatty()
  216. self._progress_nodeids_reported = set()
  217. self._show_progress_info = self._determine_show_progress_info()
  218. def _determine_show_progress_info(self):
  219. """Return True if we should display progress information based on the current config"""
  220. # do not show progress if we are not capturing output (#3038)
  221. if self.config.getoption("capture") == "no":
  222. return False
  223. # do not show progress if we are showing fixture setup/teardown
  224. if self.config.getoption("setupshow"):
  225. return False
  226. return self.config.getini("console_output_style") == "progress"
  227. def hasopt(self, char):
  228. char = {"xfailed": "x", "skipped": "s"}.get(char, char)
  229. return char in self.reportchars
  230. def write_fspath_result(self, nodeid, res):
  231. fspath = self.config.rootdir.join(nodeid.split("::")[0])
  232. if fspath != self.currentfspath:
  233. if self.currentfspath is not None and self._show_progress_info:
  234. self._write_progress_information_filling_space()
  235. self.currentfspath = fspath
  236. fspath = self.startdir.bestrelpath(fspath)
  237. self._tw.line()
  238. self._tw.write(fspath + " ")
  239. self._tw.write(res)
  240. def write_ensure_prefix(self, prefix, extra="", **kwargs):
  241. if self.currentfspath != prefix:
  242. self._tw.line()
  243. self.currentfspath = prefix
  244. self._tw.write(prefix)
  245. if extra:
  246. self._tw.write(extra, **kwargs)
  247. self.currentfspath = -2
  248. def ensure_newline(self):
  249. if self.currentfspath:
  250. self._tw.line()
  251. self.currentfspath = None
  252. def write(self, content, **markup):
  253. self._tw.write(content, **markup)
  254. def write_line(self, line, **markup):
  255. if not isinstance(line, six.text_type):
  256. line = six.text_type(line, errors="replace")
  257. self.ensure_newline()
  258. self._tw.line(line, **markup)
  259. def rewrite(self, line, **markup):
  260. """
  261. Rewinds the terminal cursor to the beginning and writes the given line.
  262. :kwarg erase: if True, will also add spaces until the full terminal width to ensure
  263. previous lines are properly erased.
  264. The rest of the keyword arguments are markup instructions.
  265. """
  266. erase = markup.pop("erase", False)
  267. if erase:
  268. fill_count = self._tw.fullwidth - len(line) - 1
  269. fill = " " * fill_count
  270. else:
  271. fill = ""
  272. line = str(line)
  273. self._tw.write("\r" + line + fill, **markup)
  274. def write_sep(self, sep, title=None, **markup):
  275. self.ensure_newline()
  276. self._tw.sep(sep, title, **markup)
  277. def section(self, title, sep="=", **kw):
  278. self._tw.sep(sep, title, **kw)
  279. def line(self, msg, **kw):
  280. self._tw.line(msg, **kw)
  281. def pytest_internalerror(self, excrepr):
  282. for line in six.text_type(excrepr).split("\n"):
  283. self.write_line("INTERNALERROR> " + line)
  284. return 1
  285. def pytest_logwarning(self, code, fslocation, message, nodeid):
  286. warnings = self.stats.setdefault("warnings", [])
  287. warning = WarningReport(
  288. code=code, fslocation=fslocation, message=message, nodeid=nodeid
  289. )
  290. warnings.append(warning)
  291. def pytest_plugin_registered(self, plugin):
  292. if self.config.option.traceconfig:
  293. msg = "PLUGIN registered: %s" % (plugin,)
  294. # XXX this event may happen during setup/teardown time
  295. # which unfortunately captures our output here
  296. # which garbles our output if we use self.write_line
  297. self.write_line(msg)
  298. def pytest_deselected(self, items):
  299. self.stats.setdefault("deselected", []).extend(items)
  300. def pytest_runtest_logstart(self, nodeid, location):
  301. # ensure that the path is printed before the
  302. # 1st test of a module starts running
  303. if self.showlongtestinfo:
  304. line = self._locationline(nodeid, *location)
  305. self.write_ensure_prefix(line, "")
  306. elif self.showfspath:
  307. fsid = nodeid.split("::")[0]
  308. self.write_fspath_result(fsid, "")
  309. def pytest_runtest_logreport(self, report):
  310. rep = report
  311. res = self.config.hook.pytest_report_teststatus(report=rep)
  312. category, letter, word = res
  313. if isinstance(word, tuple):
  314. word, markup = word
  315. else:
  316. markup = None
  317. self.stats.setdefault(category, []).append(rep)
  318. self._tests_ran = True
  319. if not letter and not word:
  320. # probably passed setup/teardown
  321. return
  322. running_xdist = hasattr(rep, "node")
  323. if self.verbosity <= 0:
  324. if not running_xdist and self.showfspath:
  325. self.write_fspath_result(rep.nodeid, letter)
  326. else:
  327. self._tw.write(letter)
  328. else:
  329. self._progress_nodeids_reported.add(rep.nodeid)
  330. if markup is None:
  331. if rep.passed:
  332. markup = {"green": True}
  333. elif rep.failed:
  334. markup = {"red": True}
  335. elif rep.skipped:
  336. markup = {"yellow": True}
  337. else:
  338. markup = {}
  339. line = self._locationline(rep.nodeid, *rep.location)
  340. if not running_xdist:
  341. self.write_ensure_prefix(line, word, **markup)
  342. if self._show_progress_info:
  343. self._write_progress_information_filling_space()
  344. else:
  345. self.ensure_newline()
  346. self._tw.write("[%s]" % rep.node.gateway.id)
  347. if self._show_progress_info:
  348. self._tw.write(
  349. self._get_progress_information_message() + " ", cyan=True
  350. )
  351. else:
  352. self._tw.write(" ")
  353. self._tw.write(word, **markup)
  354. self._tw.write(" " + line)
  355. self.currentfspath = -2
  356. def pytest_runtest_logfinish(self, nodeid):
  357. if self.verbosity <= 0 and self._show_progress_info:
  358. self._progress_nodeids_reported.add(nodeid)
  359. last_item = (
  360. len(self._progress_nodeids_reported) == self._session.testscollected
  361. )
  362. if last_item:
  363. self._write_progress_information_filling_space()
  364. else:
  365. past_edge = (
  366. self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1
  367. >= self._screen_width
  368. )
  369. if past_edge:
  370. msg = self._get_progress_information_message()
  371. self._tw.write(msg + "\n", cyan=True)
  372. _PROGRESS_LENGTH = len(" [100%]")
  373. def _get_progress_information_message(self):
  374. if self.config.getoption("capture") == "no":
  375. return ""
  376. collected = self._session.testscollected
  377. if collected:
  378. progress = len(self._progress_nodeids_reported) * 100 // collected
  379. return " [{:3d}%]".format(progress)
  380. return " [100%]"
  381. def _write_progress_information_filling_space(self):
  382. msg = self._get_progress_information_message()
  383. fill = " " * (
  384. self._tw.fullwidth - self._tw.chars_on_current_line - len(msg) - 1
  385. )
  386. self.write(fill + msg, cyan=True)
  387. def pytest_collection(self):
  388. if not self.isatty and self.config.option.verbose >= 1:
  389. self.write("collecting ... ", bold=True)
  390. def pytest_collectreport(self, report):
  391. if report.failed:
  392. self.stats.setdefault("error", []).append(report)
  393. elif report.skipped:
  394. self.stats.setdefault("skipped", []).append(report)
  395. items = [x for x in report.result if isinstance(x, pytest.Item)]
  396. self._numcollected += len(items)
  397. if self.isatty:
  398. # self.write_fspath_result(report.nodeid, 'E')
  399. self.report_collect()
  400. def report_collect(self, final=False):
  401. if self.config.option.verbose < 0:
  402. return
  403. errors = len(self.stats.get("error", []))
  404. skipped = len(self.stats.get("skipped", []))
  405. deselected = len(self.stats.get("deselected", []))
  406. if final:
  407. line = "collected "
  408. else:
  409. line = "collecting "
  410. line += (
  411. str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
  412. )
  413. if errors:
  414. line += " / %d errors" % errors
  415. if deselected:
  416. line += " / %d deselected" % deselected
  417. if skipped:
  418. line += " / %d skipped" % skipped
  419. if self.isatty:
  420. self.rewrite(line, bold=True, erase=True)
  421. if final:
  422. self.write("\n")
  423. else:
  424. self.write_line(line)
  425. @pytest.hookimpl(trylast=True)
  426. def pytest_collection_modifyitems(self):
  427. self.report_collect(True)
  428. @pytest.hookimpl(trylast=True)
  429. def pytest_sessionstart(self, session):
  430. self._session = session
  431. self._sessionstarttime = time.time()
  432. if not self.showheader:
  433. return
  434. self.write_sep("=", "test session starts", bold=True)
  435. verinfo = platform.python_version()
  436. msg = "platform %s -- Python %s" % (sys.platform, verinfo)
  437. if hasattr(sys, "pypy_version_info"):
  438. verinfo = ".".join(map(str, sys.pypy_version_info[:3]))
  439. msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3])
  440. msg += ", pytest-%s, py-%s, pluggy-%s" % (
  441. pytest.__version__,
  442. py.__version__,
  443. pluggy.__version__,
  444. )
  445. if (
  446. self.verbosity > 0
  447. or self.config.option.debug
  448. or getattr(self.config.option, "pastebin", None)
  449. ):
  450. msg += " -- " + str(sys.executable)
  451. self.write_line(msg)
  452. lines = self.config.hook.pytest_report_header(
  453. config=self.config, startdir=self.startdir
  454. )
  455. self._write_report_lines_from_hooks(lines)
  456. def _write_report_lines_from_hooks(self, lines):
  457. lines.reverse()
  458. for line in collapse(lines):
  459. self.write_line(line)
  460. def pytest_report_header(self, config):
  461. inifile = ""
  462. if config.inifile:
  463. inifile = " " + config.rootdir.bestrelpath(config.inifile)
  464. lines = ["rootdir: %s, inifile:%s" % (config.rootdir, inifile)]
  465. plugininfo = config.pluginmanager.list_plugin_distinfo()
  466. if plugininfo:
  467. lines.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo)))
  468. return lines
  469. def pytest_collection_finish(self, session):
  470. if self.config.option.collectonly:
  471. self._printcollecteditems(session.items)
  472. if self.stats.get("failed"):
  473. self._tw.sep("!", "collection failures")
  474. for rep in self.stats.get("failed"):
  475. rep.toterminal(self._tw)
  476. return 1
  477. return 0
  478. lines = self.config.hook.pytest_report_collectionfinish(
  479. config=self.config, startdir=self.startdir, items=session.items
  480. )
  481. self._write_report_lines_from_hooks(lines)
  482. def _printcollecteditems(self, items):
  483. # to print out items and their parent collectors
  484. # we take care to leave out Instances aka ()
  485. # because later versions are going to get rid of them anyway
  486. if self.config.option.verbose < 0:
  487. if self.config.option.verbose < -1:
  488. counts = {}
  489. for item in items:
  490. name = item.nodeid.split("::", 1)[0]
  491. counts[name] = counts.get(name, 0) + 1
  492. for name, count in sorted(counts.items()):
  493. self._tw.line("%s: %d" % (name, count))
  494. else:
  495. for item in items:
  496. nodeid = item.nodeid
  497. nodeid = nodeid.replace("::()::", "::")
  498. self._tw.line(nodeid)
  499. return
  500. stack = []
  501. indent = ""
  502. for item in items:
  503. needed_collectors = item.listchain()[1:] # strip root node
  504. while stack:
  505. if stack == needed_collectors[: len(stack)]:
  506. break
  507. stack.pop()
  508. for col in needed_collectors[len(stack) :]:
  509. stack.append(col)
  510. # if col.name == "()":
  511. # continue
  512. indent = (len(stack) - 1) * " "
  513. self._tw.line("%s%s" % (indent, col))
  514. @pytest.hookimpl(hookwrapper=True)
  515. def pytest_sessionfinish(self, exitstatus):
  516. outcome = yield
  517. outcome.get_result()
  518. self._tw.line("")
  519. summary_exit_codes = (
  520. EXIT_OK,
  521. EXIT_TESTSFAILED,
  522. EXIT_INTERRUPTED,
  523. EXIT_USAGEERROR,
  524. EXIT_NOTESTSCOLLECTED,
  525. )
  526. if exitstatus in summary_exit_codes:
  527. self.config.hook.pytest_terminal_summary(
  528. terminalreporter=self, exitstatus=exitstatus
  529. )
  530. if exitstatus == EXIT_INTERRUPTED:
  531. self._report_keyboardinterrupt()
  532. del self._keyboardinterrupt_memo
  533. self.summary_stats()
  534. @pytest.hookimpl(hookwrapper=True)
  535. def pytest_terminal_summary(self):
  536. self.summary_errors()
  537. self.summary_failures()
  538. yield
  539. self.summary_warnings()
  540. self.summary_passes()
  541. def pytest_keyboard_interrupt(self, excinfo):
  542. self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
  543. def pytest_unconfigure(self):
  544. if hasattr(self, "_keyboardinterrupt_memo"):
  545. self._report_keyboardinterrupt()
  546. def _report_keyboardinterrupt(self):
  547. excrepr = self._keyboardinterrupt_memo
  548. msg = excrepr.reprcrash.message
  549. self.write_sep("!", msg)
  550. if "KeyboardInterrupt" in msg:
  551. if self.config.option.fulltrace:
  552. excrepr.toterminal(self._tw)
  553. else:
  554. excrepr.reprcrash.toterminal(self._tw)
  555. self._tw.line(
  556. "(to show a full traceback on KeyboardInterrupt use --fulltrace)",
  557. yellow=True,
  558. )
  559. def _locationline(self, nodeid, fspath, lineno, domain):
  560. def mkrel(nodeid):
  561. line = self.config.cwd_relative_nodeid(nodeid)
  562. if domain and line.endswith(domain):
  563. line = line[: -len(domain)]
  564. values = domain.split("[")
  565. values[0] = values[0].replace(".", "::") # don't replace '.' in params
  566. line += "[".join(values)
  567. return line
  568. # collect_fspath comes from testid which has a "/"-normalized path
  569. if fspath:
  570. res = mkrel(nodeid).replace("::()", "") # parens-normalization
  571. if nodeid.split("::")[0] != fspath.replace("\\", nodes.SEP):
  572. res += " <- " + self.startdir.bestrelpath(fspath)
  573. else:
  574. res = "[location]"
  575. return res + " "
  576. def _getfailureheadline(self, rep):
  577. if hasattr(rep, "location"):
  578. fspath, lineno, domain = rep.location
  579. return domain
  580. else:
  581. return "test session" # XXX?
  582. def _getcrashline(self, rep):
  583. try:
  584. return str(rep.longrepr.reprcrash)
  585. except AttributeError:
  586. try:
  587. return str(rep.longrepr)[:50]
  588. except AttributeError:
  589. return ""
  590. #
  591. # summaries for sessionfinish
  592. #
  593. def getreports(self, name):
  594. values = []
  595. for x in self.stats.get(name, []):
  596. if not hasattr(x, "_pdbshown"):
  597. values.append(x)
  598. return values
  599. def summary_warnings(self):
  600. if self.hasopt("w"):
  601. all_warnings = self.stats.get("warnings")
  602. if not all_warnings:
  603. return
  604. grouped = itertools.groupby(
  605. all_warnings, key=lambda wr: wr.get_location(self.config)
  606. )
  607. self.write_sep("=", "warnings summary", yellow=True, bold=False)
  608. for location, warning_records in grouped:
  609. self._tw.line(str(location) if location else "<undetermined location>")
  610. for w in warning_records:
  611. lines = w.message.splitlines()
  612. indented = "\n".join(" " + x for x in lines)
  613. self._tw.line(indented)
  614. self._tw.line()
  615. self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html")
  616. def summary_passes(self):
  617. if self.config.option.tbstyle != "no":
  618. if self.hasopt("P"):
  619. reports = self.getreports("passed")
  620. if not reports:
  621. return
  622. self.write_sep("=", "PASSES")
  623. for rep in reports:
  624. msg = self._getfailureheadline(rep)
  625. self.write_sep("_", msg)
  626. self._outrep_summary(rep)
  627. def print_teardown_sections(self, rep):
  628. showcapture = self.config.option.showcapture
  629. if showcapture == "no":
  630. return
  631. for secname, content in rep.sections:
  632. if showcapture != "all" and showcapture not in secname:
  633. continue
  634. if "teardown" in secname:
  635. self._tw.sep("-", secname)
  636. if content[-1:] == "\n":
  637. content = content[:-1]
  638. self._tw.line(content)
  639. def summary_failures(self):
  640. if self.config.option.tbstyle != "no":
  641. reports = self.getreports("failed")
  642. if not reports:
  643. return
  644. self.write_sep("=", "FAILURES")
  645. for rep in reports:
  646. if self.config.option.tbstyle == "line":
  647. line = self._getcrashline(rep)
  648. self.write_line(line)
  649. else:
  650. msg = self._getfailureheadline(rep)
  651. markup = {"red": True, "bold": True}
  652. self.write_sep("_", msg, **markup)
  653. self._outrep_summary(rep)
  654. for report in self.getreports(""):
  655. if report.nodeid == rep.nodeid and report.when == "teardown":
  656. self.print_teardown_sections(report)
  657. def summary_errors(self):
  658. if self.config.option.tbstyle != "no":
  659. reports = self.getreports("error")
  660. if not reports:
  661. return
  662. self.write_sep("=", "ERRORS")
  663. for rep in self.stats["error"]:
  664. msg = self._getfailureheadline(rep)
  665. if not hasattr(rep, "when"):
  666. # collect
  667. msg = "ERROR collecting " + msg
  668. elif rep.when == "setup":
  669. msg = "ERROR at setup of " + msg
  670. elif rep.when == "teardown":
  671. msg = "ERROR at teardown of " + msg
  672. self.write_sep("_", msg)
  673. self._outrep_summary(rep)
  674. def _outrep_summary(self, rep):
  675. rep.toterminal(self._tw)
  676. showcapture = self.config.option.showcapture
  677. if showcapture == "no":
  678. return
  679. for secname, content in rep.sections:
  680. if showcapture != "all" and showcapture not in secname:
  681. continue
  682. self._tw.sep("-", secname)
  683. if content[-1:] == "\n":
  684. content = content[:-1]
  685. self._tw.line(content)
  686. def summary_stats(self):
  687. session_duration = time.time() - self._sessionstarttime
  688. (line, color) = build_summary_stats_line(self.stats)
  689. msg = "%s in %.2f seconds" % (line, session_duration)
  690. markup = {color: True, "bold": True}
  691. if self.verbosity >= 0:
  692. self.write_sep("=", msg, **markup)
  693. if self.verbosity == -1:
  694. self.write_line(msg, **markup)
  695. def repr_pythonversion(v=None):
  696. if v is None:
  697. v = sys.version_info
  698. try:
  699. return "%s.%s.%s-%s-%s" % v
  700. except (TypeError, ValueError):
  701. return str(v)
  702. def build_summary_stats_line(stats):
  703. keys = (
  704. "failed passed skipped deselected " "xfailed xpassed warnings error"
  705. ).split()
  706. unknown_key_seen = False
  707. for key in stats.keys():
  708. if key not in keys:
  709. if key: # setup/teardown reports have an empty key, ignore them
  710. keys.append(key)
  711. unknown_key_seen = True
  712. parts = []
  713. for key in keys:
  714. val = stats.get(key, None)
  715. if val:
  716. parts.append("%d %s" % (len(val), key))
  717. if parts:
  718. line = ", ".join(parts)
  719. else:
  720. line = "no tests ran"
  721. if "failed" in stats or "error" in stats:
  722. color = "red"
  723. elif "warnings" in stats or unknown_key_seen:
  724. color = "yellow"
  725. elif "passed" in stats:
  726. color = "green"
  727. else:
  728. color = "yellow"
  729. return (line, color)
  730. def _plugin_nameversions(plugininfo):
  731. values = []
  732. for plugin, dist in plugininfo:
  733. # gets us name and version!
  734. name = "{dist.project_name}-{dist.version}".format(dist=dist)
  735. # questionable convenience, but it keeps things short
  736. if name.startswith("pytest-"):
  737. name = name[7:]
  738. # we decided to print python package names
  739. # they can have more than one plugin
  740. if name not in values:
  741. values.append(name)
  742. return values