microproduct/atmosphericDelay/ISCEApp/site-packages/pytest_sugar.py

647 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# -*- coding: utf-8 -*-
"""
pytest_sugar
~~~~~~~~~~~~
py.test is a plugin for py.test that changes the default look
and feel of py.test (e.g. progressbar, show tests that fail instantly).
:copyright: see LICENSE for details
:license: BSD, see LICENSE for more details.
"""
from __future__ import unicode_literals
import locale
import os
import re
import sys
from packaging.version import parse
try:
from configparser import ConfigParser
except ImportError:
from ConfigParser import ConfigParser
from termcolor import colored
import py
import pytest
from _pytest.terminal import TerminalReporter
__version__ = '0.9.4'
LEN_RIGHT_MARGIN = 0
LEN_PROGRESS_PERCENTAGE = 5
LEN_PROGRESS_BAR_SETTING = '10'
LEN_PROGRESS_BAR = None
THEME = {
'header': 'magenta',
'skipped': 'blue',
'success': 'green',
'warning': 'yellow',
'fail': 'red',
'error': 'red',
'xfailed': 'green',
'xpassed': 'red',
'progressbar': 'green',
'progressbar_fail': 'red',
'progressbar_background': 'grey',
'path': 'cyan',
'name': None,
'symbol_passed': '',
'symbol_skipped': 's',
'symbol_failed': '',
'symbol_failed_not_call': '',
'symbol_xfailed_skipped': 'x',
'symbol_xfailed_failed': 'X',
'symbol_unknown': '?',
'unknown': 'blue',
'symbol_rerun': 'R',
'rerun': 'blue',
}
PROGRESS_BAR_BLOCKS = [
' ', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
]
def flatten(seq):
for x in seq:
if isinstance(x, (list, tuple)):
for y in flatten(x):
yield y
else:
yield x
def pytest_runtestloop(session):
reporter = session.config.pluginmanager.getplugin('terminalreporter')
if reporter:
reporter.tests_count = len(session.items)
class DeferredXdistPlugin(object):
def pytest_xdist_node_collection_finished(self, node, ids):
terminal_reporter = node.config.pluginmanager.getplugin(
'terminalreporter'
)
if terminal_reporter:
terminal_reporter.tests_count = len(ids)
def pytest_deselected(items):
""" Update tests_count to not include deselected tests """
if len(items) > 0:
pluginmanager = items[0].config.pluginmanager
terminal_reporter = pluginmanager.getplugin('terminalreporter')
if (hasattr(terminal_reporter, 'tests_count')
and terminal_reporter.tests_count > 0):
terminal_reporter.tests_count -= len(items)
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting", "reporting", after="general")
group._addoption(
'--old-summary', action="store_true",
dest="tb_summary", default=False,
help=(
"Show tests that failed instead of one-line tracebacks"
)
)
group._addoption(
'--force-sugar', action="store_true",
dest="force_sugar", default=False,
help=(
"Force pytest-sugar output even when not in real terminal"
)
)
def pytest_sessionstart(session):
global LEN_PROGRESS_BAR_SETTING
config = ConfigParser()
config.read([
'pytest-sugar.conf',
os.path.expanduser('~/.pytest-sugar.conf')
])
for key in THEME:
if not config.has_option('theme', key):
continue
value = config.get("theme", key)
value = value.lower()
if value in ('', 'none'):
value = None
THEME[key] = value
if config.has_option('sugar', 'progressbar_length'):
LEN_PROGRESS_BAR_SETTING = config.get('sugar', 'progressbar_length')
def strip_colors(text):
ansi_escape = re.compile(r'\x1b[^m]*m')
stripped = ansi_escape.sub('', text)
return stripped
def real_string_length(string):
return len(strip_colors(string))
IS_SUGAR_ENABLED = False
@pytest.mark.trylast
def pytest_configure(config):
global IS_SUGAR_ENABLED
if sys.stdout.isatty() or config.getvalue('force_sugar'):
IS_SUGAR_ENABLED = True
if config.pluginmanager.hasplugin('xdist'):
try:
import xdist
except ImportError:
pass
else:
from distutils.version import LooseVersion
xdist_version = LooseVersion(xdist.__version__)
if xdist_version >= LooseVersion('1.14'):
config.pluginmanager.register(DeferredXdistPlugin())
if IS_SUGAR_ENABLED and not getattr(config, 'slaveinput', None):
# Get the standard terminal reporter plugin and replace it with our
standard_reporter = config.pluginmanager.getplugin('terminalreporter')
sugar_reporter = SugarTerminalReporter(standard_reporter)
config.pluginmanager.unregister(standard_reporter)
config.pluginmanager.register(sugar_reporter, 'terminalreporter')
def pytest_report_teststatus(report):
if not IS_SUGAR_ENABLED:
return
if report.passed:
letter = colored(THEME['symbol_passed'], THEME['success'])
elif report.skipped:
letter = colored(THEME['symbol_skipped'], THEME['skipped'])
elif report.failed:
letter = colored(THEME['symbol_failed'], THEME['fail'])
if report.when != "call":
letter = colored(THEME['symbol_failed_not_call'], THEME['fail'])
elif report.outcome == 'rerun':
letter = colored(THEME['symbol_rerun'], THEME['rerun'])
else:
letter = colored(THEME['symbol_unknown'], THEME['unknown'])
if hasattr(report, "wasxfail"):
if report.skipped:
return "xfailed", colored(
THEME['symbol_xfailed_skipped'], THEME['xfailed']
), "xfail"
elif report.passed:
return "xpassed", colored(
THEME['symbol_xfailed_failed'], THEME['xpassed']
), "XPASS"
return report.outcome, letter, report.outcome.upper()
class SugarTerminalReporter(TerminalReporter):
def __init__(self, reporter):
TerminalReporter.__init__(self, reporter.config)
self.paths_left = []
self.tests_count = 0
self.tests_taken = 0
self.reports = []
self.unreported_errors = []
self.progress_blocks = []
self.reset_tracked_lines()
def reset_tracked_lines(self):
self.current_lines = {}
self.current_line_nums = {}
self.current_line_num = 0
def report_collect(self, final=False):
pass
def pytest_collectreport(self, report):
TerminalReporter.pytest_collectreport(self, report)
if report.location[0]:
self.paths_left.append(
os.path.join(os.getcwd(), report.location[0])
)
if report.failed:
self.rewrite("")
self.print_failure(report)
def pytest_sessionstart(self, session):
self._session = session
self._sessionstarttime = py.std.time.time()
verinfo = ".".join(map(str, sys.version_info[:3]))
self.write_line(
"Test session starts "
"(platform: %s, Python %s, pytest %s, pytest-sugar %s)" % (
sys.platform, verinfo, pytest.__version__, __version__,
), bold=True
)
lines = self.config.hook.pytest_report_header(
config=self.config, startdir=self.startdir)
lines.reverse()
for line in flatten(lines):
self.write_line(line)
def write_fspath_result(self, fspath, res):
return
def insert_progress(self, report):
def get_progress_bar():
length = LEN_PROGRESS_BAR
if not length:
return ''
p = (
float(self.tests_taken) / self.tests_count
if self.tests_count else 0
)
floored = int(p * length)
rem = int(round(
(p * length - floored) * (len(PROGRESS_BAR_BLOCKS) - 1)
))
progressbar = "%i%% " % round(p * 100)
# make sure we only report 100% at the last test
if progressbar == "100% " and self.tests_taken < self.tests_count:
progressbar = "99% "
# if at least one block indicates failure,
# then the percentage should reflect that
if [1 for block, success in self.progress_blocks if not success]:
progressbar = colored(progressbar, THEME['fail'])
else:
progressbar = colored(progressbar, THEME['success'])
bar = PROGRESS_BAR_BLOCKS[-1] * floored
if rem > 0:
bar += PROGRESS_BAR_BLOCKS[rem]
bar += ' ' * (LEN_PROGRESS_BAR - len(bar))
last = 0
last_theme = None
progressbar_background = THEME['progressbar_background']
if progressbar_background is None:
on_color = None
else:
on_color = 'on_' + progressbar_background
for block, success in self.progress_blocks:
if success:
theme = THEME['progressbar']
else:
theme = THEME['progressbar_fail']
if last < block:
progressbar += colored(bar[last:block],
last_theme,
on_color)
progressbar += colored(bar[block],
theme,
on_color)
last = block + 1
last_theme = theme
if last < len(bar):
progressbar += colored(bar[last:len(bar)],
last_theme,
on_color)
return progressbar
append_string = get_progress_bar()
path = self.report_key(report)
current_line = self.current_lines.get(path, "")
line_num = self.current_line_nums.get(path, self.current_line_num)
console_width = self._tw.fullwidth
num_spaces = (
console_width - real_string_length(current_line) -
real_string_length(append_string) - LEN_RIGHT_MARGIN
)
full_line = current_line + " " * num_spaces
full_line += append_string
self.overwrite(full_line, self.current_line_num - line_num)
def overwrite(self, line, rel_line_num):
# Move cursor up rel_line_num lines
if rel_line_num > 0:
self.write("\033[%dA" % rel_line_num)
# Overwrite the line
self.write("\r%s" % line)
# Return cursor to original line
if rel_line_num > 0:
self.write("\033[%dB" % rel_line_num)
def get_max_column_for_test_status(self):
return (
self._tw.fullwidth
- LEN_PROGRESS_PERCENTAGE
- LEN_PROGRESS_BAR
- LEN_RIGHT_MARGIN
)
def begin_new_line(self, report, print_filename):
path = self.report_key(report)
self.current_line_num += 1
if len(report.fspath) > self.get_max_column_for_test_status() - 5:
fspath = '...' + report.fspath[
-(self.get_max_column_for_test_status() - 5 - 5):
]
else:
fspath = report.fspath
basename = os.path.basename(fspath)
if print_filename:
if self.showlongtestinfo:
test_location = report.location[0]
test_name = report.location[2]
else:
test_location = fspath[0:-len(basename)]
test_name = fspath[-len(basename):]
if test_location:
pass
# only replace if test_location is not empty, if it is,
# test_name contains the filename
# FIXME: This doesn't work.
# test_name = test_name.replace('.', '::')
self.current_lines[path] = (
" " +
colored(test_location, THEME['path']) +
("::" if self.verbosity > 0 else "") +
colored(test_name, THEME['name']) +
" "
)
else:
self.current_lines[path] = " " * (2 + len(fspath))
self.current_line_nums[path] = self.current_line_num
self.write("\r\n")
def reached_last_column_for_test_status(self, report):
len_line = real_string_length(
self.current_lines[self.report_key(report)])
return len_line >= self.get_max_column_for_test_status()
def pytest_runtest_logstart(self, nodeid, location):
# Prevent locationline from being printed since we already
# show the module_name & in verbose mode the test name.
pass
def pytest_runtest_logfinish(self):
# prevent the default implementation to try to show
# pytest's default progress
pass
def report_key(self, report):
"""Returns a key to identify which line the report should write to."""
return report.location if self.showlongtestinfo else report.fspath
def pytest_runtest_logreport(self, report):
global LEN_PROGRESS_BAR_SETTING, LEN_PROGRESS_BAR
res = pytest_report_teststatus(report=report)
cat, letter, word = res
self.stats.setdefault(cat, []).append(report)
if not LEN_PROGRESS_BAR:
if LEN_PROGRESS_BAR_SETTING.endswith('%'):
LEN_PROGRESS_BAR = (
self._tw.fullwidth *
int(LEN_PROGRESS_BAR_SETTING[:-1]) // 100
)
else:
LEN_PROGRESS_BAR = int(LEN_PROGRESS_BAR_SETTING)
self.reports.append(report)
if report.outcome == 'failed':
print("")
self.print_failure(report)
# Ignore other reports or it will cause duplicated letters
if report.when == 'teardown':
self.tests_taken += 1
self.insert_progress(report)
path = os.path.join(os.getcwd(), report.location[0])
if report.when == 'call' or report.skipped:
path = self.report_key(report)
if path not in self.current_line_nums:
self.begin_new_line(report, print_filename=True)
elif self.reached_last_column_for_test_status(report):
# Print filename if another line was inserted in-between
print_filename = (
self.current_line_nums[self.report_key(report)] !=
self.current_line_num)
self.begin_new_line(report, print_filename)
self.current_lines[path] = self.current_lines[path] + letter
block = int(
float(self.tests_taken) * LEN_PROGRESS_BAR / self.tests_count
if self.tests_count else 0
)
if report.failed:
if (
not self.progress_blocks or
self.progress_blocks[-1][0] != block
):
self.progress_blocks.append([block, False])
elif (
self.progress_blocks and
self.progress_blocks[-1][0] == block
):
self.progress_blocks[-1][1] = False
else:
if (
not self.progress_blocks or
self.progress_blocks[-1][0] != block
):
self.progress_blocks.append([block, True])
if not letter and not word:
return
if self.verbosity > 0:
if isinstance(word, tuple):
word, markup = word
else:
if report.passed:
markup = {'green': True}
elif report.failed:
markup = {'red': True}
elif report.skipped:
markup = {'yellow': True}
line = self._locationline(str(report.fspath), *report.location)
if hasattr(report, 'node'):
self._tw.write("\r\n")
self.current_line_num += 1
if hasattr(report, 'node'):
self._tw.write("[%s] " % report.node.gateway.id)
self._tw.write(word, **markup)
self._tw.write(" " + line)
self.currentfspath = -2
def count(self, key, when=('call',)):
if self.stats.get(key):
return len([
x for x in self.stats.get(key)
if not hasattr(x, 'when') or x.when in when
])
else:
return 0
def summary_stats(self):
session_duration = py.std.time.time() - self._sessionstarttime
print("\nResults (%.2fs):" % round(session_duration, 2))
if self.count('passed') > 0:
self.write_line(colored(
" % 5d passed" % self.count('passed'),
THEME['success']
))
if self.count('xpassed') > 0:
self.write_line(colored(
" % 5d xpassed" % self.count('xpassed'),
THEME['xpassed']
))
if self.count('failed', when=['call']) > 0:
self.write_line(colored(
" % 5d failed" % self.count('failed', when=['call']),
THEME['fail']
))
for report in self.stats['failed']:
if report.when != 'call':
continue
if self.config.option.tb_summary:
crashline = self._get_decoded_crashline(report)
else:
path = os.path.dirname(report.location[0])
name = os.path.basename(report.location[0])
lineno = self._get_lineno_from_report(report)
crashline = '%s%s%s:%s %s' % (
colored(path, THEME['path']),
'/' if path else '',
colored(name, THEME['name']),
lineno if lineno else '?',
colored(report.location[2], THEME['fail'])
)
self.write_line(" - %s" % crashline)
if self.count('failed', when=['setup', 'teardown']) > 0:
self.write_line(colored(
" % 5d error" % (
self.count('failed', when=['setup', 'teardown'])
),
THEME['error']
))
if self.count('xfailed') > 0:
self.write_line(colored(
" % 5d xfailed" % self.count('xfailed'),
THEME['xfailed']
))
if self.count('skipped', when=['call', 'setup', 'teardown']) > 0:
self.write_line(colored(
" % 5d skipped" % (
self.count('skipped', when=['call', 'setup', 'teardown'])
),
THEME['skipped']
))
if self.count('rerun') > 0:
self.write_line(colored(
" % 5d rerun" % self.count('rerun'),
THEME['rerun']
))
if self.count('deselected') > 0:
self.write_line(colored(
" % 5d deselected" % self.count('deselected'),
THEME['warning']
))
def _get_decoded_crashline(self, report):
crashline = self._getcrashline(report)
if hasattr(crashline, 'decode'):
encoding = locale.getpreferredencoding()
try:
crashline = crashline.decode(encoding)
except UnicodeDecodeError:
encoding = 'utf-8'
crashline = crashline.decode(encoding, errors='replace')
return crashline
def _get_lineno_from_report(self, report):
# Doctest failures in pytest>3.10 are stored in
# reprlocation_lines, a list of (ReprFileLocation, lines)
try:
location, lines = report.longrepr.reprlocation_lines[0]
return location.lineno
except AttributeError:
pass
# Doctest failure reports have lineno=None at least up to
# pytest==3.0.7, but it is available via longrepr object.
try:
return report.longrepr.reprlocation.lineno
except AttributeError:
lineno = report.location[1]
if lineno is not None:
lineno += 1
return lineno
def summary_failures(self):
# Prevent failure summary from being shown since we already
# show the failure instantly after failure has occurred.
pass
def summary_errors(self):
# Prevent error summary from being shown since we already
# show the error instantly after error has occurred.
pass
def print_failure(self, report):
# https://github.com/Frozenball/pytest-sugar/issues/34
if hasattr(report, 'wasxfail'):
return
if self.config.option.tbstyle != "no":
if self.config.option.tbstyle == "line":
line = self._getcrashline(report)
self.write_line(line)
else:
msg = self._getfailureheadline(report)
# "when" was unset before pytest 4.2 for collection errors.
when = getattr(report, "when", "collect")
if when == "collect":
msg = "ERROR collecting " + msg
elif when == "setup":
msg = "ERROR at setup of " + msg
elif when == "teardown":
msg = "ERROR at teardown of " + msg
self.write_line('')
self.write_sep("", msg)
self._outrep_summary(report)
self.reset_tracked_lines()
# On older version of Pytest, allow default progress
if parse(pytest.__version__) <= parse('3.4'): # pragma: no cover
del SugarTerminalReporter.pytest_runtest_logfinish