Thanks to visit codestin.com
Credit goes to github.com

Skip to content
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
11 changes: 11 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ upgrading your version of coverage.py.
Unreleased
----------

- Fix: if the measurement core defaults to "sysmon" (the default for Python
3.14+ since v7.9.1), but sysmon can't support some aspect of your
configuration (concurrency settings, dynamic contexts, and so on), then the
ctrace core is used instead. Previously, this would result in an error.
Now a warning is issued instead, explaining the fallback. An explicit request
for sysmon with conflicting settings will still result in an error. Closes
`issue 2064`_.

- Fix: some multi-line case clauses or for loops (and probably other
constructs) could cause incorrect claims of missing branches with the
sys.monitoring core, as described in `issue 2070`_. This is now fixed.
Expand All @@ -33,11 +41,14 @@ Unreleased
slight performance improvement, but I couldn't reproduce the performance
gain, so it's been reverted, fixing the debugger problem.

- A new debug option ``--debug=core`` shows which core is in use and why.

- Split ``sqlite`` debugging information out of the ``sys`` :ref:`coverage
debug <cmd_debug>` and :ref:`cmd_run_debug` options since it's bulky and not
very useful.

.. _issue 1420: https://github.com/nedbat/coveragepy/issues/1420
.. _issue 2064: https://github.com/nedbat/coveragepy/issues/2064
.. _issue 2070: https://github.com/nedbat/coveragepy/issues/2070


Expand Down
1 change: 1 addition & 0 deletions coverage/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ def _init_for_start(self) -> None:

