coverage 7.6.10__cp312-cp312-musllinux_1_2_aarch64.whl → 7.12.0__cp312-cp312-musllinux_1_2_aarch64.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 +3 -1
- coverage/__main__.py +3 -1
- coverage/annotate.py +2 -3
- coverage/bytecode.py +178 -4
- coverage/cmdline.py +330 -155
- coverage/collector.py +32 -43
- coverage/config.py +167 -63
- coverage/context.py +5 -6
- coverage/control.py +165 -86
- coverage/core.py +71 -34
- coverage/data.py +4 -5
- coverage/debug.py +113 -57
- coverage/disposition.py +2 -1
- coverage/env.py +29 -78
- coverage/exceptions.py +29 -7
- coverage/execfile.py +19 -14
- coverage/files.py +24 -19
- coverage/html.py +118 -75
- coverage/htmlfiles/coverage_html.js +12 -10
- coverage/htmlfiles/index.html +45 -10
- coverage/htmlfiles/pyfile.html +2 -2
- coverage/htmlfiles/style.css +54 -6
- coverage/htmlfiles/style.scss +85 -3
- coverage/inorout.py +62 -45
- coverage/jsonreport.py +22 -9
- coverage/lcovreport.py +16 -18
- coverage/misc.py +51 -47
- coverage/multiproc.py +12 -7
- coverage/numbits.py +4 -5
- coverage/parser.py +150 -251
- coverage/patch.py +166 -0
- coverage/phystokens.py +25 -26
- coverage/plugin.py +14 -14
- coverage/plugin_support.py +37 -36
- coverage/python.py +13 -14
- coverage/pytracer.py +31 -33
- coverage/regions.py +3 -2
- coverage/report.py +60 -44
- coverage/report_core.py +7 -10
- coverage/results.py +152 -68
- coverage/sqldata.py +261 -211
- coverage/sqlitedb.py +37 -29
- coverage/sysmon.py +237 -162
- coverage/templite.py +19 -7
- coverage/tomlconfig.py +13 -13
- coverage/tracer.cpython-312-aarch64-linux-musl.so +0 -0
- coverage/tracer.pyi +3 -1
- coverage/types.py +26 -23
- coverage/version.py +4 -19
- coverage/xmlreport.py +17 -14
- {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/METADATA +50 -28
- coverage-7.12.0.dist-info/RECORD +59 -0
- {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/WHEEL +1 -1
- coverage-7.6.10.dist-info/RECORD +0 -58
- {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/entry_points.txt +0 -0
- {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info/licenses}/LICENSE.txt +0 -0
- {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/top_level.txt +0 -0
coverage/sysmon.py
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
|
2
|
-
# For details: https://github.com/
|
|
2
|
+
# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
|
|
3
3
|
|
|
4
4
|
"""Callback functions and support for sys.monitoring data collection."""
|
|
5
5
|
|
|
6
|
-
# TODO: https://github.com/python/cpython/issues/111963#issuecomment-2386584080
|
|
7
|
-
|
|
8
6
|
from __future__ import annotations
|
|
9
7
|
|
|
8
|
+
import collections
|
|
10
9
|
import functools
|
|
11
10
|
import inspect
|
|
12
11
|
import os
|
|
@@ -14,46 +13,49 @@ import os.path
|
|
|
14
13
|
import sys
|
|
15
14
|
import threading
|
|
16
15
|
import traceback
|
|
17
|
-
|
|
18
16
|
from dataclasses import dataclass
|
|
19
|
-
from types import CodeType
|
|
20
|
-
from typing import
|
|
21
|
-
Any,
|
|
22
|
-
Callable,
|
|
23
|
-
TYPE_CHECKING,
|
|
24
|
-
cast,
|
|
25
|
-
)
|
|
17
|
+
from types import CodeType
|
|
18
|
+
from typing import Any, Callable, NewType, Optional, cast
|
|
26
19
|
|
|
20
|
+
from coverage import env
|
|
21
|
+
from coverage.bytecode import TBranchTrails, always_jumps, branch_trails
|
|
27
22
|
from coverage.debug import short_filename, short_stack
|
|
23
|
+
from coverage.exceptions import NoSource, NotPython
|
|
28
24
|
from coverage.misc import isolate_module
|
|
25
|
+
from coverage.parser import PythonParser
|
|
29
26
|
from coverage.types import (
|
|
30
27
|
AnyCallable,
|
|
31
|
-
TArc,
|
|
32
28
|
TFileDisposition,
|
|
33
29
|
TLineNo,
|
|
30
|
+
TOffset,
|
|
31
|
+
Tracer,
|
|
34
32
|
TShouldStartContextFn,
|
|
35
33
|
TShouldTraceFn,
|
|
36
34
|
TTraceData,
|
|
37
35
|
TTraceFileData,
|
|
38
|
-
Tracer,
|
|
39
36
|
TWarnFn,
|
|
40
37
|
)
|
|
41
38
|
|
|
39
|
+
# Only needed for some of the commented-out logging:
|
|
40
|
+
# from coverage.debug import ppformat
|
|
41
|
+
|
|
42
42
|
os = isolate_module(os)
|
|
43
43
|
|
|
44
44
|
# pylint: disable=unused-argument
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
# $set_env.py: COVERAGE_SYSMON_LOG - Log sys.monitoring activity
|
|
47
|
+
LOG = bool(int(os.getenv("COVERAGE_SYSMON_LOG", 0)))
|
|
48
|
+
|
|
49
|
+
# $set_env.py: COVERAGE_SYSMON_STATS - Collect sys.monitoring stats
|
|
50
|
+
COLLECT_STATS = bool(int(os.getenv("COVERAGE_SYSMON_STATS", 0)))
|
|
47
51
|
|
|
48
52
|
# This module will be imported in all versions of Python, but only used in 3.12+
|
|
49
53
|
# It will be type-checked for 3.12, but not for earlier versions.
|
|
50
54
|
sys_monitoring = getattr(sys, "monitoring", None)
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# MonitorReturn = Literal[sys.monitoring.DISABLE] | None
|
|
56
|
-
MonitorReturn = Any
|
|
56
|
+
DISABLE_TYPE = NewType("DISABLE_TYPE", object)
|
|
57
|
+
MonitorReturn = Optional[DISABLE_TYPE]
|
|
58
|
+
DISABLE = cast(MonitorReturn, getattr(sys_monitoring, "DISABLE", None))
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
if LOG: # pragma: debugging
|
|
@@ -76,7 +78,10 @@ if LOG: # pragma: debugging
|
|
|
76
78
|
assert sys_monitoring is not None
|
|
77
79
|
|
|
78
80
|
short_stack = functools.partial(
|
|
79
|
-
short_stack,
|
|
81
|
+
short_stack,
|
|
82
|
+
full=True,
|
|
83
|
+
short_filenames=True,
|
|
84
|
+
frame_ids=True,
|
|
80
85
|
)
|
|
81
86
|
seen_threads: set[int] = set()
|
|
82
87
|
|
|
@@ -98,8 +103,11 @@ if LOG: # pragma: debugging
|
|
|
98
103
|
# f"{root}-{pid}.out",
|
|
99
104
|
# f"{root}-{pid}-{tslug}.out",
|
|
100
105
|
]:
|
|
101
|
-
with open(filename, "a") as f:
|
|
102
|
-
|
|
106
|
+
with open(filename, "a", encoding="utf-8") as f:
|
|
107
|
+
try:
|
|
108
|
+
print(f"{pid}:{tslug}: {msg}", file=f, flush=True)
|
|
109
|
+
except UnicodeError:
|
|
110
|
+
print(f"{pid}:{tslug}: {ascii(msg)}", file=f, flush=True)
|
|
103
111
|
|
|
104
112
|
def arg_repr(arg: Any) -> str:
|
|
105
113
|
"""Make a customized repr for logged values."""
|
|
@@ -130,7 +138,8 @@ if LOG: # pragma: debugging
|
|
|
130
138
|
return ret
|
|
131
139
|
except Exception as exc:
|
|
132
140
|
log(f"!!{exc.__class__.__name__}: {exc}")
|
|
133
|
-
|
|
141
|
+
if 1:
|
|
142
|
+
log("".join(traceback.format_exception(exc)))
|
|
134
143
|
try:
|
|
135
144
|
assert sys_monitoring is not None
|
|
136
145
|
sys_monitoring.set_events(sys.monitoring.COVERAGE_ID, 0)
|
|
@@ -163,11 +172,22 @@ class CodeInfo:
|
|
|
163
172
|
|
|
164
173
|
tracing: bool
|
|
165
174
|
file_data: TTraceFileData | None
|
|
166
|
-
|
|
167
|
-
byte_to_line: dict[int, int] | None
|
|
175
|
+
byte_to_line: dict[TOffset, TLineNo] | None
|
|
168
176
|
|
|
177
|
+
# Keys are start instruction offsets for branches.
|
|
178
|
+
# Values are dicts:
|
|
179
|
+
# {
|
|
180
|
+
# (from_line, to_line): {offset, offset, ...},
|
|
181
|
+
# (from_line, to_line): {offset, offset, ...},
|
|
182
|
+
# }
|
|
183
|
+
branch_trails: TBranchTrails
|
|
169
184
|
|
|
170
|
-
|
|
185
|
+
# Always-jumps are bytecode offsets that do no work but move
|
|
186
|
+
# to another offset.
|
|
187
|
+
always_jumps: dict[TOffset, TOffset]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def bytes_to_lines(code: CodeType) -> dict[TOffset, TLineNo]:
|
|
171
191
|
"""Make a dict mapping byte code offsets to line numbers."""
|
|
172
192
|
b2l = {}
|
|
173
193
|
for bstart, bend, lineno in code.co_lines():
|
|
@@ -204,17 +224,20 @@ class SysMonitor(Tracer):
|
|
|
204
224
|
# A list of code_objects, just to keep them alive so that id's are
|
|
205
225
|
# useful as identity.
|
|
206
226
|
self.code_objects: list[CodeType] = []
|
|
207
|
-
|
|
208
|
-
# Map id(code_object)
|
|
209
|
-
self.
|
|
227
|
+
|
|
228
|
+
# Map filename:__name__ -> set(id(code_object))
|
|
229
|
+
self.filename_code_ids: dict[str, set[int]] = collections.defaultdict(set)
|
|
230
|
+
|
|
210
231
|
self.sysmon_on = False
|
|
211
232
|
self.lock = threading.Lock()
|
|
212
233
|
|
|
213
|
-
self.stats =
|
|
214
|
-
|
|
215
|
-
|
|
234
|
+
self.stats: dict[str, int] | None = None
|
|
235
|
+
if COLLECT_STATS:
|
|
236
|
+
self.stats = dict.fromkeys(
|
|
237
|
+
"starts start_tracing returns line_lines line_arcs branches branch_trails".split(),
|
|
238
|
+
0,
|
|
239
|
+
)
|
|
216
240
|
|
|
217
|
-
self.stopped = False
|
|
218
241
|
self._activity = False
|
|
219
242
|
|
|
220
243
|
def __repr__(self) -> str:
|
|
@@ -225,44 +248,59 @@ class SysMonitor(Tracer):
|
|
|
225
248
|
@panopticon()
|
|
226
249
|
def start(self) -> None:
|
|
227
250
|
"""Start this Tracer."""
|
|
228
|
-
self.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if self.trace_arcs:
|
|
235
|
-
sys_monitoring.set_events(
|
|
236
|
-
self.myid,
|
|
237
|
-
events.PY_START | events.PY_UNWIND,
|
|
238
|
-
)
|
|
239
|
-
register(events.PY_START, self.sysmon_py_start)
|
|
240
|
-
register(events.PY_RESUME, self.sysmon_py_resume_arcs)
|
|
241
|
-
register(events.PY_RETURN, self.sysmon_py_return_arcs)
|
|
242
|
-
register(events.PY_UNWIND, self.sysmon_py_unwind_arcs)
|
|
243
|
-
register(events.LINE, self.sysmon_line_arcs)
|
|
244
|
-
else:
|
|
251
|
+
with self.lock:
|
|
252
|
+
assert sys_monitoring is not None
|
|
253
|
+
sys_monitoring.use_tool_id(self.myid, "coverage.py")
|
|
254
|
+
register = functools.partial(sys_monitoring.register_callback, self.myid)
|
|
255
|
+
events = sys.monitoring.events
|
|
256
|
+
|
|
245
257
|
sys_monitoring.set_events(self.myid, events.PY_START)
|
|
246
258
|
register(events.PY_START, self.sysmon_py_start)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
259
|
+
if self.trace_arcs:
|
|
260
|
+
register(events.PY_RETURN, self.sysmon_py_return)
|
|
261
|
+
register(events.LINE, self.sysmon_line_arcs)
|
|
262
|
+
if env.PYBEHAVIOR.branch_right_left:
|
|
263
|
+
register(
|
|
264
|
+
events.BRANCH_RIGHT, # type:ignore[attr-defined]
|
|
265
|
+
self.sysmon_branch_either,
|
|
266
|
+
)
|
|
267
|
+
register(
|
|
268
|
+
events.BRANCH_LEFT, # type:ignore[attr-defined]
|
|
269
|
+
self.sysmon_branch_either,
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
register(events.LINE, self.sysmon_line_lines)
|
|
273
|
+
sys_monitoring.restart_events()
|
|
274
|
+
self.sysmon_on = True
|
|
250
275
|
|
|
251
276
|
@panopticon()
|
|
252
277
|
def stop(self) -> None:
|
|
253
278
|
"""Stop this Tracer."""
|
|
254
|
-
if not self.sysmon_on:
|
|
255
|
-
# In forking situations, we might try to stop when we are not
|
|
256
|
-
# started. Do nothing in that case.
|
|
257
|
-
return
|
|
258
|
-
assert sys_monitoring is not None
|
|
259
|
-
sys_monitoring.set_events(self.myid, 0)
|
|
260
279
|
with self.lock:
|
|
280
|
+
if not self.sysmon_on:
|
|
281
|
+
# In forking situations, we might try to stop when we are not
|
|
282
|
+
# started. Do nothing in that case.
|
|
283
|
+
return
|
|
284
|
+
assert sys_monitoring is not None
|
|
285
|
+
sys_monitoring.set_events(self.myid, 0)
|
|
261
286
|
self.sysmon_on = False
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
287
|
+
sys_monitoring.free_tool_id(self.myid)
|
|
288
|
+
|
|
289
|
+
if LOG: # pragma: debugging
|
|
290
|
+
items = sorted(
|
|
291
|
+
self.filename_code_ids.items(),
|
|
292
|
+
key=lambda item: len(item[1]),
|
|
293
|
+
reverse=True,
|
|
294
|
+
)
|
|
295
|
+
code_objs = sum(len(code_ids) for _, code_ids in items)
|
|
296
|
+
dupes = code_objs - len(items)
|
|
297
|
+
if dupes:
|
|
298
|
+
log(f"==== Duplicate code objects: {dupes} duplicates, {code_objs} total")
|
|
299
|
+
for filename, code_ids in items:
|
|
300
|
+
if len(code_ids) > 1:
|
|
301
|
+
log(f"{len(code_ids):>5} objects: {filename}")
|
|
302
|
+
else:
|
|
303
|
+
log("==== Duplicate code objects: none")
|
|
266
304
|
|
|
267
305
|
@panopticon()
|
|
268
306
|
def post_fork(self) -> None:
|
|
@@ -279,30 +317,16 @@ class SysMonitor(Tracer):
|
|
|
279
317
|
|
|
280
318
|
def get_stats(self) -> dict[str, int] | None:
|
|
281
319
|
"""Return a dictionary of statistics, or None."""
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
# The number of frames in callers_frame takes @panopticon into account.
|
|
285
|
-
if LOG:
|
|
286
|
-
|
|
287
|
-
def callers_frame(self) -> FrameType:
|
|
288
|
-
"""Get the frame of the Python code we're monitoring."""
|
|
289
|
-
return (
|
|
290
|
-
inspect.currentframe().f_back.f_back.f_back # type: ignore[union-attr,return-value]
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
else:
|
|
294
|
-
|
|
295
|
-
def callers_frame(self) -> FrameType:
|
|
296
|
-
"""Get the frame of the Python code we're monitoring."""
|
|
297
|
-
return inspect.currentframe().f_back.f_back # type: ignore[union-attr,return-value]
|
|
320
|
+
return self.stats
|
|
298
321
|
|
|
299
322
|
@panopticon("code", "@")
|
|
300
|
-
def sysmon_py_start(self, code: CodeType, instruction_offset:
|
|
323
|
+
def sysmon_py_start(self, code: CodeType, instruction_offset: TOffset) -> MonitorReturn:
|
|
301
324
|
"""Handle sys.monitoring.events.PY_START events."""
|
|
302
|
-
# Entering a new frame. Decide if we should trace in this file.
|
|
303
325
|
self._activity = True
|
|
304
|
-
self.stats
|
|
326
|
+
if self.stats is not None:
|
|
327
|
+
self.stats["starts"] += 1
|
|
305
328
|
|
|
329
|
+
# Entering a new frame. Decide if we should trace in this file.
|
|
306
330
|
code_info = self.code_infos.get(id(code))
|
|
307
331
|
tracing_code: bool | None = None
|
|
308
332
|
file_data: TTraceFileData | None = None
|
|
@@ -314,10 +338,12 @@ class SysMonitor(Tracer):
|
|
|
314
338
|
filename = code.co_filename
|
|
315
339
|
disp = self.should_trace_cache.get(filename)
|
|
316
340
|
if disp is None:
|
|
317
|
-
frame = inspect.currentframe()
|
|
318
|
-
if
|
|
319
|
-
|
|
320
|
-
|
|
341
|
+
frame = inspect.currentframe()
|
|
342
|
+
if frame is not None:
|
|
343
|
+
frame = inspect.currentframe().f_back # type: ignore[union-attr]
|
|
344
|
+
if LOG: # pragma: debugging
|
|
345
|
+
# @panopticon adds a frame.
|
|
346
|
+
frame = frame.f_back # type: ignore[union-attr]
|
|
321
347
|
disp = self.should_trace(filename, frame) # type: ignore[arg-type]
|
|
322
348
|
self.should_trace_cache[filename] = disp
|
|
323
349
|
|
|
@@ -337,102 +363,151 @@ class SysMonitor(Tracer):
|
|
|
337
363
|
file_data = None
|
|
338
364
|
b2l = None
|
|
339
365
|
|
|
340
|
-
|
|
366
|
+
code_info = CodeInfo(
|
|
341
367
|
tracing=tracing_code,
|
|
342
368
|
file_data=file_data,
|
|
343
369
|
byte_to_line=b2l,
|
|
370
|
+
branch_trails={},
|
|
371
|
+
always_jumps={},
|
|
344
372
|
)
|
|
373
|
+
self.code_infos[id(code)] = code_info
|
|
345
374
|
self.code_objects.append(code)
|
|
346
375
|
|
|
347
376
|
if tracing_code:
|
|
377
|
+
if self.stats is not None:
|
|
378
|
+
self.stats["start_tracing"] += 1
|
|
348
379
|
events = sys.monitoring.events
|
|
349
380
|
with self.lock:
|
|
350
381
|
if self.sysmon_on:
|
|
351
382
|
assert sys_monitoring is not None
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return None
|
|
369
|
-
else:
|
|
370
|
-
return sys.monitoring.DISABLE
|
|
371
|
-
|
|
372
|
-
@panopticon("code", "@")
|
|
373
|
-
def sysmon_py_resume_arcs(
|
|
374
|
-
self, code: CodeType, instruction_offset: int,
|
|
375
|
-
) -> MonitorReturn:
|
|
376
|
-
"""Handle sys.monitoring.events.PY_RESUME events for branch coverage."""
|
|
377
|
-
frame = self.callers_frame()
|
|
378
|
-
self.last_lines[frame] = frame.f_lineno
|
|
383
|
+
local_events = events.PY_RETURN | events.PY_RESUME | events.LINE
|
|
384
|
+
if self.trace_arcs:
|
|
385
|
+
assert env.PYBEHAVIOR.branch_right_left
|
|
386
|
+
local_events |= (
|
|
387
|
+
events.BRANCH_RIGHT # type:ignore[attr-defined]
|
|
388
|
+
| events.BRANCH_LEFT # type:ignore[attr-defined]
|
|
389
|
+
)
|
|
390
|
+
sys_monitoring.set_local_events(self.myid, code, local_events)
|
|
391
|
+
|
|
392
|
+
if LOG: # pragma: debugging
|
|
393
|
+
if code.co_filename not in {"<string>"}:
|
|
394
|
+
self.filename_code_ids[f"{code.co_filename}:{code.co_name}"].add(
|
|
395
|
+
id(code)
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return DISABLE
|
|
379
399
|
|
|
380
400
|
@panopticon("code", "@", None)
|
|
381
|
-
def
|
|
382
|
-
self,
|
|
401
|
+
def sysmon_py_return(
|
|
402
|
+
self,
|
|
403
|
+
code: CodeType,
|
|
404
|
+
instruction_offset: TOffset,
|
|
405
|
+
retval: object,
|
|
383
406
|
) -> MonitorReturn:
|
|
384
407
|
"""Handle sys.monitoring.events.PY_RETURN events for branch coverage."""
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if code_info is not None and code_info.file_data is not None:
|
|
388
|
-
last_line = self.last_lines.get(frame)
|
|
389
|
-
if last_line is not None:
|
|
390
|
-
arc = (last_line, -code.co_firstlineno)
|
|
391
|
-
# log(f"adding {arc=}")
|
|
392
|
-
cast(set[TArc], code_info.file_data).add(arc)
|
|
393
|
-
|
|
394
|
-
# Leaving this function, no need for the frame any more.
|
|
395
|
-
self.last_lines.pop(frame, None)
|
|
396
|
-
|
|
397
|
-
@panopticon("code", "@", "exc")
|
|
398
|
-
def sysmon_py_unwind_arcs(
|
|
399
|
-
self, code: CodeType, instruction_offset: int, exception: BaseException,
|
|
400
|
-
) -> MonitorReturn:
|
|
401
|
-
"""Handle sys.monitoring.events.PY_UNWIND events for branch coverage."""
|
|
402
|
-
frame = self.callers_frame()
|
|
403
|
-
# Leaving this function.
|
|
404
|
-
last_line = self.last_lines.pop(frame, None)
|
|
405
|
-
if isinstance(exception, GeneratorExit):
|
|
406
|
-
# We don't want to count generator exits as arcs.
|
|
407
|
-
return
|
|
408
|
+
if self.stats is not None:
|
|
409
|
+
self.stats["returns"] += 1
|
|
408
410
|
code_info = self.code_infos.get(id(code))
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
411
|
+
# code_info is not None and code_info.file_data is not None, since we
|
|
412
|
+
# wouldn't have enabled this event if they were.
|
|
413
|
+
last_line = code_info.byte_to_line.get(instruction_offset) # type: ignore
|
|
414
|
+
if last_line is not None:
|
|
415
|
+
arc = (last_line, -code.co_firstlineno)
|
|
416
|
+
code_info.file_data.add(arc) # type: ignore
|
|
417
|
+
# log(f"adding {arc=}")
|
|
418
|
+
return DISABLE
|
|
415
419
|
|
|
416
420
|
@panopticon("code", "line")
|
|
417
|
-
def sysmon_line_lines(self, code: CodeType, line_number:
|
|
421
|
+
def sysmon_line_lines(self, code: CodeType, line_number: TLineNo) -> MonitorReturn:
|
|
418
422
|
"""Handle sys.monitoring.events.LINE events for line coverage."""
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
423
|
+
if self.stats is not None:
|
|
424
|
+
self.stats["line_lines"] += 1
|
|
425
|
+
code_info = self.code_infos.get(id(code))
|
|
426
|
+
# It should be true that code_info is not None and code_info.file_data
|
|
427
|
+
# is not None, since we wouldn't have enabled this event if they were.
|
|
428
|
+
# But somehow code_info can be None here, so we have to check.
|
|
429
|
+
if code_info is not None and code_info.file_data is not None:
|
|
430
|
+
code_info.file_data.add(line_number) # type: ignore
|
|
431
|
+
# log(f"adding {line_number=}")
|
|
432
|
+
return DISABLE
|
|
424
433
|
|
|
425
434
|
@panopticon("code", "line")
|
|
426
|
-
def sysmon_line_arcs(self, code: CodeType, line_number:
|
|
435
|
+
def sysmon_line_arcs(self, code: CodeType, line_number: TLineNo) -> MonitorReturn:
|
|
427
436
|
"""Handle sys.monitoring.events.LINE events for branch coverage."""
|
|
437
|
+
if self.stats is not None:
|
|
438
|
+
self.stats["line_arcs"] += 1
|
|
428
439
|
code_info = self.code_infos[id(code)]
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
440
|
+
# code_info is not None and code_info.file_data is not None, since we
|
|
441
|
+
# wouldn't have enabled this event if they were.
|
|
442
|
+
arc = (line_number, line_number)
|
|
443
|
+
code_info.file_data.add(arc) # type: ignore
|
|
444
|
+
# log(f"adding {arc=}")
|
|
445
|
+
return DISABLE
|
|
446
|
+
|
|
447
|
+
@panopticon("code", "@", "@")
|
|
448
|
+
def sysmon_branch_either(
|
|
449
|
+
self, code: CodeType, instruction_offset: TOffset, destination_offset: TOffset
|
|
450
|
+
) -> MonitorReturn:
|
|
451
|
+
"""Handle BRANCH_RIGHT and BRANCH_LEFT events."""
|
|
452
|
+
if self.stats is not None:
|
|
453
|
+
self.stats["branches"] += 1
|
|
454
|
+
code_info = self.code_infos[id(code)]
|
|
455
|
+
# code_info is not None and code_info.file_data is not None, since we
|
|
456
|
+
# wouldn't have enabled this event if they were.
|
|
457
|
+
if not code_info.branch_trails:
|
|
458
|
+
if self.stats is not None:
|
|
459
|
+
self.stats["branch_trails"] += 1
|
|
460
|
+
multiline_map = get_multiline_map(code.co_filename)
|
|
461
|
+
code_info.branch_trails = branch_trails(code, multiline_map=multiline_map)
|
|
462
|
+
code_info.always_jumps = always_jumps(code)
|
|
463
|
+
# log(f"branch_trails for {code}:\n{ppformat(code_info.branch_trails)}")
|
|
464
|
+
added_arc = False
|
|
465
|
+
dest_info = code_info.branch_trails.get(instruction_offset)
|
|
466
|
+
|
|
467
|
+
# Re-map the destination offset through always-jumps to deal with NOP etc.
|
|
468
|
+
dests = {destination_offset}
|
|
469
|
+
while (dest := code_info.always_jumps.get(destination_offset)) is not None:
|
|
470
|
+
destination_offset = dest
|
|
471
|
+
dests.add(destination_offset)
|
|
472
|
+
|
|
473
|
+
# log(f"dest_info = {ppformat(dest_info)}")
|
|
474
|
+
if dest_info is not None:
|
|
475
|
+
for arc, offsets in dest_info.items():
|
|
476
|
+
if arc is None:
|
|
477
|
+
continue
|
|
478
|
+
if dests & offsets:
|
|
479
|
+
code_info.file_data.add(arc) # type: ignore
|
|
480
|
+
# log(f"adding {arc=}")
|
|
481
|
+
added_arc = True
|
|
482
|
+
break
|
|
483
|
+
|
|
484
|
+
if not added_arc:
|
|
485
|
+
# This could be an exception jumping from line to line.
|
|
486
|
+
assert code_info.byte_to_line is not None
|
|
487
|
+
l1 = code_info.byte_to_line.get(instruction_offset)
|
|
488
|
+
if l1 is not None:
|
|
489
|
+
l2 = code_info.byte_to_line.get(destination_offset)
|
|
490
|
+
if l2 is not None and l1 != l2:
|
|
491
|
+
arc = (l1, l2)
|
|
492
|
+
code_info.file_data.add(arc) # type: ignore
|
|
493
|
+
# log(f"adding unforeseen {arc=}")
|
|
494
|
+
|
|
495
|
+
return DISABLE
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@functools.lru_cache(maxsize=5)
|
|
499
|
+
def get_multiline_map(filename: str) -> dict[TLineNo, TLineNo]:
|
|
500
|
+
"""Get a PythonParser for the given filename, cached."""
|
|
501
|
+
try:
|
|
502
|
+
parser = PythonParser(filename=filename)
|
|
503
|
+
parser.parse_source()
|
|
504
|
+
except NotPython:
|
|
505
|
+
# The file was not Python. This can happen when the code object refers
|
|
506
|
+
# to an original non-Python source file, like a Jinja template.
|
|
507
|
+
# In that case, just return an empty map, which might lead to slightly
|
|
508
|
+
# wrong branch coverage, but we don't have any better option.
|
|
509
|
+
return {}
|
|
510
|
+
except NoSource:
|
|
511
|
+
# This can happen if open() in python.py fails.
|
|
512
|
+
return {}
|
|
513
|
+
return parser.multiline_map
|
coverage/templite.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
|
2
|
-
# For details: https://github.com/
|
|
2
|
+
# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
|
|
3
3
|
|
|
4
4
|
"""A simple Python template renderer, for a nano-subset of Django syntax.
|
|
5
5
|
|
|
@@ -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."""
|
|
@@ -90,6 +89,10 @@ class Templite:
|
|
|
90
89
|
|
|
91
90
|
{% if var %}...{% endif %}
|
|
92
91
|
|
|
92
|
+
if-else::
|
|
93
|
+
|
|
94
|
+
{% if var %}...{% else %}...{% endif %}
|
|
95
|
+
|
|
93
96
|
Comments are within curly-hash markers::
|
|
94
97
|
|
|
95
98
|
{# This will be ignored #}
|
|
@@ -117,6 +120,7 @@ class Templite:
|
|
|
117
120
|
})
|
|
118
121
|
|
|
119
122
|
"""
|
|
123
|
+
|
|
120
124
|
def __init__(self, text: str, *contexts: dict[str, Any]) -> None:
|
|
121
125
|
"""Construct a Templite with the given `text`.
|
|
122
126
|
|
|
@@ -163,7 +167,7 @@ class Templite:
|
|
|
163
167
|
for token in tokens:
|
|
164
168
|
if token.startswith("{"):
|
|
165
169
|
start, end = 2, -2
|
|
166
|
-
squash = (token[-3] == "-")
|
|
170
|
+
squash = (token[-3] == "-") # fmt: skip
|
|
167
171
|
if squash:
|
|
168
172
|
end = -3
|
|
169
173
|
|
|
@@ -187,6 +191,14 @@ class Templite:
|
|
|
187
191
|
ops_stack.append("if")
|
|
188
192
|
code.add_line("if %s:" % self._expr_code(words[1]))
|
|
189
193
|
code.indent()
|
|
194
|
+
elif words[0] == "else":
|
|
195
|
+
if len(words) != 1:
|
|
196
|
+
self._syntax_error("Don't understand else", token)
|
|
197
|
+
if not ops_stack or ops_stack[-1] != "if":
|
|
198
|
+
self._syntax_error("Mismatched else", token)
|
|
199
|
+
code.dedent()
|
|
200
|
+
code.add_line("else:")
|
|
201
|
+
code.indent()
|
|
190
202
|
elif words[0] == "for":
|
|
191
203
|
# A loop: iterate over expression result.
|
|
192
204
|
if len(words) != 4 or words[2] != "in":
|