Skip to content

Commit c177731

Browse files
committed
fix: see through nop bytecodes to get the right arcs. #1999
1 parent 7a83ab0 commit c177731

File tree

4 files changed

+69
-5
lines changed

4 files changed

+69
-5
lines changed

CHANGES.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ upgrading your version of coverage.py.
2323
Unreleased
2424
----------
2525

26-
Nothing yet.
26+
- Fix: some code with NOP bytecodes could report missing branches that are
27+
actually executed. This is now fixed, closing `issue 1999`_. Python 3.9
28+
still shows the problem.
29+
30+
.. _issue 1999: https://github.com/nedbat/coveragepy/issues/1999
2731

2832

2933
.. start-releases

coverage/bytecode.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,16 @@ def op_set(*op_names: str) -> set[int]:
4343
)
4444

4545
# Opcodes that exit from a function.
46-
RETURNS = op_set("RETURN_VALUE", "RETURN_GENERATOR")
46+
RETURNS = op_set(
47+
"RETURN_VALUE",
48+
"RETURN_GENERATOR",
49+
)
50+
51+
# Opcodes that do nothing.
52+
NOPS = op_set(
53+
"NOP",
54+
"NOT_TAKEN",
55+
)
4756

4857

4958
class InstructionWalker:
@@ -126,7 +135,7 @@ def walk_one_branch(start_at: TOffset) -> TBranchTrail:
126135
# pylint: disable=cell-var-from-loop
127136
inst_offsets: set[TOffset] = set()
128137
to_line = None
129-
for inst2 in iwalker.walk(start_at=start_at):
138+
for inst2 in iwalker.walk(start_at=start_at, follow_jumps=True):
130139
inst_offsets.add(inst2.offset)
131140
if inst2.line_number and inst2.line_number != from_line:
132141
to_line = inst2.line_number
@@ -160,3 +169,18 @@ def walk_one_branch(start_at: TOffset) -> TBranchTrail:
160169
the_trails[offset].append(trail)
161170

162171
return the_trails
172+
173+
174+
def always_jumps(code: CodeType) -> dict[TOffset, TOffset]:
175+
"""Make a map of unconditional bytecodes jumping to others.
176+
177+
Only include bytecodes that do no work and go to another bytecode.
178+
"""
179+
jumps = {}
180+
iwalker = InstructionWalker(code)
181+
for inst in iwalker.walk(follow_jumps=False):
182+
if inst.opcode in ALWAYS_JUMPS:
183+
jumps[inst.offset] = inst.jump_target
184+
elif inst.opcode in NOPS:
185+
jumps[inst.offset] = inst.offset + 2
186+
return jumps

coverage/sysmon.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
)
2525

2626
from coverage import env
27-
from coverage.bytecode import TBranchTrails, branch_trails
27+
from coverage.bytecode import TBranchTrails, always_jumps, branch_trails
2828
from coverage.debug import short_filename, short_stack
2929
from coverage.misc import isolate_module
3030
from coverage.types import (
@@ -184,6 +184,10 @@ class CodeInfo:
184184
# Two possible trails from the branch point, left and right.
185185
branch_trails: TBranchTrails
186186

187+
# Always-jumps are bytecode offsets that do no work but move
188+
# to another offset.
189+
always_jumps: dict[TOffset, TOffset]
190+
187191

188192
def bytes_to_lines(code: CodeType) -> dict[TOffset, TLineNo]:
189193
"""Make a dict mapping byte code offsets to line numbers."""
@@ -348,6 +352,7 @@ def sysmon_py_start(
348352
file_data=file_data,
349353
byte_to_line=b2l,
350354
branch_trails={},
355+
always_jumps={},
351356
)
352357
self.code_infos[id(code)] = code_info
353358
self.code_objects.append(code)
@@ -431,15 +436,23 @@ def sysmon_branch_either(
431436
if self.stats is not None:
432437
self.stats["branch_trails"] += 1
433438
code_info.branch_trails = branch_trails(code)
439+
code_info.always_jumps = always_jumps(code)
434440
# log(f"branch_trails for {code}:\n {code_info.branch_trails}")
435441
added_arc = False
436442
dest_info = code_info.branch_trails.get(instruction_offset)
443+
444+
# Re-map the destination offset through always-jumps to deal with NOP etc.
445+
dests = {destination_offset}
446+
while (dest := code_info.always_jumps.get(destination_offset)) is not None:
447+
destination_offset = dest
448+
dests.add(destination_offset)
449+
437450
# log(f"{dest_info = }")
438451
if dest_info is not None:
439452
for offsets, arc in dest_info:
440453
if arc is None:
441454
continue
442-
if destination_offset in offsets:
455+
if dests & offsets:
443456
code_info.file_data.add(arc) # type: ignore
444457
# log(f"adding {arc=}")
445458
added_arc = True

tests/test_arcs.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2129,6 +2129,29 @@ def func():
21292129
branchz_missing="29 38 45 56 5. 9A 9.",
21302130
)
21312131

2132+
@pytest.mark.skipif(env.PYVERSION[:2] == (3, 9), reason="not sure why it isn't fixed on 3.9")
2133+
def test_bug_1999(self) -> None:
2134+
self.check_coverage("""\
2135+
import asyncio
2136+
2137+
async def async_range(number):
2138+
for n in range(number):
2139+
yield n
2140+
2141+
async def do_something(number):
2142+
try:
2143+
async for n in async_range(number):
2144+
print(n)
2145+
finally:
2146+
print("Done.")
2147+
2148+
asyncio.run(do_something(3))
2149+
""",
2150+
branchz="4-3 45 9A 9C",
2151+
branchz_missing="",
2152+
)
2153+
assert self.stdout() == "0\n1\n2\nDone.\n"
2154+
21322155

21332156
class AnnotationTest(CoverageTest):
21342157
"""Tests using type annotations."""

0 commit comments

Comments
 (0)