self._core = Core(
warn=self._warn,
debug=(self._debug if self._debug.should("core") else None),
config=self.config,
dynamic_contexts=(should_start_context is not None),
metacov=self._metacov,
Expand Down
29 changes: 24 additions & 5 deletions coverage/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from coverage.misc import isolate_module
from coverage.pytracer import PyTracer
from coverage.sysmon import SysMonitor
from coverage.types import TFileDisposition, Tracer, TWarnFn
from coverage.types import TDebugCtl, TFileDisposition, Tracer, TWarnFn

os = isolate_module(os)

Expand Down Expand Up @@ -56,11 +56,19 @@ class Core:

def __init__(
self,
*,
warn: TWarnFn,
debug: TDebugCtl | None,
config: CoverageConfig,
dynamic_contexts: bool,
metacov: bool,
) -> None:
def _debug(msg: str) -> None:
if debug:
debug.write(msg)

_debug("in core.py")

# Check the conditions that preclude us from using sys.monitoring.
reason_no_sysmon = ""
if not env.PYBEHAVIOR.pep669:
Expand All @@ -69,29 +77,40 @@ def __init__(
reason_no_sysmon = "can't measure branches in this version"
elif dynamic_contexts:
reason_no_sysmon = "doesn't yet support dynamic contexts"
elif any((bad := c) in config.concurrency for c in ["greenlet", "eventlet", "gevent"]):
reason_no_sysmon = f"doesn't support concurrency={bad}"

core_name: str | None = None
if config.timid:
core_name = "pytrace"

if core_name is None:
_debug("core.py: Using pytrace because timid=True")
elif core_name is None:
# This could still leave core_name as None.
core_name = config.core
_debug(f"core.py: core from config is {core_name!r}")

if core_name == "sysmon" and reason_no_sysmon:
warn(f"sys.monitoring {reason_no_sysmon}, using default core", slug="no-sysmon")
core_name = None
_debug(f"core.py: raising ConfigError because sysmon not usable: {reason_no_sysmon}")
raise ConfigError(
f"Can't use core=sysmon: sys.monitoring {reason_no_sysmon}", skip_tests=True
)

if core_name is None:
if env.SYSMON_DEFAULT and not reason_no_sysmon:
core_name = "sysmon"
_debug("core.py: Using sysmon because SYSMON_DEFAULT is set")
else:
core_name = "ctrace"
_debug("core.py: Defaulting to ctrace core")

if core_name == "ctrace":
if not CTRACER_FILE:
if IMPORT_ERROR and env.SHIPPING_WHEELS:
warn(f"Couldn't import C tracer: {IMPORT_ERROR}", slug="no-ctracer", once=True)
core_name = "pytrace"
_debug("core.py: Falling back to pytrace because C tracer not available")

_debug(f"core.py: Using core={core_name}")

self.tracer_kwargs = {}

Expand Down
19 changes: 18 additions & 1 deletion coverage/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,26 @@
class CoverageException(Exception):
"""The base class of all exceptions raised by Coverage.py."""

def __init__(self, *args: Any, slug: str | None = None) -> None:
def __init__(
self,
*args: Any,
slug: str | None = None,
skip_tests: bool = False,
) -> None:
"""Create an exception.

Args:
slug: A short string identifying the exception, will be used for
linking to documentation.
skip_tests: If True, raising this exception will skip the test it
is raised in. This is used for shutting off large numbers of
tests that we know will not succeed because of a configuration
mismatch.
"""

super().__init__(*args)
self.slug = slug
self.skip_tests = skip_tests


class ConfigError(CoverageException):
Expand Down
2 changes: 2 additions & 0 deletions doc/commands/cmd_debug.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ of activity to log:
* ``config``: before starting, dump all the :ref:`configuration <config>`
values.

* ``core``: log decision about choosing the measurement core to use.

* ``dataio``: log when reading or writing any data file.

* ``dataop``: log a summary of data being added to CoverageData objects.
Expand Down
3 changes: 3 additions & 0 deletions metacov.ini
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ exclude_lines =
# Lines that will never be called, but satisfy the type checker
pragma: never called

# Code that is never run during metacov, but run during normal tests.
pragma: never metacov

partial_branches =
pragma: part covered
# A for-loop that always hits its break statement
Expand Down
16 changes: 16 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,19 @@ def create_pth_file_fixture() -> Iterable[None]:
finally:
for p in pth_files:
p.unlink()


@pytest.hookimpl(wrapper=True)
def pytest_runtest_call() -> Iterable[None]:
"""Check the exception raised by the test, and skip the test if needed."""
try:
yield
except Exception as e: # pragma: never metacov
# This code is for dealing with the exception raised when we are
# measuring with a core that doesn't support branch measurement.
# During metacov, we skip those situations entirely by not running
# sysmon on 3.12 or 3.13, so this code is never needed during metacov.
if getattr(e, "skip_tests", False):
pytest.skip(f"Skipping for exception: {e}")
else:
raise
17 changes: 12 additions & 5 deletions tests/test_concurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ def cant_trace_msg(concurrency: str, the_module: ModuleType | None) -> str | Non
parts.remove("multiprocessing")
concurrency = ",".join(parts)

if the_module is None:
if testenv.SYS_MON and concurrency:
expected_out = (
f"Can't use core=sysmon: sys.monitoring doesn't support concurrency={concurrency}\n"
)
elif the_module is None:
# We don't even have the underlying module installed, we expect
# coverage to alert us to this fact.
expected_out = (
Expand Down Expand Up @@ -356,12 +360,12 @@ def gwork(q):
)
_, out = self.run_command_status("coverage run --concurrency=thread,gevent both.py")
if gevent is None:
assert out == ("Couldn't trace with concurrency=gevent, the module isn't installed.\n")
assert "Couldn't trace with concurrency=gevent, the module isn't installed.\n" in out
pytest.skip("Can't run test without gevent installed.")
if not testenv.C_TRACER:
assert testenv.PY_TRACER
assert out == (
f"Can't support concurrency=gevent with {testenv.REQUESTED_TRACER_CLASS}, "
+ "only threads are supported.\n"
"Can't support concurrency=gevent with PyTracer, only threads are supported.\n"
)
pytest.skip(f"Can't run gevent with {testenv.REQUESTED_TRACER_CLASS}.")

Expand Down Expand Up @@ -401,7 +405,10 @@ class WithoutConcurrencyModuleTest(CoverageTest):
def test_missing_module(self, module: str) -> None:
self.make_file("prog.py", "a = 1")
sys.modules[module] = None # type: ignore[assignment]
msg = f"Couldn't trace with concurrency={module}, the module isn't installed."
if testenv.SYS_MON:
msg = rf"Can't use core=sysmon: sys.monitoring doesn't support concurrency={module}"
else:
msg = rf"Couldn't trace with concurrency={module}, the module isn't installed."
with pytest.raises(ConfigError, match=msg):
self.command_line(f"run --concurrency={module} prog.py")

Expand Down
58 changes: 28 additions & 30 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,21 @@ def test_core_request_pytrace(self) -> None:

def test_core_request_sysmon(self) -> None:
self.set_environ("COVERAGE_CORE", "sysmon")
out = self.run_command("coverage run --debug=sys numbers.py")
assert out.endswith("123 456\n")
core = re_line(r" core:", out).strip()
warns = re_lines(r"\(no-sysmon\)", out)
if env.PYBEHAVIOR.pep669:
status = 0
else:
status = 1
out = self.run_command("coverage run --debug=sys numbers.py", status=status)
if status == 0:
assert out.endswith("123 456\n")
core = re_line(r" core:", out).strip()
warns = re_lines(r"\(no-sysmon\)", out)
assert core == "core: SysMonitor"
assert not warns
else:
assert core in ["core: CTracer", "core: PyTracer"]
assert warns
assert out.endswith(
"Can't use core=sysmon: sys.monitoring isn't available in this version\n"
)

def test_core_request_sysmon_no_dyncontext(self) -> None:
# Use config core= for this test just to be different.
Expand All @@ -96,19 +101,14 @@ def test_core_request_sysmon_no_dyncontext(self) -> None:
dynamic_context = test_function
""",
)
out = self.run_command("coverage run --debug=sys numbers.py")
assert out.endswith("123 456\n")
core = re_line(r" core:", out).strip()
assert core in ["core: CTracer", "core: PyTracer"]
warns = re_lines(r"\(no-sysmon\)", out)
assert len(warns) == 1
out = self.run_command("coverage run --debug=sys numbers.py", status=1)
if env.PYBEHAVIOR.pep669:
assert (
"sys.monitoring doesn't yet support dynamic contexts, using default core"
in warns[0]
"Can't use core=sysmon: sys.monitoring doesn't yet support dynamic contexts\n"
in out
)
else:
assert "sys.monitoring isn't available in this version, using default core" in warns[0]
assert "Can't use core=sysmon: sys.monitoring isn't available in this version\n" in out

def test_core_request_sysmon_no_branches(self) -> None:
# Use config core= for this test just to be different.
Expand All @@ -120,25 +120,23 @@ def test_core_request_sysmon_no_branches(self) -> None:
branch = True
""",
)
out = self.run_command("coverage run --debug=sys numbers.py")
assert out.endswith("123 456\n")
core = re_line(r" core:", out).strip()
warns = re_lines(r"\(no-sysmon\)", out)
if env.PYBEHAVIOR.branch_right_left:
status = 0
elif env.PYBEHAVIOR.pep669:
status = 1
msg = "Can't use core=sysmon: sys.monitoring can't measure branches in this version\n"
else:
status = 1
msg = "Can't use core=sysmon: sys.monitoring isn't available in this version\n"
out = self.run_command("coverage run --debug=sys numbers.py", status=status)
if status == 0:
assert out.endswith("123 456\n")
core = re_line(r" core:", out).strip()
warns = re_lines(r"\(no-sysmon\)", out)
assert core == "core: SysMonitor"
assert not warns
else:
assert core in ["core: CTracer", "core: PyTracer"]
assert len(warns) == 1
if env.PYBEHAVIOR.pep669:
assert (
"sys.monitoring can't measure branches in this version, using default core"
in warns[0]
)
else:
assert (
"sys.monitoring isn't available in this version, using default core" in warns[0]
)
assert out.endswith(msg) # pylint: disable=possibly-used-before-assignment

def test_core_request_nosuchcore(self) -> None:
# Test the coverage misconfigurations in-process with pytest. Running a
Expand Down
Loading