You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
sysmon + branch: statement after an async for whose iterator raises is reported as executed #2303
With the sysmon core and branch measurement, take an async for loop that ends because the async iterator raises an exception, not StopAsyncIteration. The statement after the loop is reported as executed, along with the arc from the loop to it, although it never runs. With COVERAGE_CORE=ctrace, and with sysmon without --branch, the line is correctly reported as missing. The equivalent synchronous for loop is measured correctly under sysmon with --branch.
coverage 7.16.1 with C extension (fresh venv, nothing else installed)
macOS 26.6.2, arm64
To Reproduce
target.py:
EXECUTED= []
asyncdefnumbers():
yield1raiseValueError("done")
asyncdefconsume():
asyncforninnumbers():
passEXECUTED.append("after loop") # never runs: the loop only ends by raising
sysmon without --branch is correct: target.py 8 1 88% 12.
The same over-report happens in the shape where I first hit it: try: async for attempt in <retrying async iterator>: with attempt: ... return ... / except ...: followed by an unreachable statement. There the iterator yields exception-swallowing context managers and re-raises from __anext__ when attempts run out, as tenacity's AsyncRetrying(reraise=True) does. The minimal form above needs neither the try/except nor the with.
Possibly the origin is in CPython: a bare sys.monitoring tool with LINE, BRANCH_LEFT and BRANCH_RIGHT enabled on consume.__code__ receives BRANCH_RIGHT from line 10 to an offset on line 12 when __anext__ raises, and no LINE event for line 12.
Impact: under the default 3.14 core with branch coverage, an unreachable line after such a loop (often a deliberate raise AssertionError("unreachable") tripwire) and its arc are over-reported as covered.
Describe the bug
With the sysmon core and branch measurement, take an
async forloop that ends because the async iterator raises an exception, notStopAsyncIteration. The statement after the loop is reported as executed, along with the arc from the loop to it, although it never runs. WithCOVERAGE_CORE=ctrace, and with sysmon without--branch, the line is correctly reported as missing. The equivalent synchronousforloop is measured correctly under sysmon with--branch.Versions
Python 3.14.5 (main, May 10 2026, 19:20:57) [Clang 22.1.3 ])To Reproduce
target.py:driver.py:Commands:
Actual (sysmon,
--branch).EXECUTEDis empty, so line 12 never ran, yet it is counted as covered:coverage jsonfor this run:executed_lines [1, 4, 5, 6, 9, 10, 11, 12],executed_branches [[10, 11], [10, 12]].Expected (and what ctrace gives):
coverage json:executed_lines [1, 4, 5, 6, 9, 10, 11],missing_branches [[10, 12]].Notes:
--branchis correct:target.py 8 1 88% 12.try: async for attempt in <retrying async iterator>: with attempt: ... return .../except ...:followed by an unreachable statement. There the iterator yields exception-swallowing context managers and re-raises from__anext__when attempts run out, as tenacity'sAsyncRetrying(reraise=True)does. The minimal form above needs neither thetry/exceptnor thewith.sys.monitoringtool with LINE, BRANCH_LEFT and BRANCH_RIGHT enabled onconsume.__code__receivesBRANCH_RIGHTfrom line 10 to an offset on line 12 when__anext__raises, and no LINE event for line 12.branch = truewith sysmon results in false negative #2289 (try/else +with, sysmon + branch false positive). For that example the same baresys.monitoringprobe receives no event for the unreachable line.Impact: under the default 3.14 core with branch coverage, an unreachable line after such a loop (often a deliberate
raise AssertionError("unreachable")tripwire) and its arc are over-reported as covered.