Skip to content

Commit 9ca3552

Browse files
committed
feat(pep669): prevent ghost local events
python/cpython#111963
1 parent e9bf5b0 commit 9ca3552

8 files changed

+432
-340
lines changed

coverage/collector.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from coverage.disposition import FileDisposition
2222
from coverage.exceptions import ConfigError
2323
from coverage.misc import human_sorted_items, isolate_module
24-
from coverage.pep669_tracer import Pep669Tracer
24+
from coverage.pep669_monitor import Pep669Monitor
2525
from coverage.plugin import CoveragePlugin
2626
from coverage.pytracer import PyTracer
2727
from coverage.types import (
@@ -130,6 +130,8 @@ def __init__(
130130
self.concurrency = concurrency
131131
assert isinstance(self.concurrency, list), f"Expected a list: {self.concurrency!r}"
132132

133+
self.pid = os.getpid()
134+
133135
self.covdata: CoverageData
134136
self.threading = None
135137
self.static_context: Optional[str] = None
@@ -153,7 +155,7 @@ def __init__(
153155
core = "ctrace"
154156

155157
if core == "sysmon":
156-
self._trace_class = Pep669Tracer
158+
self._trace_class = Pep669Monitor
157159
self.file_disposition_class = FileDisposition
158160
self.supports_plugins = False
159161
self.packed_arcs = False
@@ -343,6 +345,11 @@ def _installation_trace(self, frame: FrameType, event: str, arg: Any) -> Optiona
343345

344346
def start(self) -> None:
345347
"""Start collecting trace information."""
348+
# We may be a new collector in a forked process. The old process'
349+
# collectors will be in self._collectors, but they won't be usable.
350+
# Find them and discard them.
351+
#self.__class__._collectors = [c for c in self._collectors if c.pid == self.pid]
352+
346353
if self._collectors:
347354
self._collectors[-1].pause()
348355

@@ -360,6 +367,10 @@ def start(self) -> None:
360367
# stack of collectors.
361368
self._collectors.append(self)
362369

370+
with open("/tmp/foo.out", "a") as f:
371+
print(f"pid={os.getpid()} start: {self._collectors = }", file=f)
372+
print(f"start stack:\n{short_stack()}", file=f)
373+
363374
# Install our installation tracer in threading, to jump-start other
364375
# threads.
365376
if self.systrace and self.threading:
@@ -380,6 +391,9 @@ def stop(self) -> None:
380391

381392
# Remove this Collector from the stack, and resume the one underneath
382393
# (if any).
394+
with open("/tmp/foo.out", "a") as f:
395+
print(f"pid={os.getpid()} stop: {self._collectors = }", file=f)
396+
print(f"stop stack:\n{short_stack()}", file=f)
383397
self._collectors.pop()
384398
if self._collectors:
385399
self._collectors[-1].resume()

coverage/control.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
22
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
33

4-
"""Core control stuff for coverage.py."""
4+
"""Central control stuff for coverage.py."""
55

66
from __future__ import annotations
77

coverage/debug.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def short_filename(filename: None) -> None:
192192
pass
193193

194194
def short_filename(filename: Optional[str]) -> Optional[str]:
195-
"""shorten a file name. Directories are replaced by prefixes like 'syspath:'"""
195+
"""Shorten a file name. Directories are replaced by prefixes like 'syspath:'"""
196196
if not _FILENAME_SUBS:
197197
for pathdir in sys.path:
198198
_FILENAME_SUBS.append((pathdir, "syspath:"))

coverage/multiproc.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ def _bootstrap(self, *args, **kwargs): # type: ignore[no-untyped-def]
3737
assert debug is not None
3838
if debug.should("multiproc"):
3939
debug.write("Calling multiprocessing bootstrap")
40-
except Exception:
40+
except Exception as exc:
41+
with open("/tmp/foo.out", "a") as f:
42+
print(f"Exception during multiprocessing bootstrap init: {exc.__class__.__name__}: {exc}", file=f)
4143
print("Exception during multiprocessing bootstrap init:")
4244
traceback.print_exc(file=sys.stdout)
4345
sys.stdout.flush()
@@ -47,8 +49,18 @@ def _bootstrap(self, *args, **kwargs): # type: ignore[no-untyped-def]
4749
finally:
4850
if debug.should("multiproc"):
4951
debug.write("Finished multiprocessing bootstrap")
50-
cov.stop()
51-
cov.save()
52+
try:
53+
cov.stop()
54+
except Exception as exc:
55+
with open("/tmp/foo.out", "a") as f:
56+
print(f"Exception during multiprocessing bootstrap finally stop: {exc = }", file=f)
57+
raise
58+
try:
59+
cov.save()
60+
except Exception as exc:
61+
with open("/tmp/foo.out", "a") as f:
62+
print(f"Exception during multiprocessing bootstrap finally save: {exc = }", file=f)
63+
raise
5264
if debug.should("multiproc"):
5365
debug.write("Saved multiprocessing data")
5466

@@ -86,7 +98,7 @@ def patch_multiprocessing(rcfile: str) -> None:
8698
# When spawning processes rather than forking them, we have no state in the
8799
# new process. We sneak in there with a Stowaway: we stuff one of our own
88100
# objects into the data that gets pickled and sent to the sub-process. When
89-
# the Stowaway is unpickled, it's __setstate__ method is called, which
101+
# the Stowaway is unpickled, its __setstate__ method is called, which
90102
# re-applies the monkey-patch.
91103
# Windows only spawns, so this is needed to keep Windows working.
92104
try:

0 commit comments

Comments
 (0)