Coverage for /usr/local/lib/python3.7/site-packages/_pytest/terminal.py : 24%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1""" terminal reporting of the full testing process.
3This is a good source for looking at the various reporting hooks.
4"""
5import argparse
6import collections
7import datetime
8import platform
9import sys
10import time
11from functools import partial
12from typing import Any
13from typing import Callable
14from typing import Dict
15from typing import List
16from typing import Mapping
17from typing import Optional
18from typing import Set
19from typing import Tuple
21import attr
22import pluggy
23import py
24from more_itertools import collapse
26import pytest
27from _pytest import nodes
28from _pytest.config import Config
29from _pytest.main import ExitCode
30from _pytest.main import Session
31from _pytest.reports import CollectReport
32from _pytest.reports import TestReport
34REPORT_COLLECTING_RESOLUTION = 0.5
37class MoreQuietAction(argparse.Action):
38 """
39 a modified copy of the argparse count action which counts down and updates
40 the legacy quiet attribute at the same time
42 used to unify verbosity handling
43 """
45 def __init__(self, option_strings, dest, default=None, required=False, help=None):
46 super().__init__(
47 option_strings=option_strings,
48 dest=dest,
49 nargs=0,
50 default=default,
51 required=required,
52 help=help,
53 )
55 def __call__(self, parser, namespace, values, option_string=None):
56 new_count = getattr(namespace, self.dest, 0) - 1
57 setattr(namespace, self.dest, new_count)
58 # todo Deprecate config.quiet
59 namespace.quiet = getattr(namespace, "quiet", 0) + 1
62def pytest_addoption(parser):
63 group = parser.getgroup("terminal reporting", "reporting", after="general")
64 group._addoption(
65 "-v",
66 "--verbose",
67 action="count",
68 default=0,
69 dest="verbose",
70 help="increase verbosity.",
71 ),
72 group._addoption(
73 "-q",
74 "--quiet",
75 action=MoreQuietAction,
76 default=0,
77 dest="verbose",
78 help="decrease verbosity.",
79 ),
80 group._addoption(
81 "--verbosity",
82 dest="verbose",
83 type=int,
84 default=0,
85 help="set verbosity. Default is 0.",
86 )
87 group._addoption(
88 "-r",
89 action="store",
90 dest="reportchars",
91 default="",
92 metavar="chars",
93 help="show extra test summary info as specified by chars: (f)ailed, "
94 "(E)rror, (s)kipped, (x)failed, (X)passed, "
95 "(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. "
96 "(w)arnings are enabled by default (see --disable-warnings).",
97 )
98 group._addoption(
99 "--disable-warnings",
100 "--disable-pytest-warnings",
101 default=False,
102 dest="disable_warnings",
103 action="store_true",
104 help="disable warnings summary",
105 )
106 group._addoption(
107 "-l",
108 "--showlocals",
109 action="store_true",
110 dest="showlocals",
111 default=False,
112 help="show locals in tracebacks (disabled by default).",
113 )
114 group._addoption(
115 "--tb",
116 metavar="style",
117 action="store",
118 dest="tbstyle",
119 default="auto",
120 choices=["auto", "long", "short", "no", "line", "native"],
121 help="traceback print mode (auto/long/short/line/native/no).",
122 )
123 group._addoption(
124 "--show-capture",
125 action="store",
126 dest="showcapture",
127 choices=["no", "stdout", "stderr", "log", "all"],
128 default="all",
129 help="Controls how captured stdout/stderr/log is shown on failed tests. "
130 "Default is 'all'.",
131 )
132 group._addoption(
133 "--fulltrace",
134 "--full-trace",
135 action="store_true",
136 default=False,
137 help="don't cut any tracebacks (default is to cut).",
138 )
139 group._addoption(
140 "--color",
141 metavar="color",
142 action="store",
143 dest="color",
144 default="auto",
145 choices=["yes", "no", "auto"],
146 help="color terminal output (yes/no/auto).",
147 )
149 parser.addini(
150 "console_output_style",
151 help='console output: "classic", or with additional progress information ("progress" (percentage) | "count").',
152 default="progress",
153 )
156def pytest_configure(config: Config) -> None:
157 reporter = TerminalReporter(config, sys.stdout)
158 config.pluginmanager.register(reporter, "terminalreporter")
159 if config.option.debug or config.option.traceconfig:
161 def mywriter(tags, args):
162 msg = " ".join(map(str, args))
163 reporter.write_line("[traceconfig] " + msg)
165 config.trace.root.setprocessor("pytest:config", mywriter)
168def getreportopt(config: Config) -> str:
169 reportopts = ""
170 reportchars = config.option.reportchars
171 if not config.option.disable_warnings and "w" not in reportchars:
172 reportchars += "w"
173 elif config.option.disable_warnings and "w" in reportchars:
174 reportchars = reportchars.replace("w", "")
175 for char in reportchars:
176 if char == "a":
177 reportopts = "sxXwEf"
178 elif char == "A":
179 reportopts = "PpsxXwEf"
180 break
181 elif char not in reportopts:
182 reportopts += char
183 return reportopts
186@pytest.hookimpl(trylast=True) # after _pytest.runner
187def pytest_report_teststatus(report: TestReport) -> Tuple[str, str, str]:
188 if report.passed:
189 letter = "."
190 elif report.skipped:
191 letter = "s"
192 elif report.failed:
193 letter = "F"
194 if report.when != "call":
195 letter = "f"
197 # Report failed CollectReports as "error" (in line with pytest_collectreport).
198 outcome = report.outcome
199 if report.when == "collect" and outcome == "failed":
200 outcome = "error"
202 return outcome, letter, outcome.upper()
205@attr.s
206class WarningReport:
207 """
208 Simple structure to hold warnings information captured by ``pytest_warning_captured``.
210 :ivar str message: user friendly message about the warning
211 :ivar str|None nodeid: node id that generated the warning (see ``get_location``).
212 :ivar tuple|py.path.local fslocation:
213 file system location of the source of the warning (see ``get_location``).
214 """
216 message = attr.ib(type=str)
217 nodeid = attr.ib(type=Optional[str], default=None)
218 fslocation = attr.ib(default=None)
219 count_towards_summary = True
221 def get_location(self, config):
222 """
223 Returns the more user-friendly information about the location
224 of a warning, or None.
225 """
226 if self.nodeid:
227 return self.nodeid
228 if self.fslocation:
229 if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2:
230 filename, linenum = self.fslocation[:2]
231 relpath = py.path.local(filename).relto(config.invocation_dir)
232 if not relpath:
233 relpath = str(filename)
234 return "{}:{}".format(relpath, linenum)
235 else:
236 return str(self.fslocation)
237 return None
240class TerminalReporter:
241 def __init__(self, config: Config, file=None) -> None:
242 import _pytest.config
244 self.config = config
245 self._numcollected = 0
246 self._session = None # type: Optional[Session]
247 self._showfspath = None
249 self.stats = {} # type: Dict[str, List[Any]]
250 self.startdir = config.invocation_dir
251 if file is None:
252 file = sys.stdout
253 self._tw = _pytest.config.create_terminal_writer(config, file)
254 # self.writer will be deprecated in pytest-3.4
255 self.writer = self._tw
256 self._screen_width = self._tw.fullwidth
257 self.currentfspath = None # type: Any
258 self.reportchars = getreportopt(config)
259 self.hasmarkup = self._tw.hasmarkup
260 self.isatty = file.isatty()
261 self._progress_nodeids_reported = set() # type: Set[str]
262 self._show_progress_info = self._determine_show_progress_info()
263 self._collect_report_last_write = None # type: Optional[float]
265 def _determine_show_progress_info(self):
266 """Return True if we should display progress information based on the current config"""
267 # do not show progress if we are not capturing output (#3038)
268 if self.config.getoption("capture", "no") == "no":
269 return False
270 # do not show progress if we are showing fixture setup/teardown
271 if self.config.getoption("setupshow", False):
272 return False
273 cfg = self.config.getini("console_output_style")
274 if cfg in ("progress", "count"):
275 return cfg
276 return False
278 @property
279 def verbosity(self):
280 return self.config.option.verbose
282 @property
283 def showheader(self):
284 return self.verbosity >= 0
286 @property
287 def showfspath(self):
288 if self._showfspath is None:
289 return self.verbosity >= 0
290 return self._showfspath
292 @showfspath.setter
293 def showfspath(self, value):
294 self._showfspath = value
296 @property
297 def showlongtestinfo(self):
298 return self.verbosity > 0
300 def hasopt(self, char):
301 char = {"xfailed": "x", "skipped": "s"}.get(char, char)
302 return char in self.reportchars
304 def write_fspath_result(self, nodeid, res, **markup):
305 fspath = self.config.rootdir.join(nodeid.split("::")[0])
306 # NOTE: explicitly check for None to work around py bug, and for less
307 # overhead in general (https://github.com/pytest-dev/py/pull/207).
308 if self.currentfspath is None or fspath != self.currentfspath:
309 if self.currentfspath is not None and self._show_progress_info:
310 self._write_progress_information_filling_space()
311 self.currentfspath = fspath
312 fspath = self.startdir.bestrelpath(fspath)
313 self._tw.line()
314 self._tw.write(fspath + " ")
315 self._tw.write(res, **markup)
317 def write_ensure_prefix(self, prefix, extra="", **kwargs):
318 if self.currentfspath != prefix:
319 self._tw.line()
320 self.currentfspath = prefix
321 self._tw.write(prefix)
322 if extra:
323 self._tw.write(extra, **kwargs)
324 self.currentfspath = -2
326 def ensure_newline(self):
327 if self.currentfspath:
328 self._tw.line()
329 self.currentfspath = None
331 def write(self, content, **markup):
332 self._tw.write(content, **markup)
334 def write_line(self, line, **markup):
335 if not isinstance(line, str):
336 line = str(line, errors="replace")
337 self.ensure_newline()
338 self._tw.line(line, **markup)
340 def rewrite(self, line, **markup):
341 """
342 Rewinds the terminal cursor to the beginning and writes the given line.
344 :kwarg erase: if True, will also add spaces until the full terminal width to ensure
345 previous lines are properly erased.
347 The rest of the keyword arguments are markup instructions.
348 """
349 erase = markup.pop("erase", False)
350 if erase:
351 fill_count = self._tw.fullwidth - len(line) - 1
352 fill = " " * fill_count
353 else:
354 fill = ""
355 line = str(line)
356 self._tw.write("\r" + line + fill, **markup)
358 def write_sep(self, sep, title=None, **markup):
359 self.ensure_newline()
360 self._tw.sep(sep, title, **markup)
362 def section(self, title, sep="=", **kw):
363 self._tw.sep(sep, title, **kw)
365 def line(self, msg, **kw):
366 self._tw.line(msg, **kw)
368 def pytest_internalerror(self, excrepr):
369 for line in str(excrepr).split("\n"):
370 self.write_line("INTERNALERROR> " + line)
371 return 1
373 def pytest_warning_captured(self, warning_message, item):
374 # from _pytest.nodes import get_fslocation_from_item
375 from _pytest.warnings import warning_record_to_str
377 warnings = self.stats.setdefault("warnings", [])
378 fslocation = warning_message.filename, warning_message.lineno
379 message = warning_record_to_str(warning_message)
381 nodeid = item.nodeid if item is not None else ""
382 warning_report = WarningReport(
383 fslocation=fslocation, message=message, nodeid=nodeid
384 )
385 warnings.append(warning_report)
387 def pytest_plugin_registered(self, plugin):
388 if self.config.option.traceconfig:
389 msg = "PLUGIN registered: {}".format(plugin)
390 # XXX this event may happen during setup/teardown time
391 # which unfortunately captures our output here
392 # which garbles our output if we use self.write_line
393 self.write_line(msg)
395 def pytest_deselected(self, items):
396 self.stats.setdefault("deselected", []).extend(items)
398 def pytest_runtest_logstart(self, nodeid, location):
399 # ensure that the path is printed before the
400 # 1st test of a module starts running
401 if self.showlongtestinfo:
402 line = self._locationline(nodeid, *location)
403 self.write_ensure_prefix(line, "")
404 elif self.showfspath:
405 fsid = nodeid.split("::")[0]
406 self.write_fspath_result(fsid, "")
408 def pytest_runtest_logreport(self, report: TestReport) -> None:
409 self._tests_ran = True
410 rep = report
411 res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
412 category, letter, word = res
413 if isinstance(word, tuple):
414 word, markup = word
415 else:
416 markup = None
417 self.stats.setdefault(category, []).append(rep)
418 if not letter and not word:
419 # probably passed setup/teardown
420 return
421 running_xdist = hasattr(rep, "node")
422 if markup is None:
423 was_xfail = hasattr(report, "wasxfail")
424 if rep.passed and not was_xfail:
425 markup = {"green": True}
426 elif rep.passed and was_xfail:
427 markup = {"yellow": True}
428 elif rep.failed:
429 markup = {"red": True}
430 elif rep.skipped:
431 markup = {"yellow": True}
432 else:
433 markup = {}
434 if self.verbosity <= 0:
435 if not running_xdist and self.showfspath:
436 self.write_fspath_result(rep.nodeid, letter, **markup)
437 else:
438 self._tw.write(letter, **markup)
439 else:
440 self._progress_nodeids_reported.add(rep.nodeid)
441 line = self._locationline(rep.nodeid, *rep.location)
442 if not running_xdist:
443 self.write_ensure_prefix(line, word, **markup)
444 if self._show_progress_info:
445 self._write_progress_information_filling_space()
446 else:
447 self.ensure_newline()
448 self._tw.write("[%s]" % rep.node.gateway.id) # type: ignore
449 if self._show_progress_info:
450 self._tw.write(
451 self._get_progress_information_message() + " ", cyan=True
452 )
453 else:
454 self._tw.write(" ")
455 self._tw.write(word, **markup)
456 self._tw.write(" " + line)
457 self.currentfspath = -2
459 def pytest_runtest_logfinish(self, nodeid):
460 assert self._session
461 if self.verbosity <= 0 and self._show_progress_info:
462 if self._show_progress_info == "count":
463 num_tests = self._session.testscollected
464 progress_length = len(" [{}/{}]".format(str(num_tests), str(num_tests)))
465 else:
466 progress_length = len(" [100%]")
468 main_color, _ = _get_main_color(self.stats)
470 self._progress_nodeids_reported.add(nodeid)
471 is_last_item = (
472 len(self._progress_nodeids_reported) == self._session.testscollected
473 )
474 if is_last_item:
475 self._write_progress_information_filling_space(color=main_color)
476 else:
477 w = self._width_of_current_line
478 past_edge = w + progress_length + 1 >= self._screen_width
479 if past_edge:
480 msg = self._get_progress_information_message()
481 self._tw.write(msg + "\n", **{main_color: True})
483 def _get_progress_information_message(self) -> str:
484 assert self._session
485 collected = self._session.testscollected
486 if self._show_progress_info == "count":
487 if collected:
488 progress = self._progress_nodeids_reported
489 counter_format = "{{:{}d}}".format(len(str(collected)))
490 format_string = " [{}/{{}}]".format(counter_format)
491 return format_string.format(len(progress), collected)
492 return " [ {} / {} ]".format(collected, collected)
493 else:
494 if collected:
495 return " [{:3d}%]".format(
496 len(self._progress_nodeids_reported) * 100 // collected
497 )
498 return " [100%]"
500 def _write_progress_information_filling_space(self, color=None):
501 if not color:
502 color, _ = _get_main_color(self.stats)
503 msg = self._get_progress_information_message()
504 w = self._width_of_current_line
505 fill = self._tw.fullwidth - w - 1
506 self.write(msg.rjust(fill), **{color: True})
508 @property
509 def _width_of_current_line(self):
510 """Return the width of current line, using the superior implementation of py-1.6 when available"""
511 try:
512 return self._tw.width_of_current_line
513 except AttributeError:
514 # py < 1.6.0
515 return self._tw.chars_on_current_line
517 def pytest_collection(self):
518 if self.isatty:
519 if self.config.option.verbose >= 0:
520 self.write("collecting ... ", bold=True)
521 self._collect_report_last_write = time.time()
522 elif self.config.option.verbose >= 1:
523 self.write("collecting ... ", bold=True)
525 def pytest_collectreport(self, report: CollectReport) -> None:
526 if report.failed:
527 self.stats.setdefault("error", []).append(report)
528 elif report.skipped:
529 self.stats.setdefault("skipped", []).append(report)
530 items = [x for x in report.result if isinstance(x, pytest.Item)]
531 self._numcollected += len(items)
532 if self.isatty:
533 self.report_collect()
535 def report_collect(self, final=False):
536 if self.config.option.verbose < 0:
537 return
539 if not final:
540 # Only write "collecting" report every 0.5s.
541 t = time.time()
542 if (
543 self._collect_report_last_write is not None
544 and self._collect_report_last_write > t - REPORT_COLLECTING_RESOLUTION
545 ):
546 return
547 self._collect_report_last_write = t
549 errors = len(self.stats.get("error", []))
550 skipped = len(self.stats.get("skipped", []))
551 deselected = len(self.stats.get("deselected", []))
552 selected = self._numcollected - errors - skipped - deselected
553 if final:
554 line = "collected "
555 else:
556 line = "collecting "
557 line += (
558 str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
559 )
560 if errors:
561 line += " / %d error%s" % (errors, "s" if errors != 1 else "")
562 if deselected:
563 line += " / %d deselected" % deselected
564 if skipped:
565 line += " / %d skipped" % skipped
566 if self._numcollected > selected > 0:
567 line += " / %d selected" % selected
568 if self.isatty:
569 self.rewrite(line, bold=True, erase=True)
570 if final:
571 self.write("\n")
572 else:
573 self.write_line(line)
575 @pytest.hookimpl(trylast=True)
576 def pytest_sessionstart(self, session: Session) -> None:
577 self._session = session
578 self._sessionstarttime = time.time()
579 if not self.showheader:
580 return
581 self.write_sep("=", "test session starts", bold=True)
582 verinfo = platform.python_version()
583 msg = "platform {} -- Python {}".format(sys.platform, verinfo)
584 pypy_version_info = getattr(sys, "pypy_version_info", None)
585 if pypy_version_info:
586 verinfo = ".".join(map(str, pypy_version_info[:3]))
587 msg += "[pypy-{}-{}]".format(verinfo, pypy_version_info[3])
588 msg += ", pytest-{}, py-{}, pluggy-{}".format(
589 pytest.__version__, py.__version__, pluggy.__version__
590 )
591 if (
592 self.verbosity > 0
593 or self.config.option.debug
594 or getattr(self.config.option, "pastebin", None)
595 ):
596 msg += " -- " + str(sys.executable)
597 self.write_line(msg)
598 lines = self.config.hook.pytest_report_header(
599 config=self.config, startdir=self.startdir
600 )
601 self._write_report_lines_from_hooks(lines)
603 def _write_report_lines_from_hooks(self, lines):
604 lines.reverse()
605 for line in collapse(lines):
606 self.write_line(line)
608 def pytest_report_header(self, config):
609 line = "rootdir: %s" % config.rootdir
611 if config.inifile:
612 line += ", inifile: " + config.rootdir.bestrelpath(config.inifile)
614 testpaths = config.getini("testpaths")
615 if testpaths and config.args == testpaths:
616 rel_paths = [config.rootdir.bestrelpath(x) for x in testpaths]
617 line += ", testpaths: {}".format(", ".join(rel_paths))
618 result = [line]
620 plugininfo = config.pluginmanager.list_plugin_distinfo()
621 if plugininfo:
622 result.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo)))
623 return result
625 def pytest_collection_finish(self, session):
626 self.report_collect(True)
628 if self.config.getoption("collectonly"):
629 self._printcollecteditems(session.items)
631 lines = self.config.hook.pytest_report_collectionfinish(
632 config=self.config, startdir=self.startdir, items=session.items
633 )
634 self._write_report_lines_from_hooks(lines)
636 if self.config.getoption("collectonly"):
637 failed = self.stats.get("failed")
638 if failed:
639 self._tw.sep("!", "collection failures")
640 for rep in failed:
641 rep.toterminal(self._tw)
643 def _printcollecteditems(self, items):
644 # to print out items and their parent collectors
645 # we take care to leave out Instances aka ()
646 # because later versions are going to get rid of them anyway
647 if self.config.option.verbose < 0:
648 if self.config.option.verbose < -1:
649 counts = {} # type: Dict[str, int]
650 for item in items:
651 name = item.nodeid.split("::", 1)[0]
652 counts[name] = counts.get(name, 0) + 1
653 for name, count in sorted(counts.items()):
654 self._tw.line("%s: %d" % (name, count))
655 else:
656 for item in items:
657 self._tw.line(item.nodeid)
658 return
659 stack = []
660 indent = ""
661 for item in items:
662 needed_collectors = item.listchain()[1:] # strip root node
663 while stack:
664 if stack == needed_collectors[: len(stack)]:
665 break
666 stack.pop()
667 for col in needed_collectors[len(stack) :]:
668 stack.append(col)
669 if col.name == "()": # Skip Instances.
670 continue
671 indent = (len(stack) - 1) * " "
672 self._tw.line("{}{}".format(indent, col))
673 if self.config.option.verbose >= 1:
674 if hasattr(col, "_obj") and col._obj.__doc__:
675 for line in col._obj.__doc__.strip().splitlines():
676 self._tw.line("{}{}".format(indent + " ", line.strip()))
678 @pytest.hookimpl(hookwrapper=True)
679 def pytest_sessionfinish(self, session: Session, exitstatus: ExitCode):
680 outcome = yield
681 outcome.get_result()
682 self._tw.line("")
683 summary_exit_codes = (
684 ExitCode.OK,
685 ExitCode.TESTS_FAILED,
686 ExitCode.INTERRUPTED,
687 ExitCode.USAGE_ERROR,
688 ExitCode.NO_TESTS_COLLECTED,
689 )
690 if exitstatus in summary_exit_codes:
691 self.config.hook.pytest_terminal_summary(
692 terminalreporter=self, exitstatus=exitstatus, config=self.config
693 )
694 if session.shouldfail:
695 self.write_sep("!", session.shouldfail, red=True)
696 if exitstatus == ExitCode.INTERRUPTED:
697 self._report_keyboardinterrupt()
698 del self._keyboardinterrupt_memo
699 elif session.shouldstop:
700 self.write_sep("!", session.shouldstop, red=True)
701 self.summary_stats()
703 @pytest.hookimpl(hookwrapper=True)
704 def pytest_terminal_summary(self):
705 self.summary_errors()
706 self.summary_failures()
707 self.summary_warnings()
708 self.summary_passes()
709 yield
710 self.short_test_summary()
711 # Display any extra warnings from teardown here (if any).
712 self.summary_warnings()
714 def pytest_keyboard_interrupt(self, excinfo):
715 self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
717 def pytest_unconfigure(self):
718 if hasattr(self, "_keyboardinterrupt_memo"):
719 self._report_keyboardinterrupt()
721 def _report_keyboardinterrupt(self):
722 excrepr = self._keyboardinterrupt_memo
723 msg = excrepr.reprcrash.message
724 self.write_sep("!", msg)
725 if "KeyboardInterrupt" in msg:
726 if self.config.option.fulltrace:
727 excrepr.toterminal(self._tw)
728 else:
729 excrepr.reprcrash.toterminal(self._tw)
730 self._tw.line(
731 "(to show a full traceback on KeyboardInterrupt use --full-trace)",
732 yellow=True,
733 )
735 def _locationline(self, nodeid, fspath, lineno, domain):
736 def mkrel(nodeid):
737 line = self.config.cwd_relative_nodeid(nodeid)
738 if domain and line.endswith(domain):
739 line = line[: -len(domain)]
740 values = domain.split("[")
741 values[0] = values[0].replace(".", "::") # don't replace '.' in params
742 line += "[".join(values)
743 return line
745 # collect_fspath comes from testid which has a "/"-normalized path
747 if fspath:
748 res = mkrel(nodeid)
749 if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
750 "\\", nodes.SEP
751 ):
752 res += " <- " + self.startdir.bestrelpath(fspath)
753 else:
754 res = "[location]"
755 return res + " "
757 def _getfailureheadline(self, rep):
758 head_line = rep.head_line
759 if head_line:
760 return head_line
761 return "test session" # XXX?
763 def _getcrashline(self, rep):
764 try:
765 return str(rep.longrepr.reprcrash)
766 except AttributeError:
767 try:
768 return str(rep.longrepr)[:50]
769 except AttributeError:
770 return ""
772 #
773 # summaries for sessionfinish
774 #
775 def getreports(self, name):
776 values = []
777 for x in self.stats.get(name, []):
778 if not hasattr(x, "_pdbshown"):
779 values.append(x)
780 return values
782 def summary_warnings(self):
783 if self.hasopt("w"):
784 all_warnings = self.stats.get(
785 "warnings"
786 ) # type: Optional[List[WarningReport]]
787 if not all_warnings:
788 return
790 final = hasattr(self, "_already_displayed_warnings")
791 if final:
792 warning_reports = all_warnings[self._already_displayed_warnings :]
793 else:
794 warning_reports = all_warnings
795 self._already_displayed_warnings = len(warning_reports)
796 if not warning_reports:
797 return
799 reports_grouped_by_message = (
800 collections.OrderedDict()
801 ) # type: collections.OrderedDict[str, List[WarningReport]]
802 for wr in warning_reports:
803 reports_grouped_by_message.setdefault(wr.message, []).append(wr)
805 title = "warnings summary (final)" if final else "warnings summary"
806 self.write_sep("=", title, yellow=True, bold=False)
807 for message, warning_reports in reports_grouped_by_message.items():
808 has_any_location = False
809 for w in warning_reports:
810 location = w.get_location(self.config)
811 if location:
812 self._tw.line(str(location))
813 has_any_location = True
814 if has_any_location:
815 lines = message.splitlines()
816 indented = "\n".join(" " + x for x in lines)
817 message = indented.rstrip()
818 else:
819 message = message.rstrip()
820 self._tw.line(message)
821 self._tw.line()
822 self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html")
824 def summary_passes(self):
825 if self.config.option.tbstyle != "no":
826 if self.hasopt("P"):
827 reports = self.getreports("passed")
828 if not reports:
829 return
830 self.write_sep("=", "PASSES")
831 for rep in reports:
832 if rep.sections:
833 msg = self._getfailureheadline(rep)
834 self.write_sep("_", msg, green=True, bold=True)
835 self._outrep_summary(rep)
836 self._handle_teardown_sections(rep.nodeid)
838 def _get_teardown_reports(self, nodeid: str) -> List[TestReport]:
839 return [
840 report
841 for report in self.getreports("")
842 if report.when == "teardown" and report.nodeid == nodeid
843 ]
845 def _handle_teardown_sections(self, nodeid: str) -> None:
846 for report in self._get_teardown_reports(nodeid):
847 self.print_teardown_sections(report)
849 def print_teardown_sections(self, rep: TestReport) -> None:
850 showcapture = self.config.option.showcapture
851 if showcapture == "no":
852 return
853 for secname, content in rep.sections:
854 if showcapture != "all" and showcapture not in secname:
855 continue
856 if "teardown" in secname:
857 self._tw.sep("-", secname)
858 if content[-1:] == "\n":
859 content = content[:-1]
860 self._tw.line(content)
862 def summary_failures(self):
863 if self.config.option.tbstyle != "no":
864 reports = self.getreports("failed")
865 if not reports:
866 return
867 self.write_sep("=", "FAILURES")
868 if self.config.option.tbstyle == "line":
869 for rep in reports:
870 line = self._getcrashline(rep)
871 self.write_line(line)
872 else:
873 for rep in reports:
874 msg = self._getfailureheadline(rep)
875 self.write_sep("_", msg, red=True, bold=True)
876 self._outrep_summary(rep)
877 self._handle_teardown_sections(rep.nodeid)
879 def summary_errors(self):
880 if self.config.option.tbstyle != "no":
881 reports = self.getreports("error")
882 if not reports:
883 return
884 self.write_sep("=", "ERRORS")
885 for rep in self.stats["error"]:
886 msg = self._getfailureheadline(rep)
887 if rep.when == "collect":
888 msg = "ERROR collecting " + msg
889 else:
890 msg = "ERROR at {} of {}".format(rep.when, msg)
891 self.write_sep("_", msg, red=True, bold=True)
892 self._outrep_summary(rep)
894 def _outrep_summary(self, rep):
895 rep.toterminal(self._tw)
896 showcapture = self.config.option.showcapture
897 if showcapture == "no":
898 return
899 for secname, content in rep.sections:
900 if showcapture != "all" and showcapture not in secname:
901 continue
902 self._tw.sep("-", secname)
903 if content[-1:] == "\n":
904 content = content[:-1]
905 self._tw.line(content)
907 def summary_stats(self):
908 if self.verbosity < -1:
909 return
911 session_duration = time.time() - self._sessionstarttime
912 (parts, main_color) = build_summary_stats_line(self.stats)
913 line_parts = []
915 display_sep = self.verbosity >= 0
916 if display_sep:
917 fullwidth = self._tw.fullwidth
918 for text, markup in parts:
919 with_markup = self._tw.markup(text, **markup)
920 if display_sep:
921 fullwidth += len(with_markup) - len(text)
922 line_parts.append(with_markup)
923 msg = ", ".join(line_parts)
925 main_markup = {main_color: True}
926 duration = " in {}".format(format_session_duration(session_duration))
927 duration_with_markup = self._tw.markup(duration, **main_markup)
928 if display_sep:
929 fullwidth += len(duration_with_markup) - len(duration)
930 msg += duration_with_markup
932 if display_sep:
933 markup_for_end_sep = self._tw.markup("", **main_markup)
934 if markup_for_end_sep.endswith("\x1b[0m"):
935 markup_for_end_sep = markup_for_end_sep[:-4]
936 fullwidth += len(markup_for_end_sep)
937 msg += markup_for_end_sep
939 if display_sep:
940 self.write_sep("=", msg, fullwidth=fullwidth, **main_markup)
941 else:
942 self.write_line(msg, **main_markup)
944 def short_test_summary(self) -> None:
945 if not self.reportchars:
946 return
948 def show_simple(stat, lines: List[str]) -> None:
949 failed = self.stats.get(stat, [])
950 if not failed:
951 return
952 termwidth = self.writer.fullwidth
953 config = self.config
954 for rep in failed:
955 line = _get_line_with_reprcrash_message(config, rep, termwidth)
956 lines.append(line)
958 def show_xfailed(lines: List[str]) -> None:
959 xfailed = self.stats.get("xfailed", [])
960 for rep in xfailed:
961 verbose_word = rep._get_verbose_word(self.config)
962 pos = _get_pos(self.config, rep)
963 lines.append("{} {}".format(verbose_word, pos))
964 reason = rep.wasxfail
965 if reason:
966 lines.append(" " + str(reason))
968 def show_xpassed(lines: List[str]) -> None:
969 xpassed = self.stats.get("xpassed", [])
970 for rep in xpassed:
971 verbose_word = rep._get_verbose_word(self.config)
972 pos = _get_pos(self.config, rep)
973 reason = rep.wasxfail
974 lines.append("{} {} {}".format(verbose_word, pos, reason))
976 def show_skipped(lines: List[str]) -> None:
977 skipped = self.stats.get("skipped", [])
978 fskips = _folded_skips(skipped) if skipped else []
979 if not fskips:
980 return
981 verbose_word = skipped[0]._get_verbose_word(self.config)
982 for num, fspath, lineno, reason in fskips:
983 if reason.startswith("Skipped: "):
984 reason = reason[9:]
985 if lineno is not None:
986 lines.append(
987 "%s [%d] %s:%d: %s"
988 % (verbose_word, num, fspath, lineno, reason)
989 )
990 else:
991 lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason))
993 REPORTCHAR_ACTIONS = {
994 "x": show_xfailed,
995 "X": show_xpassed,
996 "f": partial(show_simple, "failed"),
997 "F": partial(show_simple, "failed"),
998 "s": show_skipped,
999 "S": show_skipped,
1000 "p": partial(show_simple, "passed"),
1001 "E": partial(show_simple, "error"),
1002 } # type: Mapping[str, Callable[[List[str]], None]]
1004 lines = [] # type: List[str]
1005 for char in self.reportchars:
1006 action = REPORTCHAR_ACTIONS.get(char)
1007 if action: # skipping e.g. "P" (passed with output) here.
1008 action(lines)
1010 if lines:
1011 self.write_sep("=", "short test summary info")
1012 for line in lines:
1013 self.write_line(line)
1016def _get_pos(config, rep):
1017 nodeid = config.cwd_relative_nodeid(rep.nodeid)
1018 return nodeid
1021def _get_line_with_reprcrash_message(config, rep, termwidth):
1022 """Get summary line for a report, trying to add reprcrash message."""
1023 from wcwidth import wcswidth
1025 verbose_word = rep._get_verbose_word(config)
1026 pos = _get_pos(config, rep)
1028 line = "{} {}".format(verbose_word, pos)
1029 len_line = wcswidth(line)
1030 ellipsis, len_ellipsis = "...", 3
1031 if len_line > termwidth - len_ellipsis:
1032 # No space for an additional message.
1033 return line
1035 try:
1036 msg = rep.longrepr.reprcrash.message
1037 except AttributeError:
1038 pass
1039 else:
1040 # Only use the first line.
1041 i = msg.find("\n")
1042 if i != -1:
1043 msg = msg[:i]
1044 len_msg = wcswidth(msg)
1046 sep, len_sep = " - ", 3
1047 max_len_msg = termwidth - len_line - len_sep
1048 if max_len_msg >= len_ellipsis:
1049 if len_msg > max_len_msg:
1050 max_len_msg -= len_ellipsis
1051 msg = msg[:max_len_msg]
1052 while wcswidth(msg) > max_len_msg:
1053 msg = msg[:-1]
1054 msg += ellipsis
1055 line += sep + msg
1056 return line
1059def _folded_skips(skipped):
1060 d = {}
1061 for event in skipped:
1062 key = event.longrepr
1063 assert len(key) == 3, (event, key)
1064 keywords = getattr(event, "keywords", {})
1065 # folding reports with global pytestmark variable
1066 # this is workaround, because for now we cannot identify the scope of a skip marker
1067 # TODO: revisit after marks scope would be fixed
1068 if (
1069 event.when == "setup"
1070 and "skip" in keywords
1071 and "pytestmark" not in keywords
1072 ):
1073 key = (key[0], None, key[2])
1074 d.setdefault(key, []).append(event)
1075 values = []
1076 for key, events in d.items():
1077 values.append((len(events),) + key)
1078 return values
1081_color_for_type = {
1082 "failed": "red",
1083 "error": "red",
1084 "warnings": "yellow",
1085 "passed": "green",
1086}
1087_color_for_type_default = "yellow"
1090def _make_plural(count, noun):
1091 # No need to pluralize words such as `failed` or `passed`.
1092 if noun not in ["error", "warnings"]:
1093 return count, noun
1095 # The `warnings` key is plural. To avoid API breakage, we keep it that way but
1096 # set it to singular here so we can determine plurality in the same way as we do
1097 # for `error`.
1098 noun = noun.replace("warnings", "warning")
1100 return count, noun + "s" if count != 1 else noun
1103def _get_main_color(stats) -> Tuple[str, List[str]]:
1104 known_types = (
1105 "failed passed skipped deselected xfailed xpassed warnings error".split()
1106 )
1107 unknown_type_seen = False
1108 for found_type in stats.keys():
1109 if found_type not in known_types:
1110 if found_type: # setup/teardown reports have an empty key, ignore them
1111 known_types.append(found_type)
1112 unknown_type_seen = True
1114 # main color
1115 if "failed" in stats or "error" in stats:
1116 main_color = "red"
1117 elif "warnings" in stats or unknown_type_seen:
1118 main_color = "yellow"
1119 elif "passed" in stats:
1120 main_color = "green"
1121 else:
1122 main_color = "yellow"
1124 return main_color, known_types
1127def build_summary_stats_line(stats):
1128 main_color, known_types = _get_main_color(stats)
1130 parts = []
1131 for key in known_types:
1132 reports = stats.get(key, None)
1133 if reports:
1134 count = sum(
1135 1 for rep in reports if getattr(rep, "count_towards_summary", True)
1136 )
1137 color = _color_for_type.get(key, _color_for_type_default)
1138 markup = {color: True, "bold": color == main_color}
1139 parts.append(("%d %s" % _make_plural(count, key), markup))
1141 if not parts:
1142 parts = [("no tests ran", {_color_for_type_default: True})]
1144 return parts, main_color
1147def _plugin_nameversions(plugininfo) -> List[str]:
1148 values = [] # type: List[str]
1149 for plugin, dist in plugininfo:
1150 # gets us name and version!
1151 name = "{dist.project_name}-{dist.version}".format(dist=dist)
1152 # questionable convenience, but it keeps things short
1153 if name.startswith("pytest-"):
1154 name = name[7:]
1155 # we decided to print python package names
1156 # they can have more than one plugin
1157 if name not in values:
1158 values.append(name)
1159 return values
1162def format_session_duration(seconds: float) -> str:
1163 """Format the given seconds in a human readable manner to show in the final summary"""
1164 if seconds < 60:
1165 return "{:.2f}s".format(seconds)
1166 else:
1167 dt = datetime.timedelta(seconds=int(seconds))
1168 return "{:.2f}s ({})".format(seconds, dt)