coverage 7.6.7__cp311-cp311-win_amd64.whl → 7.11.1__cp311-cp311-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coverage/__init__.py +2 -0
- coverage/__main__.py +2 -0
- coverage/annotate.py +1 -2
- coverage/bytecode.py +177 -3
- coverage/cmdline.py +329 -154
- coverage/collector.py +31 -42
- coverage/config.py +166 -62
- coverage/context.py +4 -5
- coverage/control.py +164 -85
- coverage/core.py +70 -33
- coverage/data.py +3 -4
- coverage/debug.py +112 -56
- coverage/disposition.py +1 -0
- coverage/env.py +65 -55
- coverage/exceptions.py +35 -7
- coverage/execfile.py +18 -13
- coverage/files.py +23 -18
- coverage/html.py +134 -88
- coverage/htmlfiles/style.css +42 -2
- coverage/htmlfiles/style.scss +65 -1
- coverage/inorout.py +61 -44
- coverage/jsonreport.py +17 -8
- coverage/lcovreport.py +16 -20
- coverage/misc.py +50 -46
- coverage/multiproc.py +12 -7
- coverage/numbits.py +3 -4
- coverage/parser.py +193 -269
- coverage/patch.py +166 -0
- coverage/phystokens.py +24 -25
- coverage/plugin.py +13 -13
- coverage/plugin_support.py +36 -35
- coverage/python.py +9 -13
- coverage/pytracer.py +40 -33
- coverage/regions.py +2 -1
- coverage/report.py +59 -43
- coverage/report_core.py +6 -9
- coverage/results.py +118 -66
- coverage/sqldata.py +260 -210
- coverage/sqlitedb.py +33 -25
- coverage/sysmon.py +195 -157
- coverage/templite.py +6 -6
- coverage/tomlconfig.py +12 -12
- coverage/tracer.cp311-win_amd64.pyd +0 -0
- coverage/tracer.pyi +2 -0
- coverage/types.py +25 -22
- coverage/version.py +3 -18
- coverage/xmlreport.py +16 -13
- {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/METADATA +40 -18
- coverage-7.11.1.dist-info/RECORD +59 -0
- {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/WHEEL +1 -1
- coverage-7.6.7.dist-info/RECORD +0 -58
- {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/entry_points.txt +0 -0
- {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info/licenses}/LICENSE.txt +0 -0
- {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/top_level.txt +0 -0
coverage/sqlitedb.py
CHANGED
|
@@ -8,9 +8,8 @@ from __future__ import annotations
|
|
|
8
8
|
import contextlib
|
|
9
9
|
import re
|
|
10
10
|
import sqlite3
|
|
11
|
-
|
|
12
|
-
from typing import cast, Any
|
|
13
11
|
from collections.abc import Iterable, Iterator
|
|
12
|
+
from typing import Any, cast
|
|
14
13
|
|
|
15
14
|
from coverage.debug import auto_repr, clipped_repr, exc_one_line
|
|
16
15
|
from coverage.exceptions import DataError
|
|
@@ -29,9 +28,11 @@ class SqliteDb:
|
|
|
29
28
|
etc(a, b)
|
|
30
29
|
|
|
31
30
|
"""
|
|
32
|
-
|
|
31
|
+
|
|
32
|
+
def __init__(self, filename: str, debug: TDebugCtl, no_disk: bool = False) -> None:
|
|
33
33
|
self.debug = debug
|
|
34
34
|
self.filename = filename
|
|
35
|
+
self.no_disk = no_disk
|
|
35
36
|
self.nest = 0
|
|
36
37
|
self.con: sqlite3.Connection | None = None
|
|
37
38
|
|
|
@@ -50,7 +51,11 @@ class SqliteDb:
|
|
|
50
51
|
if self.debug.should("sql"):
|
|
51
52
|
self.debug.write(f"Connecting to {self.filename!r}")
|
|
52
53
|
try:
|
|
53
|
-
|
|
54
|
+
# Use uri=True when connecting to memory URIs
|
|
55
|
+
if self.filename.startswith("file:"):
|
|
56
|
+
self.con = sqlite3.connect(self.filename, check_same_thread=False, uri=True)
|
|
57
|
+
else:
|
|
58
|
+
self.con = sqlite3.connect(self.filename, check_same_thread=False)
|
|
54
59
|
except sqlite3.Error as exc:
|
|
55
60
|
raise DataError(f"Couldn't use data file {self.filename!r}: {exc}") from exc
|
|
56
61
|
|
|
@@ -64,8 +69,9 @@ class SqliteDb:
|
|
|
64
69
|
# In Python 3.12+, we can change the config to allow journal_mode=off.
|
|
65
70
|
if hasattr(sqlite3, "SQLITE_DBCONFIG_DEFENSIVE"):
|
|
66
71
|
# Turn off defensive mode, so that journal_mode=off can succeed.
|
|
67
|
-
self.con.setconfig(
|
|
68
|
-
sqlite3.SQLITE_DBCONFIG_DEFENSIVE,
|
|
72
|
+
self.con.setconfig( # type: ignore[attr-defined, unused-ignore]
|
|
73
|
+
sqlite3.SQLITE_DBCONFIG_DEFENSIVE,
|
|
74
|
+
False,
|
|
69
75
|
)
|
|
70
76
|
|
|
71
77
|
# This pragma makes writing faster. It disables rollbacks, but we never need them.
|
|
@@ -76,13 +82,14 @@ class SqliteDb:
|
|
|
76
82
|
# to keep things going.
|
|
77
83
|
self.execute_void("pragma synchronous=off", fail_ok=True)
|
|
78
84
|
|
|
79
|
-
def close(self) -> None:
|
|
85
|
+
def close(self, force: bool = False) -> None:
|
|
80
86
|
"""If needed, close the connection."""
|
|
81
|
-
if self.con is not None
|
|
82
|
-
if self.
|
|
83
|
-
self.debug.
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
if self.con is not None:
|
|
88
|
+
if force or not self.no_disk:
|
|
89
|
+
if self.debug.should("sql"):
|
|
90
|
+
self.debug.write(f"Closing {self.con!r} on {self.filename!r}")
|
|
91
|
+
self.con.close()
|
|
92
|
+
self.con = None
|
|
86
93
|
|
|
87
94
|
def __enter__(self) -> SqliteDb:
|
|
88
95
|
if self.nest == 0:
|
|
@@ -92,7 +99,7 @@ class SqliteDb:
|
|
|
92
99
|
self.nest += 1
|
|
93
100
|
return self
|
|
94
101
|
|
|
95
|
-
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
102
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None: # type: ignore[no-untyped-def]
|
|
96
103
|
self.nest -= 1
|
|
97
104
|
if self.nest == 0:
|
|
98
105
|
try:
|
|
@@ -112,15 +119,15 @@ class SqliteDb:
|
|
|
112
119
|
try:
|
|
113
120
|
assert self.con is not None
|
|
114
121
|
try:
|
|
115
|
-
return self.con.execute(sql, parameters)
|
|
122
|
+
return self.con.execute(sql, parameters) # type: ignore[arg-type]
|
|
116
123
|
except Exception:
|
|
117
124
|
# In some cases, an error might happen that isn't really an
|
|
118
125
|
# error. Try again immediately.
|
|
119
126
|
# https://github.com/nedbat/coveragepy/issues/1010
|
|
120
|
-
return self.con.execute(sql, parameters)
|
|
127
|
+
return self.con.execute(sql, parameters) # type: ignore[arg-type]
|
|
121
128
|
except sqlite3.Error as exc:
|
|
122
129
|
msg = str(exc)
|
|
123
|
-
if self.
|
|
130
|
+
if not self.no_disk:
|
|
124
131
|
try:
|
|
125
132
|
# `execute` is the first thing we do with the database, so try
|
|
126
133
|
# hard to provide useful hints if something goes wrong now.
|
|
@@ -128,8 +135,8 @@ class SqliteDb:
|
|
|
128
135
|
cov4_sig = b"!coverage.py: This is a private format"
|
|
129
136
|
if bad_file.read(len(cov4_sig)) == cov4_sig:
|
|
130
137
|
msg = (
|
|
131
|
-
"Looks like a coverage 4.x data file. "
|
|
132
|
-
"Are you mixing versions of coverage?"
|
|
138
|
+
"Looks like a coverage 4.x data file. "
|
|
139
|
+
+ "Are you mixing versions of coverage?"
|
|
133
140
|
)
|
|
134
141
|
except Exception:
|
|
135
142
|
pass
|
|
@@ -210,18 +217,19 @@ class SqliteDb:
|
|
|
210
217
|
# https://github.com/nedbat/coveragepy/issues/1010
|
|
211
218
|
return self.con.executemany(sql, data)
|
|
212
219
|
|
|
213
|
-
def executemany_void(self, sql: str, data:
|
|
220
|
+
def executemany_void(self, sql: str, data: list[Any]) -> None:
|
|
214
221
|
"""Same as :meth:`python:sqlite3.Connection.executemany` when you don't need the cursor."""
|
|
215
|
-
|
|
216
|
-
if data:
|
|
217
|
-
self._executemany(sql, data).close()
|
|
222
|
+
self._executemany(sql, data).close()
|
|
218
223
|
|
|
219
224
|
def executescript(self, script: str) -> None:
|
|
220
225
|
"""Same as :meth:`python:sqlite3.Connection.executescript`."""
|
|
221
226
|
if self.debug.should("sql"):
|
|
222
|
-
self.debug.write(
|
|
223
|
-
|
|
224
|
-
|
|
227
|
+
self.debug.write(
|
|
228
|
+
"Executing script with {} chars: {}".format(
|
|
229
|
+
len(script),
|
|
230
|
+
clipped_repr(script, 100),
|
|
231
|
+
)
|
|
232
|
+
)
|
|
225
233
|
assert self.con is not None
|
|
226
234
|
self.con.executescript(script).close()
|
|
227
235
|
|
coverage/sysmon.py
CHANGED
|
@@ -12,46 +12,48 @@ import os.path
|
|
|
12
12
|
import sys
|
|
13
13
|
import threading
|
|
14
14
|
import traceback
|
|
15
|
-
|
|
16
15
|
from dataclasses import dataclass
|
|
17
|
-
from types import CodeType
|
|
18
|
-
from typing import
|
|
19
|
-
Any,
|
|
20
|
-
Callable,
|
|
21
|
-
TYPE_CHECKING,
|
|
22
|
-
cast,
|
|
23
|
-
)
|
|
16
|
+
from types import CodeType
|
|
17
|
+
from typing import Any, Callable, NewType, Optional, cast
|
|
24
18
|
|
|
19
|
+
from coverage import env
|
|
20
|
+
from coverage.bytecode import TBranchTrails, always_jumps, branch_trails
|
|
25
21
|
from coverage.debug import short_filename, short_stack
|
|
26
22
|
from coverage.misc import isolate_module
|
|
23
|
+
from coverage.parser import PythonParser
|
|
27
24
|
from coverage.types import (
|
|
28
25
|
AnyCallable,
|
|
29
|
-
TArc,
|
|
30
26
|
TFileDisposition,
|
|
31
27
|
TLineNo,
|
|
28
|
+
TOffset,
|
|
29
|
+
Tracer,
|
|
32
30
|
TShouldStartContextFn,
|
|
33
31
|
TShouldTraceFn,
|
|
34
32
|
TTraceData,
|
|
35
33
|
TTraceFileData,
|
|
36
|
-
Tracer,
|
|
37
34
|
TWarnFn,
|
|
38
35
|
)
|
|
39
36
|
|
|
37
|
+
# Only needed for some of the commented-out logging:
|
|
38
|
+
# from coverage.debug import ppformat
|
|
39
|
+
|
|
40
40
|
os = isolate_module(os)
|
|
41
41
|
|
|
42
42
|
# pylint: disable=unused-argument
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
# $set_env.py: COVERAGE_SYSMON_LOG - Log sys.monitoring activity
|
|
45
|
+
LOG = bool(int(os.getenv("COVERAGE_SYSMON_LOG", 0)))
|
|
46
|
+
|
|
47
|
+
# $set_env.py: COVERAGE_SYSMON_STATS - Collect sys.monitoring stats
|
|
48
|
+
COLLECT_STATS = bool(int(os.getenv("COVERAGE_SYSMON_STATS", 0)))
|
|
45
49
|
|
|
46
50
|
# This module will be imported in all versions of Python, but only used in 3.12+
|
|
47
51
|
# It will be type-checked for 3.12, but not for earlier versions.
|
|
48
52
|
sys_monitoring = getattr(sys, "monitoring", None)
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# MonitorReturn = Literal[sys.monitoring.DISABLE] | None
|
|
54
|
-
MonitorReturn = Any
|
|
54
|
+
DISABLE_TYPE = NewType("DISABLE_TYPE", object)
|
|
55
|
+
MonitorReturn = Optional[DISABLE_TYPE]
|
|
56
|
+
DISABLE = cast(MonitorReturn, getattr(sys_monitoring, "DISABLE", None))
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
if LOG: # pragma: debugging
|
|
@@ -74,7 +76,10 @@ if LOG: # pragma: debugging
|
|
|
74
76
|
assert sys_monitoring is not None
|
|
75
77
|
|
|
76
78
|
short_stack = functools.partial(
|
|
77
|
-
short_stack,
|
|
79
|
+
short_stack,
|
|
80
|
+
full=True,
|
|
81
|
+
short_filenames=True,
|
|
82
|
+
frame_ids=True,
|
|
78
83
|
)
|
|
79
84
|
seen_threads: set[int] = set()
|
|
80
85
|
|
|
@@ -96,8 +101,11 @@ if LOG: # pragma: debugging
|
|
|
96
101
|
# f"{root}-{pid}.out",
|
|
97
102
|
# f"{root}-{pid}-{tslug}.out",
|
|
98
103
|
]:
|
|
99
|
-
with open(filename, "a") as f:
|
|
100
|
-
|
|
104
|
+
with open(filename, "a", encoding="utf-8") as f:
|
|
105
|
+
try:
|
|
106
|
+
print(f"{pid}:{tslug}: {msg}", file=f, flush=True)
|
|
107
|
+
except UnicodeError:
|
|
108
|
+
print(f"{pid}:{tslug}: {ascii(msg)}", file=f, flush=True)
|
|
101
109
|
|
|
102
110
|
def arg_repr(arg: Any) -> str:
|
|
103
111
|
"""Make a customized repr for logged values."""
|
|
@@ -128,7 +136,8 @@ if LOG: # pragma: debugging
|
|
|
128
136
|
return ret
|
|
129
137
|
except Exception as exc:
|
|
130
138
|
log(f"!!{exc.__class__.__name__}: {exc}")
|
|
131
|
-
|
|
139
|
+
if 1:
|
|
140
|
+
log("".join(traceback.format_exception(exc)))
|
|
132
141
|
try:
|
|
133
142
|
assert sys_monitoring is not None
|
|
134
143
|
sys_monitoring.set_events(sys.monitoring.COVERAGE_ID, 0)
|
|
@@ -161,11 +170,22 @@ class CodeInfo:
|
|
|
161
170
|
|
|
162
171
|
tracing: bool
|
|
163
172
|
file_data: TTraceFileData | None
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
byte_to_line: dict[TOffset, TLineNo] | None
|
|
174
|
+
|
|
175
|
+
# Keys are start instruction offsets for branches.
|
|
176
|
+
# Values are dicts:
|
|
177
|
+
# {
|
|
178
|
+
# (from_line, to_line): {offset, offset, ...},
|
|
179
|
+
# (from_line, to_line): {offset, offset, ...},
|
|
180
|
+
# }
|
|
181
|
+
branch_trails: TBranchTrails
|
|
166
182
|
|
|
183
|
+
# Always-jumps are bytecode offsets that do no work but move
|
|
184
|
+
# to another offset.
|
|
185
|
+
always_jumps: dict[TOffset, TOffset]
|
|
167
186
|
|
|
168
|
-
|
|
187
|
+
|
|
188
|
+
def bytes_to_lines(code: CodeType) -> dict[TOffset, TLineNo]:
|
|
169
189
|
"""Make a dict mapping byte code offsets to line numbers."""
|
|
170
190
|
b2l = {}
|
|
171
191
|
for bstart, bend, lineno in code.co_lines():
|
|
@@ -202,17 +222,16 @@ class SysMonitor(Tracer):
|
|
|
202
222
|
# A list of code_objects, just to keep them alive so that id's are
|
|
203
223
|
# useful as identity.
|
|
204
224
|
self.code_objects: list[CodeType] = []
|
|
205
|
-
self.last_lines: dict[FrameType, int] = {}
|
|
206
|
-
# Map id(code_object) -> code_object
|
|
207
|
-
self.local_event_codes: dict[int, CodeType] = {}
|
|
208
225
|
self.sysmon_on = False
|
|
209
226
|
self.lock = threading.Lock()
|
|
210
227
|
|
|
211
|
-
self.stats =
|
|
212
|
-
|
|
213
|
-
|
|
228
|
+
self.stats: dict[str, int] | None = None
|
|
229
|
+
if COLLECT_STATS:
|
|
230
|
+
self.stats = dict.fromkeys(
|
|
231
|
+
"starts start_tracing returns line_lines line_arcs branches branch_trails".split(),
|
|
232
|
+
0,
|
|
233
|
+
)
|
|
214
234
|
|
|
215
|
-
self.stopped = False
|
|
216
235
|
self._activity = False
|
|
217
236
|
|
|
218
237
|
def __repr__(self) -> str:
|
|
@@ -223,44 +242,43 @@ class SysMonitor(Tracer):
|
|
|
223
242
|
@panopticon()
|
|
224
243
|
def start(self) -> None:
|
|
225
244
|
"""Start this Tracer."""
|
|
226
|
-
self.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if self.trace_arcs:
|
|
233
|
-
sys_monitoring.set_events(
|
|
234
|
-
self.myid,
|
|
235
|
-
events.PY_START | events.PY_UNWIND,
|
|
236
|
-
)
|
|
237
|
-
register(events.PY_START, self.sysmon_py_start)
|
|
238
|
-
register(events.PY_RESUME, self.sysmon_py_resume_arcs)
|
|
239
|
-
register(events.PY_RETURN, self.sysmon_py_return_arcs)
|
|
240
|
-
register(events.PY_UNWIND, self.sysmon_py_unwind_arcs)
|
|
241
|
-
register(events.LINE, self.sysmon_line_arcs)
|
|
242
|
-
else:
|
|
245
|
+
with self.lock:
|
|
246
|
+
assert sys_monitoring is not None
|
|
247
|
+
sys_monitoring.use_tool_id(self.myid, "coverage.py")
|
|
248
|
+
register = functools.partial(sys_monitoring.register_callback, self.myid)
|
|
249
|
+
events = sys.monitoring.events
|
|
250
|
+
|
|
243
251
|
sys_monitoring.set_events(self.myid, events.PY_START)
|
|
244
252
|
register(events.PY_START, self.sysmon_py_start)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
253
|
+
if self.trace_arcs:
|
|
254
|
+
register(events.PY_RETURN, self.sysmon_py_return)
|
|
255
|
+
register(events.LINE, self.sysmon_line_arcs)
|
|
256
|
+
if env.PYBEHAVIOR.branch_right_left:
|
|
257
|
+
register(
|
|
258
|
+
events.BRANCH_RIGHT, # type:ignore[attr-defined]
|
|
259
|
+
self.sysmon_branch_either,
|
|
260
|
+
)
|
|
261
|
+
register(
|
|
262
|
+
events.BRANCH_LEFT, # type:ignore[attr-defined]
|
|
263
|
+
self.sysmon_branch_either,
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
register(events.LINE, self.sysmon_line_lines)
|
|
267
|
+
sys_monitoring.restart_events()
|
|
268
|
+
self.sysmon_on = True
|
|
248
269
|
|
|
249
270
|
@panopticon()
|
|
250
271
|
def stop(self) -> None:
|
|
251
272
|
"""Stop this Tracer."""
|
|
252
|
-
if not self.sysmon_on:
|
|
253
|
-
# In forking situations, we might try to stop when we are not
|
|
254
|
-
# started. Do nothing in that case.
|
|
255
|
-
return
|
|
256
|
-
assert sys_monitoring is not None
|
|
257
|
-
sys_monitoring.set_events(self.myid, 0)
|
|
258
273
|
with self.lock:
|
|
274
|
+
if not self.sysmon_on:
|
|
275
|
+
# In forking situations, we might try to stop when we are not
|
|
276
|
+
# started. Do nothing in that case.
|
|
277
|
+
return
|
|
278
|
+
assert sys_monitoring is not None
|
|
279
|
+
sys_monitoring.set_events(self.myid, 0)
|
|
259
280
|
self.sysmon_on = False
|
|
260
|
-
|
|
261
|
-
sys_monitoring.set_local_events(self.myid, code, 0)
|
|
262
|
-
self.local_event_codes = {}
|
|
263
|
-
sys_monitoring.free_tool_id(self.myid)
|
|
281
|
+
sys_monitoring.free_tool_id(self.myid)
|
|
264
282
|
|
|
265
283
|
@panopticon()
|
|
266
284
|
def post_fork(self) -> None:
|
|
@@ -277,29 +295,15 @@ class SysMonitor(Tracer):
|
|
|
277
295
|
|
|
278
296
|
def get_stats(self) -> dict[str, int] | None:
|
|
279
297
|
"""Return a dictionary of statistics, or None."""
|
|
280
|
-
return
|
|
281
|
-
|
|
282
|
-
# The number of frames in callers_frame takes @panopticon into account.
|
|
283
|
-
if LOG:
|
|
284
|
-
|
|
285
|
-
def callers_frame(self) -> FrameType:
|
|
286
|
-
"""Get the frame of the Python code we're monitoring."""
|
|
287
|
-
return (
|
|
288
|
-
inspect.currentframe().f_back.f_back.f_back # type: ignore[union-attr,return-value]
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
else:
|
|
292
|
-
|
|
293
|
-
def callers_frame(self) -> FrameType:
|
|
294
|
-
"""Get the frame of the Python code we're monitoring."""
|
|
295
|
-
return inspect.currentframe().f_back.f_back # type: ignore[union-attr,return-value]
|
|
298
|
+
return self.stats
|
|
296
299
|
|
|
297
300
|
@panopticon("code", "@")
|
|
298
|
-
def sysmon_py_start(self, code: CodeType, instruction_offset:
|
|
301
|
+
def sysmon_py_start(self, code: CodeType, instruction_offset: TOffset) -> MonitorReturn:
|
|
299
302
|
"""Handle sys.monitoring.events.PY_START events."""
|
|
300
303
|
# Entering a new frame. Decide if we should trace in this file.
|
|
301
304
|
self._activity = True
|
|
302
|
-
self.stats
|
|
305
|
+
if self.stats is not None:
|
|
306
|
+
self.stats["starts"] += 1
|
|
303
307
|
|
|
304
308
|
code_info = self.code_infos.get(id(code))
|
|
305
309
|
tracing_code: bool | None = None
|
|
@@ -312,10 +316,12 @@ class SysMonitor(Tracer):
|
|
|
312
316
|
filename = code.co_filename
|
|
313
317
|
disp = self.should_trace_cache.get(filename)
|
|
314
318
|
if disp is None:
|
|
315
|
-
frame = inspect.currentframe()
|
|
316
|
-
if
|
|
317
|
-
|
|
318
|
-
|
|
319
|
+
frame = inspect.currentframe()
|
|
320
|
+
if frame is not None:
|
|
321
|
+
frame = inspect.currentframe().f_back # type: ignore[union-attr]
|
|
322
|
+
if LOG:
|
|
323
|
+
# @panopticon adds a frame.
|
|
324
|
+
frame = frame.f_back # type: ignore[union-attr]
|
|
319
325
|
disp = self.should_trace(filename, frame) # type: ignore[arg-type]
|
|
320
326
|
self.should_trace_cache[filename] = disp
|
|
321
327
|
|
|
@@ -335,102 +341,134 @@ class SysMonitor(Tracer):
|
|
|
335
341
|
file_data = None
|
|
336
342
|
b2l = None
|
|
337
343
|
|
|
338
|
-
|
|
344
|
+
code_info = CodeInfo(
|
|
339
345
|
tracing=tracing_code,
|
|
340
346
|
file_data=file_data,
|
|
341
347
|
byte_to_line=b2l,
|
|
348
|
+
branch_trails={},
|
|
349
|
+
always_jumps={},
|
|
342
350
|
)
|
|
351
|
+
self.code_infos[id(code)] = code_info
|
|
343
352
|
self.code_objects.append(code)
|
|
344
353
|
|
|
345
354
|
if tracing_code:
|
|
355
|
+
if self.stats is not None:
|
|
356
|
+
self.stats["start_tracing"] += 1
|
|
346
357
|
events = sys.monitoring.events
|
|
347
358
|
with self.lock:
|
|
348
359
|
if self.sysmon_on:
|
|
349
360
|
assert sys_monitoring is not None
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
# | events.BRANCH
|
|
359
|
-
# | events.JUMP
|
|
360
|
-
)
|
|
361
|
-
self.local_event_codes[id(code)] = code
|
|
362
|
-
|
|
363
|
-
if tracing_code and self.trace_arcs:
|
|
364
|
-
frame = self.callers_frame()
|
|
365
|
-
self.last_lines[frame] = -code.co_firstlineno
|
|
366
|
-
return None
|
|
367
|
-
else:
|
|
368
|
-
return sys.monitoring.DISABLE
|
|
361
|
+
local_events = events.PY_RETURN | events.PY_RESUME | events.LINE
|
|
362
|
+
if self.trace_arcs:
|
|
363
|
+
assert env.PYBEHAVIOR.branch_right_left
|
|
364
|
+
local_events |= (
|
|
365
|
+
events.BRANCH_RIGHT # type:ignore[attr-defined]
|
|
366
|
+
| events.BRANCH_LEFT # type:ignore[attr-defined]
|
|
367
|
+
)
|
|
368
|
+
sys_monitoring.set_local_events(self.myid, code, local_events)
|
|
369
369
|
|
|
370
|
-
|
|
371
|
-
def sysmon_py_resume_arcs(
|
|
372
|
-
self, code: CodeType, instruction_offset: int,
|
|
373
|
-
) -> MonitorReturn:
|
|
374
|
-
"""Handle sys.monitoring.events.PY_RESUME events for branch coverage."""
|
|
375
|
-
frame = self.callers_frame()
|
|
376
|
-
self.last_lines[frame] = frame.f_lineno
|
|
370
|
+
return DISABLE
|
|
377
371
|
|
|
378
372
|
@panopticon("code", "@", None)
|
|
379
|
-
def
|
|
380
|
-
self,
|
|
373
|
+
def sysmon_py_return(
|
|
374
|
+
self,
|
|
375
|
+
code: CodeType,
|
|
376
|
+
instruction_offset: TOffset,
|
|
377
|
+
retval: object,
|
|
381
378
|
) -> MonitorReturn:
|
|
382
379
|
"""Handle sys.monitoring.events.PY_RETURN events for branch coverage."""
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if code_info is not None and code_info.file_data is not None:
|
|
386
|
-
last_line = self.last_lines.get(frame)
|
|
387
|
-
if last_line is not None:
|
|
388
|
-
arc = (last_line, -code.co_firstlineno)
|
|
389
|
-
# log(f"adding {arc=}")
|
|
390
|
-
cast(set[TArc], code_info.file_data).add(arc)
|
|
391
|
-
|
|
392
|
-
# Leaving this function, no need for the frame any more.
|
|
393
|
-
self.last_lines.pop(frame, None)
|
|
394
|
-
|
|
395
|
-
@panopticon("code", "@", "exc")
|
|
396
|
-
def sysmon_py_unwind_arcs(
|
|
397
|
-
self, code: CodeType, instruction_offset: int, exception: BaseException,
|
|
398
|
-
) -> MonitorReturn:
|
|
399
|
-
"""Handle sys.monitoring.events.PY_UNWIND events for branch coverage."""
|
|
400
|
-
frame = self.callers_frame()
|
|
401
|
-
# Leaving this function.
|
|
402
|
-
last_line = self.last_lines.pop(frame, None)
|
|
403
|
-
if isinstance(exception, GeneratorExit):
|
|
404
|
-
# We don't want to count generator exits as arcs.
|
|
405
|
-
return
|
|
380
|
+
if self.stats is not None:
|
|
381
|
+
self.stats["returns"] += 1
|
|
406
382
|
code_info = self.code_infos.get(id(code))
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
383
|
+
# code_info is not None and code_info.file_data is not None, since we
|
|
384
|
+
# wouldn't have enabled this event if they were.
|
|
385
|
+
last_line = code_info.byte_to_line[instruction_offset] # type: ignore
|
|
386
|
+
if last_line is not None:
|
|
387
|
+
arc = (last_line, -code.co_firstlineno)
|
|
388
|
+
code_info.file_data.add(arc) # type: ignore
|
|
389
|
+
# log(f"adding {arc=}")
|
|
390
|
+
return DISABLE
|
|
413
391
|
|
|
414
392
|
@panopticon("code", "line")
|
|
415
|
-
def sysmon_line_lines(self, code: CodeType, line_number:
|
|
393
|
+
def sysmon_line_lines(self, code: CodeType, line_number: TLineNo) -> MonitorReturn:
|
|
416
394
|
"""Handle sys.monitoring.events.LINE events for line coverage."""
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
395
|
+
if self.stats is not None:
|
|
396
|
+
self.stats["line_lines"] += 1
|
|
397
|
+
code_info = self.code_infos.get(id(code))
|
|
398
|
+
# It should be true that code_info is not None and code_info.file_data
|
|
399
|
+
# is not None, since we wouldn't have enabled this event if they were.
|
|
400
|
+
# But somehow code_info can be None here, so we have to check.
|
|
401
|
+
if code_info is not None and code_info.file_data is not None:
|
|
402
|
+
code_info.file_data.add(line_number) # type: ignore
|
|
403
|
+
# log(f"adding {line_number=}")
|
|
404
|
+
return DISABLE
|
|
422
405
|
|
|
423
406
|
@panopticon("code", "line")
|
|
424
|
-
def sysmon_line_arcs(self, code: CodeType, line_number:
|
|
407
|
+
def sysmon_line_arcs(self, code: CodeType, line_number: TLineNo) -> MonitorReturn:
|
|
425
408
|
"""Handle sys.monitoring.events.LINE events for branch coverage."""
|
|
409
|
+
if self.stats is not None:
|
|
410
|
+
self.stats["line_arcs"] += 1
|
|
426
411
|
code_info = self.code_infos[id(code)]
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
412
|
+
# code_info is not None and code_info.file_data is not None, since we
|
|
413
|
+
# wouldn't have enabled this event if they were.
|
|
414
|
+
arc = (line_number, line_number)
|
|
415
|
+
code_info.file_data.add(arc) # type: ignore
|
|
416
|
+
# log(f"adding {arc=}")
|
|
417
|
+
return DISABLE
|
|
418
|
+
|
|
419
|
+
@panopticon("code", "@", "@")
|
|
420
|
+
def sysmon_branch_either(
|
|
421
|
+
self, code: CodeType, instruction_offset: TOffset, destination_offset: TOffset
|
|
422
|
+
) -> MonitorReturn:
|
|
423
|
+
"""Handle BRANCH_RIGHT and BRANCH_LEFT events."""
|
|
424
|
+
if self.stats is not None:
|
|
425
|
+
self.stats["branches"] += 1
|
|
426
|
+
code_info = self.code_infos[id(code)]
|
|
427
|
+
# code_info is not None and code_info.file_data is not None, since we
|
|
428
|
+
# wouldn't have enabled this event if they were.
|
|
429
|
+
if not code_info.branch_trails:
|
|
430
|
+
if self.stats is not None:
|
|
431
|
+
self.stats["branch_trails"] += 1
|
|
432
|
+
multiline_map = get_multiline_map(code.co_filename)
|
|
433
|
+
code_info.branch_trails = branch_trails(code, multiline_map=multiline_map)
|
|
434
|
+
code_info.always_jumps = always_jumps(code)
|
|
435
|
+
# log(f"branch_trails for {code}:\n{ppformat(code_info.branch_trails)}")
|
|
436
|
+
added_arc = False
|
|
437
|
+
dest_info = code_info.branch_trails.get(instruction_offset)
|
|
438
|
+
|
|
439
|
+
# Re-map the destination offset through always-jumps to deal with NOP etc.
|
|
440
|
+
dests = {destination_offset}
|
|
441
|
+
while (dest := code_info.always_jumps.get(destination_offset)) is not None:
|
|
442
|
+
destination_offset = dest
|
|
443
|
+
dests.add(destination_offset)
|
|
444
|
+
|
|
445
|
+
# log(f"dest_info = {ppformat(dest_info)}")
|
|
446
|
+
if dest_info is not None:
|
|
447
|
+
for arc, offsets in dest_info.items():
|
|
448
|
+
if arc is None:
|
|
449
|
+
continue
|
|
450
|
+
if dests & offsets:
|
|
451
|
+
code_info.file_data.add(arc) # type: ignore
|
|
452
|
+
# log(f"adding {arc=}")
|
|
453
|
+
added_arc = True
|
|
454
|
+
break
|
|
455
|
+
|
|
456
|
+
if not added_arc:
|
|
457
|
+
# This could be an exception jumping from line to line.
|
|
458
|
+
assert code_info.byte_to_line is not None
|
|
459
|
+
l1 = code_info.byte_to_line[instruction_offset]
|
|
460
|
+
l2 = code_info.byte_to_line.get(destination_offset)
|
|
461
|
+
if l2 is not None and l1 != l2:
|
|
462
|
+
arc = (l1, l2)
|
|
463
|
+
code_info.file_data.add(arc) # type: ignore
|
|
464
|
+
# log(f"adding unforeseen {arc=}")
|
|
465
|
+
|
|
466
|
+
return DISABLE
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@functools.lru_cache(maxsize=5)
|
|
470
|
+
def get_multiline_map(filename: str) -> dict[TLineNo, TLineNo]:
|
|
471
|
+
"""Get a PythonParser for the given filename, cached."""
|
|
472
|
+
parser = PythonParser(filename=filename)
|
|
473
|
+
parser.parse_source()
|
|
474
|
+
return parser.multiline_map
|
coverage/templite.py
CHANGED
|
@@ -13,19 +13,18 @@ http://aosabook.org/en/500L/a-template-engine.html
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import re
|
|
16
|
-
|
|
17
|
-
from typing import (
|
|
18
|
-
Any, Callable, NoReturn, cast,
|
|
19
|
-
)
|
|
16
|
+
from typing import Any, Callable, NoReturn, cast
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
class TempliteSyntaxError(ValueError):
|
|
23
20
|
"""Raised when a template has a syntax error."""
|
|
21
|
+
|
|
24
22
|
pass
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
class TempliteValueError(ValueError):
|
|
28
26
|
"""Raised when an expression won't evaluate in a template."""
|
|
27
|
+
|
|
29
28
|
pass
|
|
30
29
|
|
|
31
30
|
|
|
@@ -53,7 +52,7 @@ class CodeBuilder:
|
|
|
53
52
|
self.code.append(section)
|
|
54
53
|
return section
|
|
55
54
|
|
|
56
|
-
INDENT_STEP = 4
|
|
55
|
+
INDENT_STEP = 4 # PEP8 says so!
|
|
57
56
|
|
|
58
57
|
def indent(self) -> None:
|
|
59
58
|
"""Increase the current indent for following lines."""
|
|
@@ -117,6 +116,7 @@ class Templite:
|
|
|
117
116
|
})
|
|
118
117
|
|
|
119
118
|
"""
|
|
119
|
+
|
|
120
120
|
def __init__(self, text: str, *contexts: dict[str, Any]) -> None:
|
|
121
121
|
"""Construct a Templite with the given `text`.
|
|
122
122
|
|
|
@@ -163,7 +163,7 @@ class Templite:
|
|
|
163
163
|
for token in tokens:
|
|
164
164
|
if token.startswith("{"):
|
|
165
165
|
start, end = 2, -2
|
|
166
|
-
squash = (token[-3] == "-")
|
|
166
|
+
squash = (token[-3] == "-") # fmt: skip
|
|
167
167
|
if squash:
|
|
168
168
|
end = -3
|
|
169
169
|
|