updates
This commit is contained in:
@@ -1,41 +1,2 @@
|
||||
"""pytest-cov: avoid already-imported warning: PYTEST_DONT_REWRITE."""
|
||||
|
||||
__version__ = '7.0.0'
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class CoverageError(Exception):
|
||||
"""Indicates that our coverage is too low"""
|
||||
|
||||
|
||||
class PytestCovWarning(pytest.PytestWarning):
|
||||
"""
|
||||
The base for all pytest-cov warnings, never raised directly.
|
||||
"""
|
||||
|
||||
|
||||
class CovDisabledWarning(PytestCovWarning):
|
||||
"""
|
||||
Indicates that Coverage was manually disabled.
|
||||
"""
|
||||
|
||||
|
||||
class CovReportWarning(PytestCovWarning):
|
||||
"""
|
||||
Indicates that we failed to generate a report.
|
||||
"""
|
||||
|
||||
|
||||
class CentralCovContextWarning(PytestCovWarning):
|
||||
"""
|
||||
Indicates that dynamic_context was set to test_function instead of using the builtin --cov-context.
|
||||
"""
|
||||
|
||||
|
||||
class DistCovError(Exception):
|
||||
"""
|
||||
Raised when dynamic_context is set to test_function and xdist is also used.
|
||||
|
||||
See: https://github.com/pytest-dev/pytest-cov/issues/604
|
||||
"""
|
||||
__version__ = '4.1.0'
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except ImportError:
|
||||
from io import StringIO
|
||||
|
||||
|
||||
StringIO # pyflakes, this is for re-export
|
||||
|
||||
|
||||
class SessionWrapper:
|
||||
def __init__(self, session):
|
||||
self._session = session
|
||||
if hasattr(session, 'testsfailed'):
|
||||
self._attr = 'testsfailed'
|
||||
else:
|
||||
self._attr = '_testsfailed'
|
||||
|
||||
@property
|
||||
def testsfailed(self):
|
||||
return getattr(self._session, self._attr)
|
||||
|
||||
@testsfailed.setter
|
||||
def testsfailed(self, value):
|
||||
setattr(self._session, self._attr, value)
|
||||
122
Backend/venv/lib/python3.12/site-packages/pytest_cov/embed.py
Normal file
122
Backend/venv/lib/python3.12/site-packages/pytest_cov/embed.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""Activate coverage at python startup if appropriate.
|
||||
|
||||
The python site initialisation will ensure that anything we import
|
||||
will be removed and not visible at the end of python startup. However
|
||||
we minimise all work by putting these init actions in this separate
|
||||
module and only importing what is needed when needed.
|
||||
|
||||
For normal python startup when coverage should not be activated the pth
|
||||
file checks a single env var and does not import or call the init fn
|
||||
here.
|
||||
|
||||
For python startup when an ancestor process has set the env indicating
|
||||
that code coverage is being collected we activate coverage based on
|
||||
info passed via env vars.
|
||||
"""
|
||||
import atexit
|
||||
import os
|
||||
import signal
|
||||
|
||||
_active_cov = None
|
||||
|
||||
|
||||
def init():
|
||||
# Only continue if ancestor process has set everything needed in
|
||||
# the env.
|
||||
global _active_cov
|
||||
|
||||
cov_source = os.environ.get('COV_CORE_SOURCE')
|
||||
cov_config = os.environ.get('COV_CORE_CONFIG')
|
||||
cov_datafile = os.environ.get('COV_CORE_DATAFILE')
|
||||
cov_branch = True if os.environ.get('COV_CORE_BRANCH') == 'enabled' else None
|
||||
cov_context = os.environ.get('COV_CORE_CONTEXT')
|
||||
|
||||
if cov_datafile:
|
||||
if _active_cov:
|
||||
cleanup()
|
||||
# Import what we need to activate coverage.
|
||||
import coverage
|
||||
|
||||
# Determine all source roots.
|
||||
if cov_source in os.pathsep:
|
||||
cov_source = None
|
||||
else:
|
||||
cov_source = cov_source.split(os.pathsep)
|
||||
if cov_config == os.pathsep:
|
||||
cov_config = True
|
||||
|
||||
# Activate coverage for this process.
|
||||
cov = _active_cov = coverage.Coverage(
|
||||
source=cov_source,
|
||||
branch=cov_branch,
|
||||
data_suffix=True,
|
||||
config_file=cov_config,
|
||||
auto_data=True,
|
||||
data_file=cov_datafile
|
||||
)
|
||||
cov.load()
|
||||
cov.start()
|
||||
if cov_context:
|
||||
cov.switch_context(cov_context)
|
||||
cov._warn_no_data = False
|
||||
cov._warn_unimported_source = False
|
||||
return cov
|
||||
|
||||
|
||||
def _cleanup(cov):
|
||||
if cov is not None:
|
||||
cov.stop()
|
||||
cov.save()
|
||||
cov._auto_save = False # prevent autosaving from cov._atexit in case the interpreter lacks atexit.unregister
|
||||
try:
|
||||
atexit.unregister(cov._atexit)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def cleanup():
|
||||
global _active_cov
|
||||
global _cleanup_in_progress
|
||||
global _pending_signal
|
||||
|
||||
_cleanup_in_progress = True
|
||||
_cleanup(_active_cov)
|
||||
_active_cov = None
|
||||
_cleanup_in_progress = False
|
||||
if _pending_signal:
|
||||
pending_signal = _pending_signal
|
||||
_pending_signal = None
|
||||
_signal_cleanup_handler(*pending_signal)
|
||||
|
||||
|
||||
_previous_handlers = {}
|
||||
_pending_signal = None
|
||||
_cleanup_in_progress = False
|
||||
|
||||
|
||||
def _signal_cleanup_handler(signum, frame):
|
||||
global _pending_signal
|
||||
if _cleanup_in_progress:
|
||||
_pending_signal = signum, frame
|
||||
return
|
||||
cleanup()
|
||||
_previous_handler = _previous_handlers.get(signum)
|
||||
if _previous_handler == signal.SIG_IGN:
|
||||
return
|
||||
elif _previous_handler and _previous_handler is not _signal_cleanup_handler:
|
||||
_previous_handler(signum, frame)
|
||||
elif signum == signal.SIGTERM:
|
||||
os._exit(128 + signum)
|
||||
elif signum == signal.SIGINT:
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
|
||||
def cleanup_on_signal(signum):
|
||||
previous = signal.getsignal(signum)
|
||||
if previous is not _signal_cleanup_handler:
|
||||
_previous_handlers[signum] = previous
|
||||
signal.signal(signum, _signal_cleanup_handler)
|
||||
|
||||
|
||||
def cleanup_on_sigterm():
|
||||
cleanup_on_signal(signal.SIGTERM)
|
||||
@@ -1,28 +1,17 @@
|
||||
"""Coverage controllers for use by pytest-cov and nose-cov."""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import copy
|
||||
import functools
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import socket
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
import coverage
|
||||
from coverage.data import CoverageData
|
||||
from coverage.sqldata import filename_suffix
|
||||
|
||||
from . import CentralCovContextWarning
|
||||
from . import DistCovError
|
||||
|
||||
|
||||
class BrokenCovConfigError(Exception):
|
||||
pass
|
||||
from .compat import StringIO
|
||||
from .embed import cleanup
|
||||
|
||||
|
||||
class _NullFile:
|
||||
@@ -45,7 +34,7 @@ def _ensure_topdir(meth):
|
||||
@functools.wraps(meth)
|
||||
def ensure_topdir_wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
original_cwd = Path.cwd()
|
||||
original_cwd = os.getcwd()
|
||||
except OSError:
|
||||
# Looks like it's gone, this is non-ideal because a side-effect will
|
||||
# be introduced in the tests here but we can't do anything about it.
|
||||
@@ -63,14 +52,13 @@ def _ensure_topdir(meth):
|
||||
class CovController:
|
||||
"""Base class for different plugin implementations."""
|
||||
|
||||
def __init__(self, options: argparse.Namespace, config: Union[None, object], nodeid: Union[None, str]):
|
||||
def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, config=None, nodeid=None):
|
||||
"""Get some common config used by multiple derived classes."""
|
||||
self.cov_source = options.cov_source
|
||||
self.cov_report = options.cov_report
|
||||
self.cov_config = options.cov_config
|
||||
self.cov_append = options.cov_append
|
||||
self.cov_branch = options.cov_branch
|
||||
self.cov_precision = options.cov_precision
|
||||
self.cov_source = cov_source
|
||||
self.cov_report = cov_report
|
||||
self.cov_config = cov_config
|
||||
self.cov_append = cov_append
|
||||
self.cov_branch = cov_branch
|
||||
self.config = config
|
||||
self.nodeid = nodeid
|
||||
|
||||
@@ -79,73 +67,67 @@ class CovController:
|
||||
self.data_file = None
|
||||
self.node_descs = set()
|
||||
self.failed_workers = []
|
||||
self.topdir = os.fspath(Path.cwd())
|
||||
self.topdir = os.getcwd()
|
||||
self.is_collocated = None
|
||||
self.started = False
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ensure_topdir(self):
|
||||
original_cwd = Path.cwd()
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(self.topdir)
|
||||
yield
|
||||
os.chdir(original_cwd)
|
||||
|
||||
@_ensure_topdir
|
||||
def pause(self):
|
||||
self.started = False
|
||||
self.cov.stop()
|
||||
self.unset_env()
|
||||
|
||||
@_ensure_topdir
|
||||
def resume(self):
|
||||
self.cov.start()
|
||||
self.started = True
|
||||
self.set_env()
|
||||
|
||||
def start(self):
|
||||
self.started = True
|
||||
@_ensure_topdir
|
||||
def set_env(self):
|
||||
"""Put info about coverage into the env so that subprocesses can activate coverage."""
|
||||
if self.cov_source is None:
|
||||
os.environ['COV_CORE_SOURCE'] = os.pathsep
|
||||
else:
|
||||
os.environ['COV_CORE_SOURCE'] = os.pathsep.join(self.cov_source)
|
||||
config_file = os.path.abspath(self.cov_config)
|
||||
if os.path.exists(config_file):
|
||||
os.environ['COV_CORE_CONFIG'] = config_file
|
||||
else:
|
||||
os.environ['COV_CORE_CONFIG'] = os.pathsep
|
||||
os.environ['COV_CORE_DATAFILE'] = os.path.abspath(self.cov.config.data_file)
|
||||
if self.cov_branch:
|
||||
os.environ['COV_CORE_BRANCH'] = 'enabled'
|
||||
|
||||
def finish(self):
|
||||
self.started = False
|
||||
@staticmethod
|
||||
def unset_env():
|
||||
"""Remove coverage info from env."""
|
||||
os.environ.pop('COV_CORE_SOURCE', None)
|
||||
os.environ.pop('COV_CORE_CONFIG', None)
|
||||
os.environ.pop('COV_CORE_DATAFILE', None)
|
||||
os.environ.pop('COV_CORE_BRANCH', None)
|
||||
os.environ.pop('COV_CORE_CONTEXT', None)
|
||||
|
||||
@staticmethod
|
||||
def get_node_desc(platform, version_info):
|
||||
"""Return a description of this node."""
|
||||
|
||||
return 'platform {}, python {}'.format(platform, '{}.{}.{}-{}-{}'.format(*version_info[:5]))
|
||||
return 'platform {}, python {}'.format(platform, '%s.%s.%s-%s-%s' % version_info[:5])
|
||||
|
||||
@staticmethod
|
||||
def get_width():
|
||||
# taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L26
|
||||
width, _ = shutil.get_terminal_size(fallback=(80, 24))
|
||||
# The Windows get_terminal_size may be bogus, let's sanify a bit.
|
||||
if width < 40:
|
||||
width = 80
|
||||
return width
|
||||
|
||||
def sep(self, stream, s, txt):
|
||||
def sep(stream, s, txt):
|
||||
if hasattr(stream, 'sep'):
|
||||
stream.sep(s, txt)
|
||||
else:
|
||||
fullwidth = self.get_width()
|
||||
# taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L126
|
||||
# The goal is to have the line be as long as possible
|
||||
# under the condition that len(line) <= fullwidth.
|
||||
if sys.platform == 'win32':
|
||||
# If we print in the last column on windows we are on a
|
||||
# new line but there is no way to verify/neutralize this
|
||||
# (we may not know the exact line width).
|
||||
# So let's be defensive to avoid empty lines in the output.
|
||||
fullwidth -= 1
|
||||
N = max((fullwidth - len(txt) - 2) // (2 * len(s)), 1)
|
||||
fill = s * N
|
||||
line = f'{fill} {txt} {fill}'
|
||||
# In some situations there is room for an extra sepchar at the right,
|
||||
# in particular if we consider that with a sepchar like "_ " the
|
||||
# trailing space is not important at the end of the line.
|
||||
if len(line) + len(s.rstrip()) <= fullwidth:
|
||||
line += s.rstrip()
|
||||
# (end of terminalwriter borrowed code)
|
||||
line += '\n\n'
|
||||
stream.write(line)
|
||||
sep_total = max((70 - 2 - len(txt)), 2)
|
||||
sep_len = sep_total // 2
|
||||
sep_extra = sep_total % 2
|
||||
out = f'{s * sep_len} {txt} {s * (sep_len + sep_extra)}\n'
|
||||
stream.write(out)
|
||||
|
||||
@_ensure_topdir
|
||||
def summary(self, stream):
|
||||
@@ -153,21 +135,22 @@ class CovController:
|
||||
total = None
|
||||
|
||||
if not self.cov_report:
|
||||
with _backup(self.cov, 'config'):
|
||||
with _backup(self.cov, "config"):
|
||||
return self.cov.report(show_missing=True, ignore_errors=True, file=_NullFile)
|
||||
|
||||
# Output coverage section header.
|
||||
if len(self.node_descs) == 1:
|
||||
self.sep(stream, '_', f'coverage: {"".join(self.node_descs)}')
|
||||
self.sep(stream, '-', f"coverage: {''.join(self.node_descs)}")
|
||||
else:
|
||||
self.sep(stream, '_', 'coverage')
|
||||
self.sep(stream, '-', 'coverage')
|
||||
for node_desc in sorted(self.node_descs):
|
||||
self.sep(stream, ' ', f'{node_desc}')
|
||||
|
||||
# Report on any failed workers.
|
||||
if self.failed_workers:
|
||||
self.sep(stream, '_', 'coverage: failed workers')
|
||||
stream.write('The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.\n')
|
||||
self.sep(stream, '-', 'coverage: failed workers')
|
||||
stream.write('The following workers failed to return coverage data, '
|
||||
'ensure that pytest-cov is installed on these workers.\n')
|
||||
for node in self.failed_workers:
|
||||
stream.write(f'{node.gateway.id}\n')
|
||||
|
||||
@@ -177,23 +160,22 @@ class CovController:
|
||||
'show_missing': ('term-missing' in self.cov_report) or None,
|
||||
'ignore_errors': True,
|
||||
'file': stream,
|
||||
'precision': self.cov_precision,
|
||||
}
|
||||
skip_covered = isinstance(self.cov_report, dict) and 'skip-covered' in self.cov_report.values()
|
||||
options.update({'skip_covered': skip_covered or None})
|
||||
with _backup(self.cov, 'config'):
|
||||
with _backup(self.cov, "config"):
|
||||
total = self.cov.report(**options)
|
||||
|
||||
# Produce annotated source code report if wanted.
|
||||
if 'annotate' in self.cov_report:
|
||||
annotate_dir = self.cov_report['annotate']
|
||||
|
||||
with _backup(self.cov, 'config'):
|
||||
with _backup(self.cov, "config"):
|
||||
self.cov.annotate(ignore_errors=True, directory=annotate_dir)
|
||||
# We need to call Coverage.report here, just to get the total
|
||||
# Coverage.annotate don't return any total and we need it for --cov-fail-under.
|
||||
|
||||
with _backup(self.cov, 'config'):
|
||||
with _backup(self.cov, "config"):
|
||||
total = self.cov.report(ignore_errors=True, file=_NullFile)
|
||||
if annotate_dir:
|
||||
stream.write(f'Coverage annotated source written to dir {annotate_dir}\n')
|
||||
@@ -203,44 +185,28 @@ class CovController:
|
||||
# Produce html report if wanted.
|
||||
if 'html' in self.cov_report:
|
||||
output = self.cov_report['html']
|
||||
with _backup(self.cov, 'config'):
|
||||
with _backup(self.cov, "config"):
|
||||
total = self.cov.html_report(ignore_errors=True, directory=output)
|
||||
stream.write(f'Coverage HTML written to dir {self.cov.config.html_dir if output is None else output}\n')
|
||||
|
||||
# Produce xml report if wanted.
|
||||
if 'xml' in self.cov_report:
|
||||
output = self.cov_report['xml']
|
||||
with _backup(self.cov, 'config'):
|
||||
with _backup(self.cov, "config"):
|
||||
total = self.cov.xml_report(ignore_errors=True, outfile=output)
|
||||
stream.write(f'Coverage XML written to file {self.cov.config.xml_output if output is None else output}\n')
|
||||
|
||||
# Produce json report if wanted
|
||||
if 'json' in self.cov_report:
|
||||
output = self.cov_report['json']
|
||||
with _backup(self.cov, 'config'):
|
||||
with _backup(self.cov, "config"):
|
||||
total = self.cov.json_report(ignore_errors=True, outfile=output)
|
||||
stream.write('Coverage JSON written to file %s\n' % (self.cov.config.json_output if output is None else output))
|
||||
|
||||
# Produce Markdown report if wanted.
|
||||
if 'markdown' in self.cov_report:
|
||||
output = self.cov_report['markdown']
|
||||
with _backup(self.cov, 'config'):
|
||||
with Path(output).open('w') as output_file:
|
||||
total = self.cov.report(ignore_errors=True, file=output_file, output_format='markdown')
|
||||
stream.write(f'Coverage Markdown information written to file {output}\n')
|
||||
|
||||
# Produce Markdown report if wanted, appending to output file
|
||||
if 'markdown-append' in self.cov_report:
|
||||
output = self.cov_report['markdown-append']
|
||||
with _backup(self.cov, 'config'):
|
||||
with Path(output).open('a') as output_file:
|
||||
total = self.cov.report(ignore_errors=True, file=output_file, output_format='markdown')
|
||||
stream.write(f'Coverage Markdown information appended to file {output}\n')
|
||||
|
||||
# Produce lcov report if wanted.
|
||||
if 'lcov' in self.cov_report:
|
||||
output = self.cov_report['lcov']
|
||||
with _backup(self.cov, 'config'):
|
||||
with _backup(self.cov, "config"):
|
||||
self.cov.lcov_report(ignore_errors=True, outfile=output)
|
||||
|
||||
# We need to call Coverage.report here, just to get the total
|
||||
@@ -257,39 +223,29 @@ class Central(CovController):
|
||||
|
||||
@_ensure_topdir
|
||||
def start(self):
|
||||
self.cov = coverage.Coverage(
|
||||
source=self.cov_source,
|
||||
branch=self.cov_branch,
|
||||
data_suffix=True,
|
||||
config_file=self.cov_config,
|
||||
)
|
||||
if self.cov.config.dynamic_context == 'test_function':
|
||||
message = (
|
||||
'Detected dynamic_context=test_function in coverage configuration. '
|
||||
'This is unnecessary as this plugin provides the more complete --cov-context option.'
|
||||
)
|
||||
warnings.warn(CentralCovContextWarning(message), stacklevel=1)
|
||||
cleanup()
|
||||
|
||||
self.combining_cov = coverage.Coverage(
|
||||
source=self.cov_source,
|
||||
branch=self.cov_branch,
|
||||
data_suffix=f'{filename_suffix(True)}.combine',
|
||||
data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100
|
||||
config_file=self.cov_config,
|
||||
)
|
||||
self.cov = coverage.Coverage(source=self.cov_source,
|
||||
branch=self.cov_branch,
|
||||
data_suffix=True,
|
||||
config_file=self.cov_config)
|
||||
self.combining_cov = coverage.Coverage(source=self.cov_source,
|
||||
branch=self.cov_branch,
|
||||
data_suffix=True,
|
||||
data_file=os.path.abspath(self.cov.config.data_file),
|
||||
config_file=self.cov_config)
|
||||
|
||||
# Erase or load any previous coverage data and start coverage.
|
||||
if not self.cov_append:
|
||||
self.cov.erase()
|
||||
self.cov.start()
|
||||
|
||||
super().start()
|
||||
self.set_env()
|
||||
|
||||
@_ensure_topdir
|
||||
def finish(self):
|
||||
"""Stop coverage, save data to file and set the list of coverage objects to report on."""
|
||||
super().finish()
|
||||
|
||||
self.unset_env()
|
||||
self.cov.stop()
|
||||
self.cov.save()
|
||||
|
||||
@@ -307,28 +263,26 @@ class DistMaster(CovController):
|
||||
|
||||
@_ensure_topdir
|
||||
def start(self):
|
||||
self.cov = coverage.Coverage(
|
||||
source=self.cov_source,
|
||||
branch=self.cov_branch,
|
||||
data_suffix=True,
|
||||
config_file=self.cov_config,
|
||||
)
|
||||
if self.cov.config.dynamic_context == 'test_function':
|
||||
raise DistCovError(
|
||||
'Detected dynamic_context=test_function in coverage configuration. '
|
||||
'This is known to cause issues when using xdist, see: https://github.com/pytest-dev/pytest-cov/issues/604\n'
|
||||
'It is recommended to use --cov-context instead.'
|
||||
)
|
||||
cleanup()
|
||||
|
||||
# Ensure coverage rc file rsynced if appropriate.
|
||||
if self.cov_config and os.path.exists(self.cov_config):
|
||||
# rsyncdir is going away in pytest-xdist 4.0, already deprecated
|
||||
if hasattr(self.config.option, 'rsyncdir'):
|
||||
self.config.option.rsyncdir.append(self.cov_config)
|
||||
|
||||
self.cov = coverage.Coverage(source=self.cov_source,
|
||||
branch=self.cov_branch,
|
||||
data_suffix=True,
|
||||
config_file=self.cov_config)
|
||||
self.cov._warn_no_data = False
|
||||
self.cov._warn_unimported_source = False
|
||||
self.cov._warn_preimported_source = False
|
||||
self.combining_cov = coverage.Coverage(
|
||||
source=self.cov_source,
|
||||
branch=self.cov_branch,
|
||||
data_suffix=f'{filename_suffix(True)}.combine',
|
||||
data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100
|
||||
config_file=self.cov_config,
|
||||
)
|
||||
self.combining_cov = coverage.Coverage(source=self.cov_source,
|
||||
branch=self.cov_branch,
|
||||
data_suffix=True,
|
||||
data_file=os.path.abspath(self.cov.config.data_file),
|
||||
config_file=self.cov_config)
|
||||
if not self.cov_append:
|
||||
self.cov.erase()
|
||||
self.cov.start()
|
||||
@@ -337,13 +291,11 @@ class DistMaster(CovController):
|
||||
def configure_node(self, node):
|
||||
"""Workers need to know if they are collocated and what files have moved."""
|
||||
|
||||
node.workerinput.update(
|
||||
{
|
||||
'cov_master_host': socket.gethostname(),
|
||||
'cov_master_topdir': self.topdir,
|
||||
'cov_master_rsync_roots': [str(root) for root in node.nodemanager.roots],
|
||||
}
|
||||
)
|
||||
node.workerinput.update({
|
||||
'cov_master_host': socket.gethostname(),
|
||||
'cov_master_topdir': self.topdir,
|
||||
'cov_master_rsync_roots': [str(root) for root in node.nodemanager.roots],
|
||||
})
|
||||
|
||||
def testnodedown(self, node, error):
|
||||
"""Collect data file name from worker."""
|
||||
@@ -358,17 +310,27 @@ class DistMaster(CovController):
|
||||
# If worker is not collocated then we must save the data file
|
||||
# that it returns to us.
|
||||
if 'cov_worker_data' in output:
|
||||
data_suffix = '%s.%s.%06d.%s' % ( # noqa: UP031
|
||||
socket.gethostname(),
|
||||
os.getpid(),
|
||||
random.randint(0, 999999), # noqa: S311
|
||||
output['cov_worker_node_id'],
|
||||
data_suffix = '%s.%s.%06d.%s' % (
|
||||
socket.gethostname(), os.getpid(),
|
||||
random.randint(0, 999999),
|
||||
output['cov_worker_node_id']
|
||||
)
|
||||
|
||||
cov_data = CoverageData(
|
||||
suffix=data_suffix,
|
||||
)
|
||||
cov_data.loads(output['cov_worker_data'])
|
||||
cov = coverage.Coverage(source=self.cov_source,
|
||||
branch=self.cov_branch,
|
||||
data_suffix=data_suffix,
|
||||
config_file=self.cov_config)
|
||||
cov.start()
|
||||
if coverage.version_info < (5, 0):
|
||||
data = CoverageData()
|
||||
data.read_fileobj(StringIO(output['cov_worker_data']))
|
||||
cov.data.update(data)
|
||||
else:
|
||||
data = CoverageData(no_disk=True)
|
||||
data.loads(output['cov_worker_data'])
|
||||
cov.get_data().update(data)
|
||||
cov.stop()
|
||||
cov.save()
|
||||
path = output['cov_worker_path']
|
||||
self.cov.config.paths['source'].append(path)
|
||||
|
||||
@@ -395,38 +357,34 @@ class DistWorker(CovController):
|
||||
|
||||
@_ensure_topdir
|
||||
def start(self):
|
||||
# Determine whether we are collocated with master.
|
||||
self.is_collocated = (
|
||||
socket.gethostname() == self.config.workerinput['cov_master_host']
|
||||
and self.topdir == self.config.workerinput['cov_master_topdir']
|
||||
)
|
||||
|
||||
# If we are not collocated, then rewrite master paths to worker paths.
|
||||
cleanup()
|
||||
|
||||
# Determine whether we are collocated with master.
|
||||
self.is_collocated = (socket.gethostname() == self.config.workerinput['cov_master_host'] and
|
||||
self.topdir == self.config.workerinput['cov_master_topdir'])
|
||||
|
||||
# If we are not collocated then rewrite master paths to worker paths.
|
||||
if not self.is_collocated:
|
||||
master_topdir = self.config.workerinput['cov_master_topdir']
|
||||
worker_topdir = self.topdir
|
||||
if self.cov_source is not None:
|
||||
self.cov_source = [source.replace(master_topdir, worker_topdir) for source in self.cov_source]
|
||||
self.cov_source = [source.replace(master_topdir, worker_topdir)
|
||||
for source in self.cov_source]
|
||||
self.cov_config = self.cov_config.replace(master_topdir, worker_topdir)
|
||||
|
||||
# Erase any previous data and start coverage.
|
||||
self.cov = coverage.Coverage(
|
||||
source=self.cov_source,
|
||||
branch=self.cov_branch,
|
||||
data_suffix=True,
|
||||
config_file=self.cov_config,
|
||||
)
|
||||
# Prevent workers from issuing module-not-measured type of warnings (expected for a workers to not have coverage in all the files).
|
||||
self.cov._warn_unimported_source = False
|
||||
self.cov = coverage.Coverage(source=self.cov_source,
|
||||
branch=self.cov_branch,
|
||||
data_suffix=True,
|
||||
config_file=self.cov_config)
|
||||
self.cov.start()
|
||||
|
||||
super().start()
|
||||
self.set_env()
|
||||
|
||||
@_ensure_topdir
|
||||
def finish(self):
|
||||
"""Stop coverage and send relevant info back to the master."""
|
||||
super().finish()
|
||||
|
||||
self.unset_env()
|
||||
self.cov.stop()
|
||||
|
||||
if self.is_collocated:
|
||||
@@ -446,15 +404,20 @@ class DistWorker(CovController):
|
||||
# it on the master node.
|
||||
|
||||
# Send all the data to the master over the channel.
|
||||
data = self.cov.get_data().dumps()
|
||||
if coverage.version_info < (5, 0):
|
||||
buff = StringIO()
|
||||
self.cov.data.write_fileobj(buff)
|
||||
data = buff.getvalue()
|
||||
else:
|
||||
data = self.cov.get_data().dumps()
|
||||
|
||||
self.config.workeroutput.update(
|
||||
{
|
||||
'cov_worker_path': self.topdir,
|
||||
'cov_worker_node_id': self.nodeid,
|
||||
'cov_worker_data': data,
|
||||
}
|
||||
)
|
||||
self.config.workeroutput.update({
|
||||
'cov_worker_path': self.topdir,
|
||||
'cov_worker_node_id': self.nodeid,
|
||||
'cov_worker_data': data,
|
||||
})
|
||||
|
||||
def summary(self, stream):
|
||||
"""Only the master reports so do nothing."""
|
||||
|
||||
pass
|
||||
|
||||
@@ -1,36 +1,47 @@
|
||||
"""Coverage plugin for pytest."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import coverage
|
||||
import pytest
|
||||
|
||||
from . import CovDisabledWarning
|
||||
from . import CovReportWarning
|
||||
from . import PytestCovWarning
|
||||
from . import compat
|
||||
from . import embed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .engine import CovController
|
||||
|
||||
COVERAGE_SQLITE_WARNING_RE = re.compile('unclosed database in <sqlite3.Connection object at', re.I)
|
||||
class CoverageError(Exception):
|
||||
"""Indicates that our coverage is too low"""
|
||||
|
||||
|
||||
class PytestCovWarning(pytest.PytestWarning):
|
||||
"""
|
||||
The base for all pytest-cov warnings, never raised directly
|
||||
"""
|
||||
|
||||
|
||||
class CovDisabledWarning(PytestCovWarning):
|
||||
"""Indicates that Coverage was manually disabled"""
|
||||
|
||||
|
||||
class CovReportWarning(PytestCovWarning):
|
||||
"""Indicates that we failed to generate a report"""
|
||||
|
||||
|
||||
def validate_report(arg):
|
||||
file_choices = ['annotate', 'html', 'xml', 'json', 'markdown', 'markdown-append', 'lcov']
|
||||
file_choices = ['annotate', 'html', 'xml', 'json', 'lcov']
|
||||
term_choices = ['term', 'term-missing']
|
||||
term_modifier_choices = ['skip-covered']
|
||||
all_choices = term_choices + file_choices
|
||||
values = arg.split(':', 1)
|
||||
values = arg.split(":", 1)
|
||||
report_type = values[0]
|
||||
if report_type not in [*all_choices, '']:
|
||||
if report_type not in all_choices + ['']:
|
||||
msg = f'invalid choice: "{arg}" (choose from "{all_choices}")'
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
|
||||
if report_type == 'lcov' and coverage.version_info <= (6, 3):
|
||||
raise argparse.ArgumentTypeError('LCOV output is only supported with coverage.py >= 6.3')
|
||||
|
||||
if len(values) == 1:
|
||||
return report_type, None
|
||||
|
||||
@@ -39,7 +50,8 @@ def validate_report(arg):
|
||||
return report_type, report_modifier
|
||||
|
||||
if report_type not in file_choices:
|
||||
msg = f'output specifier not supported for: "{arg}" (choose from "{file_choices}")'
|
||||
msg = 'output specifier not supported for: "{}" (choose from "{}")'.format(arg,
|
||||
file_choices)
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
|
||||
return values
|
||||
@@ -52,16 +64,17 @@ def validate_fail_under(num_str):
|
||||
try:
|
||||
value = float(num_str)
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError('An integer or float value is required.') from None
|
||||
raise argparse.ArgumentTypeError('An integer or float value is required.')
|
||||
if value > 100:
|
||||
raise argparse.ArgumentTypeError(
|
||||
'Your desire for over-achievement is admirable but misplaced. The maximum value is 100. Perhaps write more integration tests?'
|
||||
)
|
||||
raise argparse.ArgumentTypeError('Your desire for over-achievement is admirable but misplaced. '
|
||||
'The maximum value is 100. Perhaps write more integration tests?')
|
||||
return value
|
||||
|
||||
|
||||
def validate_context(arg):
|
||||
if arg != 'test':
|
||||
if coverage.version_info <= (5, 0):
|
||||
raise argparse.ArgumentTypeError('Contexts are only supported with coverage.py >= 5.x')
|
||||
if arg != "test":
|
||||
raise argparse.ArgumentTypeError('The only supported value is "test".')
|
||||
return arg
|
||||
|
||||
@@ -71,107 +84,46 @@ class StoreReport(argparse.Action):
|
||||
report_type, file = values
|
||||
namespace.cov_report[report_type] = file
|
||||
|
||||
# coverage.py doesn't set a default file for markdown output_format
|
||||
if report_type in ['markdown', 'markdown-append'] and file is None:
|
||||
namespace.cov_report[report_type] = 'coverage.md'
|
||||
if all(x in namespace.cov_report for x in ['markdown', 'markdown-append']):
|
||||
self._validate_markdown_dest_files(namespace.cov_report, parser)
|
||||
|
||||
def _validate_markdown_dest_files(self, cov_report_options, parser):
|
||||
markdown_file = cov_report_options['markdown']
|
||||
markdown_append_file = cov_report_options['markdown-append']
|
||||
if markdown_file == markdown_append_file:
|
||||
error_message = f"markdown and markdown-append options cannot point to the same file: '{markdown_file}'."
|
||||
error_message += ' Please redirect one of them using :DEST (e.g. --cov-report=markdown:dest_file.md)'
|
||||
parser.error(error_message)
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"""Add options to control coverage."""
|
||||
|
||||
group = parser.getgroup('cov', 'coverage reporting with distributed testing support')
|
||||
group.addoption(
|
||||
'--cov',
|
||||
action='append',
|
||||
default=[],
|
||||
metavar='SOURCE',
|
||||
nargs='?',
|
||||
const=True,
|
||||
dest='cov_source',
|
||||
help='Path or package name to measure during execution (multi-allowed). '
|
||||
'Use --cov= to not do any source filtering and record everything.',
|
||||
)
|
||||
group.addoption(
|
||||
'--cov-reset',
|
||||
action='store_const',
|
||||
const=[],
|
||||
dest='cov_source',
|
||||
help='Reset cov sources accumulated in options so far. ',
|
||||
)
|
||||
group.addoption(
|
||||
'--cov-report',
|
||||
action=StoreReport,
|
||||
default={},
|
||||
metavar='TYPE',
|
||||
type=validate_report,
|
||||
help='Type of report to generate: term, term-missing, '
|
||||
'annotate, html, xml, json, markdown, markdown-append, lcov (multi-allowed). '
|
||||
'term, term-missing may be followed by ":skip-covered". '
|
||||
'annotate, html, xml, json, markdown, markdown-append and lcov may be followed by ":DEST" '
|
||||
'where DEST specifies the output location. '
|
||||
'Use --cov-report= to not generate any output.',
|
||||
)
|
||||
group.addoption(
|
||||
'--cov-config',
|
||||
action='store',
|
||||
default='.coveragerc',
|
||||
metavar='PATH',
|
||||
help='Config file for coverage. Default: .coveragerc',
|
||||
)
|
||||
group.addoption(
|
||||
'--no-cov-on-fail',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Do not report coverage if test run fails. Default: False',
|
||||
)
|
||||
group.addoption(
|
||||
'--no-cov',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Disable coverage report completely (useful for debuggers). Default: False',
|
||||
)
|
||||
group.addoption(
|
||||
'--cov-fail-under',
|
||||
action='store',
|
||||
metavar='MIN',
|
||||
type=validate_fail_under,
|
||||
help='Fail if the total coverage is less than MIN.',
|
||||
)
|
||||
group.addoption(
|
||||
'--cov-append',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Do not delete coverage but append to current. Default: False',
|
||||
)
|
||||
group.addoption(
|
||||
'--cov-branch',
|
||||
action='store_true',
|
||||
default=None,
|
||||
help='Enable branch coverage.',
|
||||
)
|
||||
group.addoption(
|
||||
'--cov-precision',
|
||||
type=int,
|
||||
default=None,
|
||||
help='Override the reporting precision.',
|
||||
)
|
||||
group.addoption(
|
||||
'--cov-context',
|
||||
action='store',
|
||||
metavar='CONTEXT',
|
||||
type=validate_context,
|
||||
help='Dynamic contexts to use. "test" for now.',
|
||||
)
|
||||
group = parser.getgroup(
|
||||
'cov', 'coverage reporting with distributed testing support')
|
||||
group.addoption('--cov', action='append', default=[], metavar='SOURCE',
|
||||
nargs='?', const=True, dest='cov_source',
|
||||
help='Path or package name to measure during execution (multi-allowed). '
|
||||
'Use --cov= to not do any source filtering and record everything.')
|
||||
group.addoption('--cov-reset', action='store_const', const=[], dest='cov_source',
|
||||
help='Reset cov sources accumulated in options so far. ')
|
||||
group.addoption('--cov-report', action=StoreReport, default={},
|
||||
metavar='TYPE', type=validate_report,
|
||||
help='Type of report to generate: term, term-missing, '
|
||||
'annotate, html, xml, json, lcov (multi-allowed). '
|
||||
'term, term-missing may be followed by ":skip-covered". '
|
||||
'annotate, html, xml, json and lcov may be followed by ":DEST" '
|
||||
'where DEST specifies the output location. '
|
||||
'Use --cov-report= to not generate any output.')
|
||||
group.addoption('--cov-config', action='store', default='.coveragerc',
|
||||
metavar='PATH',
|
||||
help='Config file for coverage. Default: .coveragerc')
|
||||
group.addoption('--no-cov-on-fail', action='store_true', default=False,
|
||||
help='Do not report coverage if test run fails. '
|
||||
'Default: False')
|
||||
group.addoption('--no-cov', action='store_true', default=False,
|
||||
help='Disable coverage report completely (useful for debuggers). '
|
||||
'Default: False')
|
||||
group.addoption('--cov-fail-under', action='store', metavar='MIN',
|
||||
type=validate_fail_under,
|
||||
help='Fail if the total coverage is less than MIN.')
|
||||
group.addoption('--cov-append', action='store_true', default=False,
|
||||
help='Do not delete coverage but append to current. '
|
||||
'Default: False')
|
||||
group.addoption('--cov-branch', action='store_true', default=None,
|
||||
help='Enable branch coverage.')
|
||||
group.addoption('--cov-context', action='store', metavar='CONTEXT',
|
||||
type=validate_context,
|
||||
help='Dynamic contexts to use. "test" for now.')
|
||||
|
||||
|
||||
def _prepare_cov_source(cov_source):
|
||||
@@ -209,7 +161,7 @@ class CovPlugin:
|
||||
distributed worker.
|
||||
"""
|
||||
|
||||
def __init__(self, options: argparse.Namespace, pluginmanager, start=True, no_cov_should_warn=False):
|
||||
def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False):
|
||||
"""Creates a coverage pytest plugin.
|
||||
|
||||
We read the rc file that coverage uses to get the data file
|
||||
@@ -220,16 +172,17 @@ class CovPlugin:
|
||||
# Our implementation is unknown at this time.
|
||||
self.pid = None
|
||||
self.cov_controller = None
|
||||
self.cov_report = StringIO()
|
||||
self.cov_report = compat.StringIO()
|
||||
self.cov_total = None
|
||||
self.failed = False
|
||||
self._started = False
|
||||
self._start_path = None
|
||||
self._disabled = False
|
||||
self.options = options
|
||||
self._wrote_heading = False
|
||||
|
||||
is_dist = getattr(options, 'numprocesses', False) or getattr(options, 'distload', False) or getattr(options, 'dist', 'no') != 'no'
|
||||
is_dist = (getattr(options, 'numprocesses', False) or
|
||||
getattr(options, 'distload', False) or
|
||||
getattr(options, 'dist', 'no') != 'no')
|
||||
if getattr(options, 'no_cov', False):
|
||||
self._disabled = True
|
||||
return
|
||||
@@ -251,7 +204,8 @@ class CovPlugin:
|
||||
|
||||
# worker is started in pytest hook
|
||||
|
||||
def start(self, controller_cls: type['CovController'], config=None, nodeid=None):
|
||||
def start(self, controller_cls, config=None, nodeid=None):
|
||||
|
||||
if config is None:
|
||||
# fake config option for engine
|
||||
class Config:
|
||||
@@ -259,15 +213,21 @@ class CovPlugin:
|
||||
|
||||
config = Config()
|
||||
|
||||
self.cov_controller = controller_cls(self.options, config, nodeid)
|
||||
self.cov_controller = controller_cls(
|
||||
self.options.cov_source,
|
||||
self.options.cov_report,
|
||||
self.options.cov_config,
|
||||
self.options.cov_append,
|
||||
self.options.cov_branch,
|
||||
config,
|
||||
nodeid
|
||||
)
|
||||
self.cov_controller.start()
|
||||
self._started = True
|
||||
self._start_path = Path.cwd()
|
||||
self._start_path = os.getcwd()
|
||||
cov_config = self.cov_controller.cov.config
|
||||
if self.options.cov_fail_under is None and hasattr(cov_config, 'fail_under'):
|
||||
self.options.cov_fail_under = cov_config.fail_under
|
||||
if self.options.cov_precision is None:
|
||||
self.options.cov_precision = getattr(cov_config, 'precision', 0)
|
||||
|
||||
def _is_worker(self, session):
|
||||
return getattr(session.config, 'workerinput', None) is not None
|
||||
@@ -286,13 +246,15 @@ class CovPlugin:
|
||||
|
||||
self.pid = os.getpid()
|
||||
if self._is_worker(session):
|
||||
nodeid = session.config.workerinput.get('workerid', session.nodeid)
|
||||
nodeid = (
|
||||
session.config.workerinput.get('workerid', getattr(session, 'nodeid'))
|
||||
)
|
||||
self.start(engine.DistWorker, session.config, nodeid)
|
||||
elif not self._started:
|
||||
self.start(engine.Central)
|
||||
|
||||
if self.options.cov_context == 'test':
|
||||
session.config.pluginmanager.register(TestContextPlugin(self.cov_controller), '_cov_contexts')
|
||||
session.config.pluginmanager.register(TestContextPlugin(self.cov_controller.cov), '_cov_contexts')
|
||||
|
||||
@pytest.hookimpl(optionalhook=True)
|
||||
def pytest_configure_node(self, node):
|
||||
@@ -316,81 +278,50 @@ class CovPlugin:
|
||||
needed = self.options.cov_report or self.options.cov_fail_under
|
||||
return needed and not (self.failed and self.options.no_cov_on_fail)
|
||||
|
||||
def _failed_cov_total(self):
|
||||
cov_fail_under = self.options.cov_fail_under
|
||||
return cov_fail_under is not None and self.cov_total < cov_fail_under
|
||||
|
||||
# we need to wrap pytest_runtestloop. by the time pytest_sessionfinish
|
||||
# runs, it's too late to set testsfailed
|
||||
@pytest.hookimpl(wrapper=True)
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtestloop(self, session):
|
||||
yield
|
||||
|
||||
if self._disabled:
|
||||
return (yield)
|
||||
return
|
||||
|
||||
# we add default warning configuration to prevent certain warnings to bubble up as errors due to rigid filterwarnings configuration
|
||||
for _, message, category, _, _ in warnings.filters:
|
||||
if category is ResourceWarning and message == COVERAGE_SQLITE_WARNING_RE:
|
||||
break
|
||||
else:
|
||||
warnings.filterwarnings('default', 'unclosed database in <sqlite3.Connection object at', ResourceWarning)
|
||||
for _, _, category, _, _ in warnings.filters:
|
||||
if category is PytestCovWarning:
|
||||
break
|
||||
else:
|
||||
warnings.simplefilter('once', PytestCovWarning)
|
||||
from coverage.exceptions import CoverageWarning
|
||||
compat_session = compat.SessionWrapper(session)
|
||||
|
||||
for _, _, category, _, _ in warnings.filters:
|
||||
if category is CoverageWarning:
|
||||
break
|
||||
else:
|
||||
warnings.simplefilter('once', CoverageWarning)
|
||||
|
||||
result = yield
|
||||
|
||||
self.failed = bool(session.testsfailed)
|
||||
self.failed = bool(compat_session.testsfailed)
|
||||
if self.cov_controller is not None:
|
||||
self.cov_controller.finish()
|
||||
|
||||
if not self._is_worker(session) and self._should_report():
|
||||
|
||||
# import coverage lazily here to avoid importing
|
||||
# it for unit tests that don't need it
|
||||
from coverage.misc import CoverageException
|
||||
from coverage.results import display_covered
|
||||
from coverage.results import should_fail_under
|
||||
|
||||
try:
|
||||
self.cov_total = self.cov_controller.summary(self.cov_report)
|
||||
except CoverageException as exc:
|
||||
message = f'Failed to generate report: {exc}\n'
|
||||
session.config.pluginmanager.getplugin('terminalreporter').write(f'\nWARNING: {message}\n', red=True, bold=True)
|
||||
warnings.warn(CovReportWarning(message), stacklevel=1)
|
||||
session.config.pluginmanager.getplugin("terminalreporter").write(
|
||||
f'WARNING: {message}\n', red=True, bold=True)
|
||||
warnings.warn(CovReportWarning(message))
|
||||
self.cov_total = 0
|
||||
assert self.cov_total is not None, 'Test coverage should never be `None`'
|
||||
cov_fail_under = self.options.cov_fail_under
|
||||
cov_precision = self.options.cov_precision
|
||||
if cov_fail_under is None or self.options.collectonly:
|
||||
return
|
||||
if should_fail_under(self.cov_total, cov_fail_under, cov_precision):
|
||||
message = 'Coverage failure: total of {total} is less than fail-under={fail_under:.{p}f}'.format(
|
||||
total=display_covered(self.cov_total, cov_precision),
|
||||
fail_under=cov_fail_under,
|
||||
p=cov_precision,
|
||||
)
|
||||
session.config.pluginmanager.getplugin('terminalreporter').write(f'\nERROR: {message}\n', red=True, bold=True)
|
||||
if self._failed_cov_total() and not self.options.collectonly:
|
||||
# make sure we get the EXIT_TESTSFAILED exit code
|
||||
session.testsfailed += 1
|
||||
|
||||
return result
|
||||
|
||||
def write_heading(self, terminalreporter):
|
||||
if not self._wrote_heading:
|
||||
terminalreporter.write_sep('=', 'tests coverage')
|
||||
self._wrote_heading = True
|
||||
compat_session.testsfailed += 1
|
||||
|
||||
def pytest_terminal_summary(self, terminalreporter):
|
||||
if self._disabled:
|
||||
if self.options.no_cov_should_warn:
|
||||
self.write_heading(terminalreporter)
|
||||
message = 'Coverage disabled via --no-cov switch!'
|
||||
terminalreporter.write(f'WARNING: {message}\n', red=True, bold=True)
|
||||
warnings.warn(CovDisabledWarning(message), stacklevel=1)
|
||||
warnings.warn(CovDisabledWarning(message))
|
||||
return
|
||||
if self.cov_controller is None:
|
||||
return
|
||||
@@ -401,25 +332,38 @@ class CovPlugin:
|
||||
|
||||
report = self.cov_report.getvalue()
|
||||
|
||||
# Avoid undesirable new lines when output is disabled with "--cov-report=".
|
||||
if report:
|
||||
self.write_heading(terminalreporter)
|
||||
terminalreporter.write(report)
|
||||
terminalreporter.write('\n' + report + '\n')
|
||||
|
||||
if self.options.cov_fail_under is not None and self.options.cov_fail_under > 0:
|
||||
self.write_heading(terminalreporter)
|
||||
failed = self.cov_total < self.options.cov_fail_under
|
||||
markup = {'red': True, 'bold': True} if failed else {'green': True}
|
||||
message = '{fail}Required test coverage of {required}% {reached}. Total coverage: {actual:.2f}%\n'.format(
|
||||
required=self.options.cov_fail_under,
|
||||
actual=self.cov_total,
|
||||
fail='FAIL ' if failed else '',
|
||||
reached='not reached' if failed else 'reached',
|
||||
message = (
|
||||
'{fail}Required test coverage of {required}% {reached}. '
|
||||
'Total coverage: {actual:.2f}%\n'
|
||||
.format(
|
||||
required=self.options.cov_fail_under,
|
||||
actual=self.cov_total,
|
||||
fail="FAIL " if failed else "",
|
||||
reached="not reached" if failed else "reached"
|
||||
)
|
||||
)
|
||||
terminalreporter.write(message, **markup)
|
||||
|
||||
def pytest_runtest_setup(self, item):
|
||||
if os.getpid() != self.pid:
|
||||
# test is run in another process than session, run
|
||||
# coverage manually
|
||||
embed.init()
|
||||
|
||||
def pytest_runtest_teardown(self, item):
|
||||
embed.cleanup()
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_call(self, item):
|
||||
if item.get_closest_marker('no_cover') or 'no_cover' in getattr(item, 'fixturenames', ()):
|
||||
if (item.get_closest_marker('no_cover')
|
||||
or 'no_cover' in getattr(item, 'fixturenames', ())):
|
||||
self.cov_controller.pause()
|
||||
yield
|
||||
self.cov_controller.resume()
|
||||
@@ -428,10 +372,8 @@ class CovPlugin:
|
||||
|
||||
|
||||
class TestContextPlugin:
|
||||
cov_controller: 'CovController'
|
||||
|
||||
def __init__(self, cov_controller):
|
||||
self.cov_controller = cov_controller
|
||||
def __init__(self, cov):
|
||||
self.cov = cov
|
||||
|
||||
def pytest_runtest_setup(self, item):
|
||||
self.switch_context(item, 'setup')
|
||||
@@ -443,13 +385,15 @@ class TestContextPlugin:
|
||||
self.switch_context(item, 'run')
|
||||
|
||||
def switch_context(self, item, when):
|
||||
if self.cov_controller.started:
|
||||
self.cov_controller.cov.switch_context(f'{item.nodeid}|{when}')
|
||||
context = f"{item.nodeid}|{when}"
|
||||
self.cov.switch_context(context)
|
||||
os.environ['COV_CORE_CONTEXT'] = context
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def no_cover():
|
||||
"""A pytest fixture to disable coverage."""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -465,4 +409,4 @@ def cov(request):
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line('markers', 'no_cover: disable coverage for this test.')
|
||||
config.addinivalue_line("markers", "no_cover: disable coverage for this test.")
|
||||
|
||||
Reference in New Issue
Block a user