Skip to content

Commit

Permalink
refactor: move core information to its own class and file
Browse files Browse the repository at this point in the history
This simplifies calling into Collector, and will make core information
available in more places.
  • Loading branch information
nedbat committed Jul 20, 2024
1 parent 694cd6e commit 1f08ea1
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 94 deletions.
2 changes: 1 addition & 1 deletion coverage/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
import coverage
from coverage import Coverage
from coverage import env
from coverage.collector import HAS_CTRACER
from coverage.config import CoverageConfig
from coverage.control import DEFAULT_DATAFILE
from coverage.core import HAS_CTRACER
from coverage.data import combinable_files, debug_data_file
from coverage.debug import info_header, short_stack, write_formatted_info
from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource
Expand Down
92 changes: 11 additions & 81 deletions coverage/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,19 @@

from coverage import env
from coverage.config import CoverageConfig
from coverage.core import Core
from coverage.data import CoverageData
from coverage.debug import short_stack
from coverage.disposition import FileDisposition
from coverage.exceptions import ConfigError
from coverage.misc import human_sorted_items, isolate_module
from coverage.plugin import CoveragePlugin
from coverage.pytracer import PyTracer
from coverage.sysmon import SysMonitor
from coverage.types import (
TArc, TFileDisposition, TTraceData, TTraceFn, TracerCore, TWarnFn,
TArc, TFileDisposition, TTraceData, TTraceFn, Tracer, TWarnFn,
)

os = isolate_module(os)


try:
# Use the C extension code when we can, for speed.
from coverage.tracer import CTracer, CFileDisposition
HAS_CTRACER = True
except ImportError:
# Couldn't import the C extension, maybe it isn't built.
if os.getenv("COVERAGE_CORE") == "ctrace": # pragma: part covered
# During testing, we use the COVERAGE_CORE environment variable
# to indicate that we've fiddled with the environment to test this
# fallback code. If we thought we had a C tracer, but couldn't import
# it, then exit quickly and clearly instead of dribbling confusing
# errors. I'm using sys.exit here instead of an exception because an
# exception here causes all sorts of other noise in unittest.
sys.stderr.write("*** COVERAGE_CORE is 'ctrace' but can't import CTracer!\n")
sys.exit(1)
HAS_CTRACER = False

T = TypeVar("T")


Expand Down Expand Up @@ -78,15 +59,14 @@ class Collector:

