Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle issue 604 #657

Merged
merged 4 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/pytest_cov/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,47 @@
"""pytest-cov: avoid already-imported warning: PYTEST_DONT_REWRITE."""

__version__ = '5.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 CovFailUnderWarning(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
"""
37 changes: 31 additions & 6 deletions src/pytest_cov/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@
import random
import socket
import sys
import warnings
from io import StringIO
from pathlib import Path

import coverage
from coverage.data import CoverageData
from coverage.sqldata import filename_suffix

from . import CentralCovContextWarning
from . import DistCovError
from .embed import cleanup


class BrokenCovConfigError(Exception):
pass


class _NullFile:
@staticmethod
def write(v):
Expand Down Expand Up @@ -51,6 +59,10 @@ def ensure_topdir_wrapper(self, *args, **kwargs):
return ensure_topdir_wrapper


def _data_suffix(name):
return f'{filename_suffix(True)}.{name}'


class CovController:
"""Base class for different plugin implementations."""

Expand Down Expand Up @@ -230,13 +242,20 @@ def start(self):
self.cov = coverage.Coverage(
source=self.cov_source,
branch=self.cov_branch,
data_suffix=True,
data_suffix=_data_suffix('c'),
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)

self.combining_cov = coverage.Coverage(
source=self.cov_source,
branch=self.cov_branch,
data_suffix=True,
data_suffix=_data_suffix('cc'),
data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100
config_file=self.cov_config,
)
Expand Down Expand Up @@ -274,16 +293,22 @@ def start(self):
self.cov = coverage.Coverage(
source=self.cov_source,
branch=self.cov_branch,
data_suffix=True,
data_suffix=_data_suffix('m'),
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.'
)
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=True,
data_suffix=_data_suffix('mc'),
data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100
config_file=self.cov_config,
)
Expand Down Expand Up @@ -330,7 +355,7 @@ def testnodedown(self, node, error):
data.read_fileobj(StringIO(output['cov_worker_data']))
cov.data.update(data)
else:
data = CoverageData(no_disk=True)
data = CoverageData(no_disk=True, suffix='should-not-exist')
data.loads(output['cov_worker_data'])
cov.get_data().update(data)
cov.stop()
Expand Down Expand Up @@ -381,7 +406,7 @@ def start(self):
self.cov = coverage.Coverage(
source=self.cov_source,
branch=self.cov_branch,
data_suffix=True,
data_suffix=_data_suffix(f'w{self.nodeid}'),
config_file=self.cov_config,
)
self.cov.start()
Expand Down
25 changes: 3 additions & 22 deletions src/pytest_cov/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,13 @@
from coverage.results import display_covered
from coverage.results import should_fail_under

from . import CovDisabledWarning
from . import CovFailUnderWarning
from . import CovReportWarning
from . import compat
from . import embed


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 CovFailUnderWarning(PytestCovWarning):
"""Indicates that we failed to generate a report"""


def validate_report(arg):
file_choices = ['annotate', 'html', 'xml', 'json', 'lcov']
term_choices = ['term', 'term-missing']
Expand Down
75 changes: 72 additions & 3 deletions tests/test_pytest_cov.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def test_bar():

"""


COVERAGERC_SOURCE = """\
[run]
source = .
Expand Down Expand Up @@ -153,8 +152,13 @@ def test_foo(cov):

xdist_params = pytest.mark.parametrize(
'opts',
['', pytest.param('-n 1', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"'))],
ids=['nodist', 'xdist'],
[
'',
pytest.param('-n 1', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')),
pytest.param('-n 2', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')),
pytest.param('-n 3', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')),
],
ids=['nodist', '1xdist', '2xdist', '3xdist'],
)


Expand Down Expand Up @@ -1631,6 +1635,71 @@ def test_append_coverage(pytester, testdir, opts, prop):
)


@xdist_params
def test_coverage_plugin(pytester, testdir, opts, prop):
script = testdir.makepyfile(test_1=prop.code)
testdir.makepyfile(
coverageplugin="""
import coverage

class ExamplePlugin(coverage.CoveragePlugin):
pass

def coverage_init(reg, options):
reg.add_file_tracer(ExamplePlugin())
"""
)
testdir.makepyprojecttoml(f"""
[tool.coverage.run]
plugins = ["coverageplugin"]
concurrency = ["thread", "multiprocessing"]
{prop.conf}
""")
result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args)
result.stdout.fnmatch_lines(
[
f'test_1* {prop.result}*',
]
)


@xdist_params
def test_dynamic_context(pytester, testdir, opts, prop):
script = testdir.makepyfile(test_1=prop.code)
testdir.makepyprojecttoml(f"""
[tool.coverage.run]
dynamic_context = "test_function"
parallel = true
{prop.conf}
""")
result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args)
if opts:
result.stderr.fnmatch_lines(['pytest_cov.DistCovError: Detected dynamic_context=test_function*'])
else:
result.stdout.fnmatch_lines(
[
'* CentralCovContextWarning: Detected dynamic_context=test_function*',
f'test_1* {prop.result}*',
]
)


@xdist_params
def test_simple(pytester, testdir, opts, prop):
script = testdir.makepyfile(test_1=prop.code)
testdir.makepyprojecttoml(f"""
[tool.coverage.run]
parallel = true
{prop.conf}
""")
result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args)
result.stdout.fnmatch_lines(
[
f'test_1* {prop.result}*',
]
)


@xdist_params
def test_do_not_append_coverage(pytester, testdir, opts, prop):
script = testdir.makepyfile(test_1=prop.code)
Expand Down
Loading