Skip to content

Commit a5d138a

Browse files
committed
wip
1 parent 0e1fb4d commit a5d138a

File tree

11 files changed

+129
-66
lines changed

11 files changed

+129
-66
lines changed

coverage/collector.py

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from typing import Any, Callable, TypeVar, cast
1515

1616
from coverage import env
17-
from coverage.config import CoverageConfig
1817
from coverage.core import Core
1918
from coverage.data import CoverageData
2019
from coverage.debug import short_stack
@@ -60,9 +59,6 @@ class Collector:
6059
# the top, and resumed when they become the top again.
6160
_collectors: list[Collector] = []
6261

63-
# The concurrency settings we support here.
64-
LIGHT_THREADS = {"greenlet", "eventlet", "gevent"}
65-
6662
def __init__(
6763
self,
6864
core: Core,
@@ -112,8 +108,7 @@ def __init__(
112108
self.file_mapper = file_mapper
113109
self.branch = branch
114110
self.warn = warn
115-
self.concurrency = concurrency
116-
assert isinstance(self.concurrency, list), f"Expected a list: {self.concurrency!r}"
111+
assert isinstance(concurrency, list), f"Expected a list: {concurrency!r}"
117112

118113
self.pid = os.getpid()
119114

@@ -125,37 +120,27 @@ def __init__(
125120

126121
self.concur_id_func = None
127122

128-
# We can handle a few concurrency options here, but only one at a time.
129-
concurrencies = set(self.concurrency)
130-
unknown = concurrencies - CoverageConfig.CONCURRENCY_CHOICES
131-
if unknown:
132-
show = ", ".join(sorted(unknown))
133-
raise ConfigError(f"Unknown concurrency choices: {show}")
134-
light_threads = concurrencies & self.LIGHT_THREADS
135-
if len(light_threads) > 1:
136-
show = ", ".join(sorted(light_threads))
137-
raise ConfigError(f"Conflicting concurrency settings: {show}")
138123
do_threading = False
139124

140125
tried = "nothing" # to satisfy pylint
141126
try:
142-
if "greenlet" in concurrencies:
127+
if "greenlet" in concurrency:
143128
tried = "greenlet"
144129
import greenlet
145130

146131
self.concur_id_func = greenlet.getcurrent
147-
elif "eventlet" in concurrencies:
132+
elif "eventlet" in concurrency:
148133
tried = "eventlet"
149134
import eventlet.greenthread
150135

151136
self.concur_id_func = eventlet.greenthread.getcurrent
152-
elif "gevent" in concurrencies:
137+
elif "gevent" in concurrency:
153138
tried = "gevent"
154139
import gevent
155140

156141
self.concur_id_func = gevent.getcurrent
157142

158-
if "thread" in concurrencies:
143+
if "thread" in concurrency:
159144
do_threading = True
160145
except ImportError as ex:
161146
msg = f"Couldn't trace with concurrency={tried}, the module isn't installed."
@@ -169,7 +154,7 @@ def __init__(
169154
),
170155
)
171156

172-
if do_threading or not concurrencies:
157+
if do_threading or not concurrency:
173158
# It's important to import threading only if we need it. If
174159
# it's imported early, and the program being measured uses
175160
# gevent, then gevent's monkey-patching won't work properly.

coverage/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,9 @@ def copy(self) -> CoverageConfig:
390390
"multiprocessing",
391391
}
392392

393+
# Mutually exclusive concurrency settings.
394+
LIGHT_THREADS = {"greenlet", "eventlet", "gevent"}
395+
393396
CONFIG_FILE_OPTIONS = [
394397
# These are *args for _set_attr_from_config_option:
395398
# (attr, where, type_="")
@@ -563,6 +566,17 @@ def post_process(self) -> None:
563566
if "subprocess" in self.patch:
564567
self.parallel = True
565568

569+
# We can handle a few concurrency options here, but only one at a time.
570+
concurrencies = set(self.concurrency)
571+
unknown = concurrencies - self.CONCURRENCY_CHOICES
572+
if unknown:
573+
show = ", ".join(sorted(unknown))
574+
raise ConfigError(f"Unknown concurrency choices: {show}")
575+
light_threads = concurrencies & self.LIGHT_THREADS
576+
if len(light_threads) > 1:
577+
show = ", ".join(sorted(light_threads))
578+
raise ConfigError(f"Conflicting concurrency settings: {show}")
579+
566580
def debug_info(self) -> list[tuple[str, Any]]:
567581
"""Make a list of (name, value) pairs for writing debug info."""
568582
return human_sorted_items((k, v) for k, v in self.__dict__.items() if not k.startswith("_"))

coverage/control.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ def load(self) -> None:
562562
def _init_for_start(self) -> None:
563563
"""Initialization for start()"""
564564
# Construct the collector.
565-
concurrency: list[str] = self.config.concurrency or []
565+
concurrency: list[str] = self.config.concurrency
566566
if "multiprocessing" in concurrency:
567567
if self.config.config_file is None:
568568
raise ConfigError("multiprocessing requires a configuration file")

coverage/core.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,20 @@ def __init__(
6969
reason_no_sysmon = "can't measure branches in this version"
7070
elif dynamic_contexts:
7171
reason_no_sysmon = "doesn't yet support dynamic contexts"
72+
elif any((bad := c) in config.concurrency for c in ["greenlet", "eventlet", "gevent"]):
73+
reason_no_sysmon = f"doesn't support concurrency={bad}"
7274

7375
core_name: str | None = None
7476
if config.timid:
7577
core_name = "pytrace"
76-
77-
if core_name is None:
78+
elif core_name is None:
79+
# This could still leave core_name as None.
7880
core_name = config.core
7981

8082
if core_name == "sysmon" and reason_no_sysmon:
81-
warn(f"sys.monitoring {reason_no_sysmon}, using default core", slug="no-sysmon")
82-
core_name = None
83+
raise ConfigError(
84+
f"Can't use core=sysmon: sys.monitoring {reason_no_sysmon}", skip_tests=True
85+
)
8386

8487
if core_name is None:
8588
if env.SYSMON_DEFAULT and not reason_no_sysmon:

coverage/exceptions.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,26 @@
1111
class CoverageException(Exception):
1212
"""The base class of all exceptions raised by Coverage.py."""
1313

14-
def __init__(self, *args: Any, slug: str | None = None) -> None:
14+
def __init__(
15+
self,
16+
*args: Any,
17+
slug: str | None = None,
18+
skip_tests: bool = False,
19+
) -> None:
20+
"""Create an exception.
21+
22+
Args:
23+
slug: A short string identifying the exception, will be used for
24+
linking to documentation.
25+
skip_tests: If True, raising this exception will skip the test it
26+
is raised in. This is used for shutting off large numbers of
27+
tests that we know will not succeed because of a configuration
28+
mismatch.
29+
"""
30+
1531
super().__init__(*args)
1632
self.slug = slug
33+
self.skip_tests = skip_tests
1734

1835

1936
class ConfigError(CoverageException):

tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,15 @@ def create_pth_file_fixture() -> Iterable[None]:
104104
finally:
105105
for p in pth_files:
106106
p.unlink()
107+
108+
109+
@pytest.hookimpl(wrapper=True)
110+
def pytest_runtest_call() -> Iterable[None]:
111+
"""Check the exception raised by the test, and skip the test if needed."""
112+
try:
113+
yield
114+
except Exception as e:
115+
if getattr(e, "skip_tests", False):
116+
pytest.skip(f"Skipping for exception: {e}")
117+
else:
118+
raise

tests/test_concurrency.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,11 @@ def cant_trace_msg(concurrency: str, the_module: ModuleType | None) -> str | Non
182182
parts.remove("multiprocessing")
183183
concurrency = ",".join(parts)
184184

185-
if the_module is None:
185+
if testenv.SYS_MON and concurrency:
186+
expected_out = (
187+
f"Can't use core=sysmon: sys.monitoring doesn't support concurrency={concurrency}\n"
188+
)
189+
elif the_module is None:
186190
# We don't even have the underlying module installed, we expect
187191
# coverage to alert us to this fact.
188192
expected_out = (
@@ -251,10 +255,16 @@ def try_some_code(
251255
lines = line_count(code)
252256
assert line_counts(data)["try_it.py"] == lines
253257

258+
@pytest.mark.skipif(
259+
not testenv.CAN_MEASURE_THREADS, reason="Can't measure threads with this core."
260+
)
254261
def test_threads(self) -> None:
255262
code = (THREAD + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT)
256263
self.try_some_code(code, "thread", threading)
257264

265+
@pytest.mark.skipif(
266+
not testenv.CAN_MEASURE_THREADS, reason="Can't measure threads with this core."
267+
)
258268
def test_threads_simple_code(self) -> None:
259269
code = SIMPLE.format(QLIMIT=self.QLIMIT)
260270
self.try_some_code(code, "thread", threading)
@@ -318,6 +328,9 @@ def do():
318328
self.try_some_code(BUG_330, "eventlet", eventlet, "0\n")
319329

320330
# Sometimes a test fails due to inherent randomness. Try more times.
331+
@pytest.mark.skipif(
332+
not testenv.CAN_MEASURE_THREADS, reason="Can't measure threads with this core."
333+
)
321334
@pytest.mark.flaky(max_runs=3)
322335
def test_threads_with_gevent(self) -> None:
323336
self.make_file(
@@ -347,13 +360,17 @@ def gwork(q):
347360
)
348361
_, out = self.run_command_status("coverage run --concurrency=thread,gevent both.py")
349362
if gevent is None:
350-
assert out == ("Couldn't trace with concurrency=gevent, the module isn't installed.\n")
363+
assert "Couldn't trace with concurrency=gevent, the module isn't installed.\n" in out
351364
pytest.skip("Can't run test without gevent installed.")
352365
if not testenv.C_TRACER:
353-
assert out == (
354-
f"Can't support concurrency=gevent with {testenv.REQUESTED_TRACER_CLASS}, "
355-
+ "only threads are supported.\n"
356-
)
366+
if testenv.PY_TRACER:
367+
assert out == (
368+
"Can't support concurrency=gevent with PyTracer, only threads are supported.\n"
369+
)
370+
else:
371+
assert out == (
372+
"Can't use core=sysmon: sys.monitoring doesn't support concurrency=gevent\n"
373+
)
357374
pytest.skip(f"Can't run gevent with {testenv.REQUESTED_TRACER_CLASS}.")
358375

359376
assert out == "done\n"
@@ -392,7 +409,10 @@ class WithoutConcurrencyModuleTest(CoverageTest):
392409
def test_missing_module(self, module: str) -> None:
393410
self.make_file("prog.py", "a = 1")
394411
sys.modules[module] = None # type: ignore[assignment]
395-
msg = f"Couldn't trace with concurrency={module}, the module isn't installed."
412+
if testenv.SYS_MON:
413+
msg = rf"Can't use core=sysmon: sys.monitoring doesn't support concurrency={module}"
414+
else:
415+
msg = rf"Couldn't trace with concurrency={module}, the module isn't installed."
396416
with pytest.raises(ConfigError, match=msg):
397417
self.command_line(f"run --concurrency={module} prog.py")
398418

@@ -554,6 +574,9 @@ def test_multiprocessing_and_gevent(self, start_method: str) -> None:
554574
start_method=start_method,
555575
)
556576

577+
@pytest.mark.skipif(
578+
not testenv.CAN_MEASURE_BRANCHES, reason="Can't measure branches with this core"
579+
)
557580
def test_multiprocessing_with_branching(self, start_method: str) -> None:
558581
nprocs = 3
559582
upto = 30

tests/test_config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def test_toml_config_file(self) -> None:
8383
[tool.somethingelse]
8484
authors = ["Joe D'Ávila <[email protected]>"]
8585
[tool.coverage.run]
86-
concurrency = ["a", "b"]
86+
concurrency = ["thread", "eventlet"]
8787
timid = true
8888
data_file = ".hello_kitty.data"
8989
plugins = ["plugins.a_plugin"]
@@ -99,7 +99,7 @@ def test_toml_config_file(self) -> None:
9999
cov = coverage.Coverage()
100100
assert cov.config.timid
101101
assert not cov.config.branch
102-
assert cov.config.concurrency == ["a", "b"]
102+
assert cov.config.concurrency == ["thread", "eventlet"]
103103
assert cov.config.data_file == ".hello_kitty.data"
104104
assert cov.config.plugins == ["plugins.a_plugin"]
105105
assert cov.config.precision == 3

tests/test_core.py

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,21 @@ def test_core_request_pytrace(self) -> None:
7575

7676
def test_core_request_sysmon(self) -> None:
7777
self.set_environ("COVERAGE_CORE", "sysmon")
78-
out = self.run_command("coverage run --debug=sys numbers.py")
79-
assert out.endswith("123 456\n")
80-
core = re_line(r" core:", out).strip()
81-
warns = re_lines(r"\(no-sysmon\)", out)
8278
if env.PYBEHAVIOR.pep669:
79+
status = 0
80+
else:
81+
status = 1
82+
out = self.run_command("coverage run --debug=sys numbers.py", status=status)
83+
if status == 0:
84+
assert out.endswith("123 456\n")
85+
core = re_line(r" core:", out).strip()
86+
warns = re_lines(r"\(no-sysmon\)", out)
8387
assert core == "core: SysMonitor"
8488
assert not warns
8589
else:
86-
assert core in ["core: CTracer", "core: PyTracer"]
87-
assert warns
90+
assert out.endswith(
91+
"Can't use core=sysmon: sys.monitoring isn't available in this version\n"
92+
)
8893

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

113113
def test_core_request_sysmon_no_branches(self) -> None:
114114
# Use config core= for this test just to be different.
@@ -120,25 +120,23 @@ def test_core_request_sysmon_no_branches(self) -> None:
120120
branch = True
121121
""",
122122
)
123-
out = self.run_command("coverage run --debug=sys numbers.py")
124-
assert out.endswith("123 456\n")
125-
core = re_line(r" core:", out).strip()
126-
warns = re_lines(r"\(no-sysmon\)", out)
127123
if env.PYBEHAVIOR.branch_right_left:
124+
status = 0
125+
elif env.PYBEHAVIOR.pep669:
126+
status = 1
127+
msg = "Can't use core=sysmon: sys.monitoring can't measure branches in this version\n"
128+
else:
129+
status = 1
130+
msg = "Can't use core=sysmon: sys.monitoring isn't available in this version\n"
131+
out = self.run_command("coverage run --debug=sys numbers.py", status=status)
132+
if status == 0:
133+
assert out.endswith("123 456\n")
134+
core = re_line(r" core:", out).strip()
135+
warns = re_lines(r"\(no-sysmon\)", out)
128136
assert core == "core: SysMonitor"
129137
assert not warns
130138
else:
131-
assert core in ["core: CTracer", "core: PyTracer"]
132-
assert len(warns) == 1
133-
if env.PYBEHAVIOR.pep669:
134-
assert (
135-
"sys.monitoring can't measure branches in this version, using default core"
136-
in warns[0]
137-
)
138-
else:
139-
assert (
140-
"sys.monitoring isn't available in this version, using default core" in warns[0]
141-
)
139+
assert out.endswith(msg) # pylint: disable=possibly-used-before-assignment
142140

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

tests/test_process.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,9 @@ def test_subprocess_in_directories(self) -> None:
15161516
assert line_counts(data)["main.py"] == 6
15171517
assert line_counts(data)["subproc.py"] == 2
15181518

1519+
@pytest.mark.skipif(
1520+
not testenv.CAN_MEASURE_BRANCHES, reason="Can't measure branches with this core"
1521+
)
15191522
def test_subprocess_gets_nonfile_config(self) -> None:
15201523
# https://github.com/nedbat/coveragepy/issues/2021
15211524
self.make_file(

0 commit comments

Comments
 (0)