Coverage for coverage / collector.py: 63.950%
229 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
4"""Raw data collector for coverage.py."""
6from __future__ import annotations 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
8import contextlib 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
9import functools 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
10import os 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
11import sys 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
12from collections.abc import Mapping 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
13from types import FrameType 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
14from typing import Any, Callable, TypeVar, cast 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
16from coverage import env 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
17from coverage.core import Core 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
18from coverage.data import CoverageData 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
19from coverage.debug import short_stack 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
20from coverage.exceptions import ConfigError 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
21from coverage.misc import human_sorted_items, isolate_module 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
22from coverage.plugin import CoveragePlugin 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
23from coverage.types import ( 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
24 TArc,
25 TCheckIncludeFn,
26 TFileDisposition,
27 Tracer,
28 TShouldStartContextFn,
29 TShouldTraceFn,
30 TTraceData,
31 TTraceFn,
32 TWarnFn,
33)
35os = isolate_module(os) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
38T = TypeVar("T") 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
41class Collector: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
42 """Collects trace data.
44 Creates a Tracer object for each thread, since they track stack
45 information. Each Tracer points to the same shared data, contributing
46 traced data points.
48 When the Collector is started, it creates a Tracer for the current thread,
49 and installs a function to create Tracers for each new thread started.
50 When the Collector is stopped, all active Tracers are stopped.
52 Threads started while the Collector is stopped will never have Tracers
53 associated with them.
55 """
57 # The stack of active Collectors. Collectors are added here when started,
58 # and popped when stopped. Collectors on the stack are paused when not
59 # the top, and resumed when they become the top again.
60 _collectors: list[Collector] = [] 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
62 def __init__( 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
63 self,
64 core: Core,
65 should_trace: TShouldTraceFn,
66 check_include: TCheckIncludeFn,
67 should_start_context: TShouldStartContextFn | None,
68 file_mapper: Callable[[str], str],
69 branch: bool,
70 warn: TWarnFn,
71 concurrency: list[str],
72 ) -> None:
73 """Create a collector.
75 `should_trace` is a function, taking a file name and a frame, and
76 returning a `coverage.FileDisposition object`.
78 `check_include` is a function taking a file name and a frame. It returns
79 a boolean: True if the file should be traced, False if not.
81 `should_start_context` is a function taking a frame, and returning a
82 string. If the frame should be the start of a new context, the string
83 is the new context. If the frame should not be the start of a new
84 context, return None.
86 `file_mapper` is a function taking a filename, and returning a Unicode
87 filename. The result is the name that will be recorded in the data
88 file.
90 If `branch` is true, then branches will be measured. This involves
91 collecting data on which statements followed each other (arcs). Use
92 `get_arc_data` to get the arc data.
94 `warn` is a warning function, taking a single string message argument
95 and an optional slug argument which will be a string or None, to be
96 used if a warning needs to be issued.
98 `concurrency` is a list of strings indicating the concurrency libraries
99 in use. Valid values are "greenlet", "eventlet", "gevent", or "thread"
100 (the default). "thread" can be combined with one of the other three.
101 Other values are ignored.
103 """
104 self.core = core 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
105 self.should_trace = should_trace 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
106 self.check_include = check_include 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
107 self.should_start_context = should_start_context 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
108 self.file_mapper = file_mapper 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
109 self.branch = branch 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
110 self.warn = warn 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
111 assert isinstance(concurrency, list), f"Expected a list: {concurrency!r}" 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
113 self.pid = os.getpid() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
115 self.covdata: CoverageData 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
116 self.threading = None 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
117 self.static_context: str | None = None 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
119 self.origin = short_stack() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
121 self.concur_id_func = None 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
123 do_threading = False 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
125 tried = "nothing" # to satisfy pylint 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
126 try: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
127 if "greenlet" in concurrency: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
128 tried = "greenlet" 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
129 import greenlet 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
131 self.concur_id_func = greenlet.getcurrent 1dBeCfDgEhFiGjHkIlJmKnLoMaN5bO6cP7QRS
132 elif "eventlet" in concurrency: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
133 tried = "eventlet" 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
134 import eventlet.greenthread 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
136 self.concur_id_func = eventlet.greenthread.getcurrent 1dBeCfDgEhFiGjHkIlJmKnLoMaN5bO6cP7
137 elif "gevent" in concurrency: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
138 tried = "gevent" 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
139 import gevent 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
141 self.concur_id_func = gevent.getcurrent 1dBeCfDgEhFiGjHkIlJmKnLoMaN5bO6cP7
143 if "thread" in concurrency: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
144 do_threading = True 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpWqXrYsZt0u1v2w3x4QRS
145 except ImportError as ex: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
146 msg = f"Couldn't trace with concurrency={tried}, the module isn't installed." 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
147 raise ConfigError(msg) from ex 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
149 if self.concur_id_func and not hasattr(core.tracer_class, "concur_id_func"): 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
150 raise ConfigError( 1BCDEFGHIJKLMN5O6P7QRS
151 "Can't support concurrency={} with {}, only threads are supported.".format(
152 tried,
153 self.tracer_name(),
154 ),
155 )
157 if do_threading or not concurrency: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
158 # It's important to import threading only if we need it. If
159 # it's imported early, and the program being measured uses
160 # gevent, then gevent's monkey-patching won't work properly.
161 import threading 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
163 self.threading = threading 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
165 self.reset() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
167 def __repr__(self) -> str: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
168 return f"<Collector at {id(self):#x}: {self.tracer_name()}>"
170 def use_data(self, covdata: CoverageData, context: str | None) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
171 """Use `covdata` for recording data."""
172 self.covdata = covdata 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
173 self.static_context = context 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
174 self.covdata.set_context(self.static_context) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
176 def tracer_name(self) -> str: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
177 """Return the class name of the tracer we're using."""
178 return self.core.tracer_class.__name__ 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
180 def _clear_data(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
181 """Clear out existing data, but stay ready for more collection."""
182 # We used to use self.data.clear(), but that would remove filename
183 # keys and data values that were still in use higher up the stack
184 # when we are called as part of switch_context.
185 with self.data_lock or contextlib.nullcontext(): 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
186 for d in self.data.values(): 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
187 d.clear() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
189 for tracer in self.tracers: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
190 tracer.reset_activity() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
192 def reset(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
193 """Clear collected data, and prepare to collect more."""
194 self.data_lock = self.threading.Lock() if self.threading else None 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
196 # The trace data we are collecting.
197 self.data: TTraceData = {} 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
199 # A dictionary mapping file names to file tracer plugin names that will
200 # handle them.
201 self.file_tracers: dict[str, str] = {} 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
203 self.disabled_plugins: set[str] = set() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
205 # The .should_trace_cache attribute is a cache from file names to
206 # coverage.FileDisposition objects, or None. When a file is first
207 # considered for tracing, a FileDisposition is obtained from
208 # Coverage.should_trace. Its .trace attribute indicates whether the
209 # file should be traced or not. If it should be, a plugin with dynamic
210 # file names can decide not to trace it based on the dynamic file name
211 # being excluded by the inclusion rules, in which case the
212 # FileDisposition will be replaced by None in the cache.
213 if env.PYPY: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
214 import __pypy__ # pylint: disable=import-error 1QRS
216 # Alex Gaynor said:
217 # should_trace_cache is a strictly growing key: once a key is in
218 # it, it never changes. Further, the keys used to access it are
219 # generally constant, given sufficient context. That is to say, at
220 # any given point _trace() is called, pypy is able to know the key.
221 # This is because the key is determined by the physical source code
222 # line, and that's invariant with the call site.
223 #
224 # This property of a dict with immutable keys, combined with
225 # call-site-constant keys is a match for PyPy's module dict,
226 # which is optimized for such workloads.
227 #
228 # This gives a 20% benefit on the workload described at
229 # https://bitbucket.org/pypy/pypy/issue/1871/10x-slower-than-cpython-under-coverage
230 self.should_trace_cache = __pypy__.newdict("module") 1QRS
231 else:
232 self.should_trace_cache = {} 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)
234 # Our active Tracers.
235 self.tracers: list[Tracer] = [] 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
237 self._clear_data() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
239 def lock_data(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
240 """Lock self.data_lock, for use by the C tracer."""
241 if self.data_lock is not None:
242 self.data_lock.acquire()
244 def unlock_data(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
245 """Unlock self.data_lock, for use by the C tracer."""
246 if self.data_lock is not None:
247 self.data_lock.release()
249 def _start_tracer(self) -> TTraceFn | None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
250 """Start a new Tracer object, and store it in self.tracers."""
251 tracer = self.core.tracer_class(**self.core.tracer_kwargs)
252 tracer.data = self.data
253 tracer.lock_data = self.lock_data
254 tracer.unlock_data = self.unlock_data
255 tracer.trace_arcs = self.branch
256 tracer.should_trace = self.should_trace
257 tracer.should_trace_cache = self.should_trace_cache
258 tracer.warn = self.warn
260 if hasattr(tracer, "concur_id_func"):
261 tracer.concur_id_func = self.concur_id_func
262 if hasattr(tracer, "file_tracers"):
263 tracer.file_tracers = self.file_tracers
264 if hasattr(tracer, "threading"):
265 tracer.threading = self.threading
266 if hasattr(tracer, "check_include"):
267 tracer.check_include = self.check_include
268 if hasattr(tracer, "should_start_context"):
269 tracer.should_start_context = self.should_start_context
270 if hasattr(tracer, "switch_context"):
271 tracer.switch_context = self.switch_context
272 if hasattr(tracer, "disable_plugin"):
273 tracer.disable_plugin = self.disable_plugin
275 fn = tracer.start()
276 self.tracers.append(tracer)
278 return fn
280 # The trace function has to be set individually on each thread before
281 # execution begins. Ironically, the only support the threading module has
282 # for running code before the thread main is the tracing function. So we
283 # install this as a trace function, and the first time it's called, it does
284 # the real trace installation.
285 #
286 # New in 3.12: threading.settrace_all_threads: https://github.com/python/cpython/pull/96681
288 def _installation_trace(self, frame: FrameType, event: str, arg: Any) -> TTraceFn | None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
289 """Called on new threads, installs the real tracer."""
290 # Remove ourselves as the trace function.
291 sys.settrace(None)
292 # Install the real tracer.
293 fn: TTraceFn | None = self._start_tracer()
294 # Invoke the real trace function with the current event, to be sure
295 # not to lose an event.
296 if fn:
297 fn = fn(frame, event, arg)
298 # Return the new trace function to continue tracing in this scope.
299 return fn
301 def start(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
302 """Start collecting trace information."""
303 # We may be a new collector in a forked process. The old process'
304 # collectors will be in self._collectors, but they won't be usable.
305 # Find them and discard them.
306 keep_collectors = [] 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
307 for c in self._collectors: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
308 if c.pid == self.pid: 308 ↛ 311line 308 didn't jump to line 311 because the condition on line 308 was always true1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
309 keep_collectors.append(c) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
310 else:
311 c.post_fork()
312 self._collectors[:] = keep_collectors 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
314 if self._collectors: 314 ↛ 317line 314 didn't jump to line 317 because the condition on line 314 was always true1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
315 self._collectors[-1].pause() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
317 self.tracers = []
319 try:
320 # Install the tracer on this thread.
321 self._start_tracer() 1QRS
322 except:
323 if self._collectors:
324 self._collectors[-1].resume()
325 raise
327 # If _start_tracer succeeded, then we add ourselves to the global
328 # stack of collectors.
329 self._collectors.append(self) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
331 # Install our installation tracer in threading, to jump-start other
332 # threads.
333 if self.core.systrace and self.threading: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
334 self.threading.settrace(self._installation_trace) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
336 def stop(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
337 """Stop collecting trace information."""
338 assert self._collectors 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
339 if self._collectors[-1] is not self: 339 ↛ 340line 339 didn't jump to line 340 because the condition on line 339 was never true1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
340 print("self._collectors:")
341 for c in self._collectors:
342 print(f" {c!r}\n{c.origin}")
343 assert self._collectors[-1] is self, ( 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
344 f"Expected current collector to be {self!r}, but it's {self._collectors[-1]!r}"
345 )
347 self.pause() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
349 # Remove this Collector from the stack, and resume the one underneath (if any).
350 self._collectors.pop()
351 if self._collectors:
352 self._collectors[-1].resume()
354 def pause(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
355 """Pause tracing, but be prepared to `resume`."""
356 for tracer in self.tracers: 356 ↛ 363line 356 didn't jump to line 363 because the loop on line 356 didn't complete1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
357 tracer.stop() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
358 stats = tracer.get_stats() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpWqXrYsZt0u1v2w3x4QRS
359 if stats: 359 ↛ 360line 359 didn't jump to line 360 because the condition on line 359 was never true1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpWqXrYsZt0u1v2w3x4QRS
360 print(f"\nCoverage.py {tracer.__class__.__name__} stats:")
361 for k, v in human_sorted_items(stats.items()):
362 print(f"{k:>20}: {v}")
363 if self.threading:
364 self.threading.settrace(None)
366 def resume(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
367 """Resume tracing after a `pause`."""
368 for tracer in self.tracers: 1defghijklmnoyzAabcpqrstuvwx
369 tracer.start() 1defghijklmnoyzAabcpqrstuvwx
370 if self.core.systrace: 370 ↛ exitline 370 didn't return from function 'resume' because the condition on line 370 was always true1defghijklmnoyzAabcpqrstuvwx
371 if self.threading: 371 ↛ 374line 371 didn't jump to line 374 because the condition on line 371 was always true1defghijklmnoyzAabcpqrstuvwx
372 self.threading.settrace(self._installation_trace) 1defghijklmnoyzAabcpqrstuvwx
373 else:
374 self._start_tracer()
376 def post_fork(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
377 """After a fork, tracers might need to adjust."""
378 for tracer in self.tracers:
379 if hasattr(tracer, "post_fork"):
380 tracer.post_fork()
382 def _activity(self) -> bool: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
383 """Has any activity been traced?
385 Returns a boolean, True if any trace function was invoked.
387 """
388 return any(tracer.activity() for tracer in self.tracers) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
390 def switch_context(self, new_context: str | None) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
391 """Switch to a new dynamic context."""
392 context: str | None
393 self.flush_data()
394 if self.static_context:
395 context = self.static_context
396 if new_context:
397 context += "|" + new_context
398 else:
399 context = new_context
400 self.covdata.set_context(context)
402 def disable_plugin(self, disposition: TFileDisposition) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
403 """Disable the plugin mentioned in `disposition`."""
404 file_tracer = disposition.file_tracer
405 assert file_tracer is not None
406 plugin = file_tracer._coverage_plugin
407 plugin_name = plugin._coverage_plugin_name
408 self.warn(f"Disabling plug-in {plugin_name!r} due to previous exception")
409 plugin._coverage_enabled = False
410 disposition.trace = False
412 @functools.cache # pylint: disable=method-cache-max-size-none 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
413 def cached_mapped_file(self, filename: str) -> str: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
414 """A locally cached version of file names mapped through file_mapper."""
415 return self.file_mapper(filename) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
417 def mapped_file_dict(self, d: Mapping[str, T]) -> dict[str, T]: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
418 """Return a dict like d, but with keys modified by file_mapper."""
419 # The call to list(items()) ensures that the GIL protects the dictionary
420 # iterator against concurrent modifications by tracers running
421 # in other threads. We try three times in case of concurrent
422 # access, hoping to get a clean copy.
423 runtime_err = None 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
424 for _ in range(3): # pragma: part covered 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
425 try: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
426 items = list(d.items()) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
427 except RuntimeError as ex: # pragma: cant happen
428 runtime_err = ex
429 else:
430 break 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
431 else: # pragma: cant happen
432 assert isinstance(runtime_err, Exception)
433 raise runtime_err
435 return {self.cached_mapped_file(k): v for k, v in items if v} 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
437 def plugin_was_disabled(self, plugin: CoveragePlugin) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
438 """Record that `plugin` was disabled during the run."""
439 self.disabled_plugins.add(plugin._coverage_plugin_name) 1defghijklmnoyzAabcpqrstuvwx
441 def flush_data(self) -> bool: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
442 """Save the collected data to our associated `CoverageData`.
444 Data may have also been saved along the way. This forces the
445 last of the data to be saved.
447 Returns True if there was data to save, False if not.
448 """
449 if not self._activity(): 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
450 return False 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
452 if self.branch: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
453 if self.core.packed_arcs: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
454 # Unpack the line number pairs packed into integers. See
455 # tracer.c:CTracer_record_pair for the C code that creates
456 # these packed ints.
457 arc_data: dict[str, list[TArc]] = {} 1defghijklmnoyzAabcpqrstuvwx
458 packed_data = cast(dict[str, set[int]], self.data) 1defghijklmnoyzAabcpqrstuvwx
460 # The list() here and in the inner loop are to get a clean copy
461 # even as tracers are continuing to add data.
462 for fname, packeds in list(packed_data.items()): 1defghijklmnoyzAabcpqrstuvwx
463 tuples = [] 1defghijklmnoyzAabcpqrstuvwx
464 for packed in list(packeds): 1defghijklmnoyzAabcpqrstuvwx
465 l1 = packed & 0xFFFFF 1defghijklmnoyzAabcpqrstuvwx
466 l2 = (packed & (0xFFFFF << 20)) >> 20 1defghijklmnoyzAabcpqrstuvwx
467 if packed & (1 << 40): 1defghijklmnoyzAabcpqrstuvwx
468 l1 *= -1 1defghijklmnoyzAabcpqrstuvwx
469 if packed & (1 << 41): 1defghijklmnoyzAabcpqrstuvwx
470 l2 *= -1 1defghijklmnoyzAabcpqrstuvwx
471 tuples.append((l1, l2)) 1defghijklmnoyzAabcpqrstuvwx
472 arc_data[fname] = tuples 1defghijklmnoyzAabcpqrstuvwx
473 else:
474 arc_data = cast(dict[str, list[TArc]], self.data) 1BCDEFGHIJKLMTUVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
475 self.covdata.add_arcs(self.mapped_file_dict(arc_data)) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
476 else:
477 line_data = cast(dict[str, set[int]], self.data) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
478 self.covdata.add_lines(self.mapped_file_dict(line_data)) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
480 file_tracers = { 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
481 k: v for k, v in self.file_tracers.items() if v not in self.disabled_plugins
482 }
483 self.covdata.add_file_tracers(self.mapped_file_dict(file_tracers)) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
485 self._clear_data() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS
486 return True 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS