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