Skip to content

Commit 8c88177

Browse files
committed
wip
1 parent eab4ae5 commit 8c88177

File tree

7 files changed

+71
-64
lines changed

7 files changed

+71
-64
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
@@ -560,7 +560,7 @@ def load(self) -> None:
560560
def _init_for_start(self) -> None:
561561
"""Initialization for start()"""
562562
# Construct the collector.
563-
concurrency: list[str] = self.config.concurrency or []
563+
concurrency: list[str] = self.config.concurrency
564564
if "multiprocessing" in concurrency:
565565
if self.config.config_file is None:
566566
raise ConfigError("multiprocessing requires a configuration file")

coverage/core.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,18 @@ 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(f"sys.monitoring {reason_no_sysmon}, can't use core=sysmon")
8384

8485
if core_name is None:
8586
if env.SYSMON_DEFAULT and not reason_no_sysmon:

tests/test_concurrency.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ def cant_trace_msg(concurrency: str, the_module: ModuleType | None) -> str | Non
190190
)
191191
elif testenv.C_TRACER or concurrency == "thread" or concurrency == "":
192192
expected_out = None
193+
elif testenv.SYS_MON:
194+
expected_out = (
195+
f"sys.monitoring doesn't support concurrency={concurrency}, can't use core=sysmon\n"
196+
)
193197
else:
194198
expected_out = (
195199
f"Can't support concurrency={concurrency} with {testenv.REQUESTED_TRACER_CLASS}, "
@@ -347,13 +351,17 @@ def gwork(q):
347351
)
348352
_, out = self.run_command_status("coverage run --concurrency=thread,gevent both.py")
349353
if gevent is None:
350-
assert out == ("Couldn't trace with concurrency=gevent, the module isn't installed.\n")
354+
assert "Couldn't trace with concurrency=gevent, the module isn't installed.\n" in out
351355
pytest.skip("Can't run test without gevent installed.")
352356
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-
)
357+
if testenv.PY_TRACER:
358+
assert out == (
359+
"Can't support concurrency=gevent with PyTracer, only threads are supported.\n"
360+
)
361+
else:
362+
assert out == (
363+
"sys.monitoring doesn't support concurrency=gevent, can't use core=sysmon\n"
364+
)
357365
pytest.skip(f"Can't run gevent with {testenv.REQUESTED_TRACER_CLASS}.")
358366

359367
assert out == "done\n"
@@ -392,7 +400,10 @@ class WithoutConcurrencyModuleTest(CoverageTest):
392400
def test_missing_module(self, module: str) -> None:
393401
self.make_file("prog.py", "a = 1")
394402
sys.modules[module] = None # type: ignore[assignment]
395-
msg = f"Couldn't trace with concurrency={module}, the module isn't installed."
403+
if testenv.SYS_MON:
404+
msg = rf"sys.monitoring doesn't support concurrency={module}, can't use core=sysmon"
405+
else:
406+
msg = rf"Couldn't trace with concurrency={module}, the module isn't installed."
396407
with pytest.raises(ConfigError, match=msg):
397408
self.command_line(f"run --concurrency={module} prog.py")
398409

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: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,19 @@ 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 == "sys.monitoring isn't available in this version, can't use core=sysmon\n"
8891

8992
def test_core_request_sysmon_no_dyncontext(self) -> None:
9093
# Use config core= for this test just to be different.
@@ -96,19 +99,14 @@ def test_core_request_sysmon_no_dyncontext(self) -> None:
9699
dynamic_context = test_function
97100
""",
98101
)
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
102+
out = self.run_command("coverage run --debug=sys numbers.py", status=1)
105103
if env.PYBEHAVIOR.pep669:
106104
assert (
107-
"sys.monitoring doesn't yet support dynamic contexts, using default core"
108-
in warns[0]
105+
"sys.monitoring doesn't yet support dynamic contexts, can't use core=sysmon\n"
106+
in out
109107
)
110108
else:
111-
assert "sys.monitoring isn't available in this version, using default core" in warns[0]
109+
assert "sys.monitoring isn't available in this version, can't use core=sysmon\n" in out
112110

113111
def test_core_request_sysmon_no_branches(self) -> None:
114112
# Use config core= for this test just to be different.
@@ -120,25 +118,23 @@ def test_core_request_sysmon_no_branches(self) -> None:
120118
branch = True
121119
""",
122120
)
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)
127121
if env.PYBEHAVIOR.branch_right_left:
122+
status = 0
123+
elif env.PYBEHAVIOR.pep669:
124+
status = 1
125+
msg = "sys.monitoring can't measure branches in this version, can't use core=sysmon\n"
126+
else:
127+
status = 1
128+
msg = "sys.monitoring isn't available in this version, can't use core=sysmon\n"
129+
out = self.run_command("coverage run --debug=sys numbers.py", status=status)
130+
if status == 0:
131+
assert out.endswith("123 456\n")
132+
core = re_line(r" core:", out).strip()
133+
warns = re_lines(r"\(no-sysmon\)", out)
128134
assert core == "core: SysMonitor"
129135
assert not warns
130136
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-
)
137+
assert out == msg
142138

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

0 commit comments

Comments
 (0)