def __init__(
self,
core: Core,
should_trace: Callable[[str, FrameType], TFileDisposition],
check_include: Callable[[str, FrameType], bool],
should_start_context: Callable[[FrameType], str | None] | None,
file_mapper: Callable[[str], str],
timid: bool,
branch: bool,
warn: TWarnFn,
concurrency: list[str],
metacov: bool,
) -> None:
"""Create a collector.
Expand All @@ -105,11 +85,6 @@ def __init__(
filename. The result is the name that will be recorded in the data
file.
If `timid` is true, then a slower simpler trace function will be
used. This is important for some environments where manipulation of
tracing functions make the faster more sophisticated trace function not
operate properly.
If `branch` is true, then branches will be measured. This involves
collecting data on which statements followed each other (arcs). Use
`get_arc_data` to get the arc data.
Expand All @@ -124,6 +99,7 @@ def __init__(
Other values are ignored.
"""
self.core = core
self.should_trace = should_trace
self.check_include = check_include
self.should_start_context = should_start_context
Expand All @@ -143,52 +119,6 @@ def __init__(

self.concur_id_func = None

self._trace_class: type[TracerCore]
self.file_disposition_class: type[TFileDisposition]

core: str | None
if timid:
core = "pytrace"
else:
core = os.getenv("COVERAGE_CORE")

if core == "sysmon" and not env.PYBEHAVIOR.pep669:
self.warn("sys.monitoring isn't available, using default core", slug="no-sysmon")
core = None

if not core:
# Once we're comfortable with sysmon as a default:
# if env.PYBEHAVIOR.pep669 and self.should_start_context is None:
# core = "sysmon"
if HAS_CTRACER:
core = "ctrace"
else:
core = "pytrace"

if core == "sysmon":
self._trace_class = SysMonitor
self._core_kwargs = {"tool_id": 3 if metacov else 1}
self.file_disposition_class = FileDisposition
self.supports_plugins = False
self.packed_arcs = False
self.systrace = False
elif core == "ctrace":
self._trace_class = CTracer
self._core_kwargs = {}
self.file_disposition_class = CFileDisposition
self.supports_plugins = True
self.packed_arcs = True
self.systrace = True
elif core == "pytrace":
self._trace_class = PyTracer
self._core_kwargs = {}
self.file_disposition_class = FileDisposition
self.supports_plugins = False
self.packed_arcs = False
self.systrace = True
else:
raise ConfigError(f"Unknown core value: {core!r}")

# We can handle a few concurrency options here, but only one at a time.
concurrencies = set(self.concurrency)
unknown = concurrencies - CoverageConfig.CONCURRENCY_CHOICES
Expand Down Expand Up @@ -222,7 +152,7 @@ def __init__(
msg = f"Couldn't trace with concurrency={tried}, the module isn't installed."
raise ConfigError(msg) from ex

if self.concur_id_func and not hasattr(self._trace_class, "concur_id_func"):
if self.concur_id_func and not hasattr(core.tracer_class, "concur_id_func"):
raise ConfigError(
"Can't support concurrency={} with {}, only threads are supported.".format(
tried, self.tracer_name(),
Expand All @@ -249,7 +179,7 @@ def use_data(self, covdata: CoverageData, context: str | None) -> None:

def tracer_name(self) -> str:
"""Return the class name of the tracer we're using."""
return self._trace_class.__name__
return self.core.tracer_class.__name__

def _clear_data(self) -> None:
"""Clear out existing data, but stay ready for more collection."""
Expand Down Expand Up @@ -305,7 +235,7 @@ def reset(self) -> None:
self.should_trace_cache = {}

# Our active Tracers.
self.tracers: list[TracerCore] = []
self.tracers: list[Tracer] = []

self._clear_data()

Expand All @@ -321,7 +251,7 @@ def unlock_data(self) -> None:

def _start_tracer(self) -> TTraceFn | None:
"""Start a new Tracer object, and store it in self.tracers."""
tracer = self._trace_class(**self._core_kwargs)
tracer = self.core.tracer_class(**self.core.tracer_kwargs)
tracer.data = self.data
tracer.lock_data = self.lock_data
tracer.unlock_data = self.unlock_data
Expand Down Expand Up @@ -403,7 +333,7 @@ def start(self) -> None:

# Install our installation tracer in threading, to jump-start other
# threads.
if self.systrace and self.threading:
if self.core.systrace and self.threading:
self.threading.settrace(self._installation_trace)

def stop(self) -> None:
Expand Down Expand Up @@ -440,7 +370,7 @@ def resume(self) -> None:
"""Resume tracing after a `pause`."""
for tracer in self.tracers:
tracer.start()
if self.systrace:
if self.core.systrace:
if self.threading:
self.threading.settrace(self._installation_trace)
else:
Expand Down Expand Up @@ -523,7 +453,7 @@ def flush_data(self) -> bool:
return False

if self.branch:
if self.packed_arcs:
if self.core.packed_arcs:
# Unpack the line number pairs packed into integers. See
# tracer.c:CTracer_record_pair for the C code that creates
# these packed ints.
Expand Down
16 changes: 11 additions & 5 deletions coverage/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@

from coverage import env
from coverage.annotate import AnnotateReporter
from coverage.collector import Collector, HAS_CTRACER
from coverage.collector import Collector
from coverage.config import CoverageConfig, read_coverage_config
from coverage.context import should_start_context_test_function, combine_context_switchers
from coverage.core import Core, HAS_CTRACER
from coverage.data import CoverageData, combine_parallel_data
from coverage.debug import (
DebugControl, NoDebugging, short_stack, write_formatted_info, relevant_environment_display,
Expand Down Expand Up @@ -262,6 +263,7 @@ def __init__( # pylint: disable=too-many-arguments
self._inorout: InOrOut | None = None
self._plugins: Plugins = Plugins()
self._data: CoverageData | None = None
self._core: Core | None = None
self._collector: Collector | None = None
self._metacov = False

Expand Down Expand Up @@ -532,16 +534,20 @@ def _init_for_start(self) -> None:

should_start_context = combine_context_switchers(context_switchers)

self._core = Core(
warn=self._warn,
timid=self.config.timid,
metacov=self._metacov,
)
self._collector = Collector(
core=self._core,
should_trace=self._should_trace,
check_include=self._check_include_omit_etc,
should_start_context=should_start_context,
file_mapper=self._file_mapper,
timid=self.config.timid,
branch=self.config.branch,
warn=self._warn,
concurrency=concurrency,
metacov=self._metacov,
)

suffix = self._data_suffix_specified
Expand All @@ -563,7 +569,7 @@ def _init_for_start(self) -> None:
self._collector.use_data(self._data, self.config.context)

# Early warning if we aren't going to be able to support plugins.
if self._plugins.file_tracers and not self._collector.supports_plugins:
if self._plugins.file_tracers and not self._core.supports_plugins:
self._warn(
"Plugin file tracers ({}) aren't supported with {}".format(
", ".join(
Expand All @@ -584,7 +590,7 @@ def _init_for_start(self) -> None:
include_namespace_packages=self.config.include_namespace_packages,
)
self._inorout.plugins = self._plugins
self._inorout.disp_class = self._collector.file_disposition_class
self._inorout.disp_class = self._core.file_disposition_class

# It's useful to write debug info after initing for start.
self._should_write_debug = True
Expand Down
90 changes: 90 additions & 0 deletions coverage/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt

"""Management of core choices."""

from __future__ import annotations

import os
import sys
from typing import Any

from coverage import env
from coverage.disposition import FileDisposition
from coverage.exceptions import ConfigError
from coverage.pytracer import PyTracer
from coverage.sysmon import SysMonitor
from coverage.types import TFileDisposition, Tracer, TWarnFn


try:
# Use the C extension code when we can, for speed.
from coverage.tracer import CTracer, CFileDisposition
HAS_CTRACER = True
except ImportError:
# Couldn't import the C extension, maybe it isn't built.
if os.getenv("COVERAGE_CORE") == "ctrace": # pragma: part covered
# During testing, we use the COVERAGE_CORE environment variable
# to indicate that we've fiddled with the environment to test this
# fallback code. If we thought we had a C tracer, but couldn't import
# it, then exit quickly and clearly instead of dribbling confusing
# errors. I'm using sys.exit here instead of an exception because an
# exception here causes all sorts of other noise in unittest.
sys.stderr.write("*** COVERAGE_CORE is 'ctrace' but can't import CTracer!\n")
sys.exit(1)
HAS_CTRACER = False


class Core:
"""Information about the central technology enabling execution measurement."""

tracer_class: type[Tracer]
tracer_kwargs: dict[str, Any]
file_disposition_class: type[TFileDisposition]
supports_plugins: bool
packed_arcs: bool
systrace: bool

def __init__(self, warn: TWarnFn, timid: bool, metacov: bool) -> None:
core_name: str | None
if timid:
core_name = "pytrace"
else:
core_name = os.getenv("COVERAGE_CORE")

if core_name == "sysmon" and not env.PYBEHAVIOR.pep669:
warn("sys.monitoring isn't available, using default core", slug="no-sysmon")
core_name = None

if not core_name:
# Once we're comfortable with sysmon as a default:
# if env.PYBEHAVIOR.pep669 and self.should_start_context is None:
# core_name = "sysmon"
if HAS_CTRACER:
core_name = "ctrace"
else:
core_name = "pytrace"

if core_name == "sysmon":
self.tracer_class = SysMonitor
self.tracer_kwargs = {"tool_id": 3 if metacov else 1}
self.file_disposition_class = FileDisposition
self.supports_plugins = False
self.packed_arcs = False
self.systrace = False
elif core_name == "ctrace":
self.tracer_class = CTracer
self.tracer_kwargs = {}
self.file_disposition_class = CFileDisposition
self.supports_plugins = True
self.packed_arcs = True
self.systrace = True
elif core_name == "pytrace":
self.tracer_class = PyTracer
self.tracer_kwargs = {}
self.file_disposition_class = FileDisposition
self.supports_plugins = False
self.packed_arcs = False
self.systrace = True
else:
raise ConfigError(f"Unknown core value: {core_name!r}")
4 changes: 2 additions & 2 deletions coverage/pytracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from coverage import env
from coverage.types import (
TArc, TFileDisposition, TLineNo, TTraceData, TTraceFileData, TTraceFn,
TracerCore, TWarnFn,
Tracer, TWarnFn,
)

# We need the YIELD_VALUE opcode below, in a comparison-friendly form.
Expand All @@ -36,7 +36,7 @@

THIS_FILE = __file__.rstrip("co")

class PyTracer(TracerCore):
class PyTracer(Tracer):
"""Python implementation of the raw data tracer."""

# Because of poor implementations of trace-function-manipulating tools,
Expand Down
4 changes: 2 additions & 2 deletions coverage/sysmon.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
TLineNo,
TTraceData,
TTraceFileData,
TracerCore,
Tracer,
TWarnFn,
)

Expand Down Expand Up @@ -171,7 +171,7 @@ def bytes_to_lines(code: CodeType) -> dict[int, int]:
return b2l


class SysMonitor(TracerCore):
class SysMonitor(Tracer):
"""Python implementation of the raw data tracer for PEP669 implementations."""

# One of these will be used across threads. Be careful.
Expand Down
Loading

0 comments on commit 1f08ea1

Please sign in to comment.