coveragepy
coveragepy copied to clipboard
any() + generator expression regression in 6.0
Describe the bug
Statements using any()
with a generator expression are considered as having a branch on Coverage 6.0b1+, and there's no way to cover the ->exit
branch.
To Reproduce
Python: checked with 3.9 and 3.10 Coverage versions: no problem on 5.5, problem on 6.0b1+ Packages: none Code:
queries = ["select 1 from ..."]
any(q for q in queries)
Running:
coverage erase && coverage run example.py && coverage report --show-missing
Old versions output:
Name Stmts Miss Branch BrPart Cover Missing
--------------------------------------------------------
example.py 2 0 2 0 100%
--------------------------------------------------------
TOTAL 2 0 2 0 100%
New versions:
Name Stmts Miss Branch BrPart Cover Missing
--------------------------------------------------------
example.py 2 0 2 1 75% 2->exit
--------------------------------------------------------
TOTAL 2 0 2 1 75%
Expected behavior
The generator expression is passed to any()
, which will iterate it, so there's no reason to mark it as a branch.
Additional context
I encountered this whilst writing a test like:
def f():
queries = ["select 1 from ..."]
assert not any(q.startswith("insert ") for q in queries)
Hm, on coverage 5.5 this seems to happen only with --timid
.
The missing "branch" is perhaps the fact that you don't iterate over the entire generator, i.e. you don't exit the generator. I'm struggling to find any value in that, though.
Hm, this doesn't reproduce for a handwritten generator:
def g():
yield 42
any(g())
In fact, it doesn't reproduce for:
any(a for a in ['a'])
but reproduces for:
pass
any(a for a in ['a'])
@adamchainz @ikonst I can definitely reproduce what you are seeing.
I don't understand yet why it's a difference between 5.5 and 5.6b1, since the specific code involved in analyzing those cases and output the condition (which you can see in the HTML report: "line 2 didn't finish the generator expression on line 2") didn't change between those two versions.
But to the larger question: is it useful to mark an unfinished generator expression (or any comprehension) as an untaken branch? We also mark an untaken branch of a fully-formed for loop doesn't complete its iteration. Maybe the key here is any() or all(), which are designed to short-circuit, but that feels like a larger analysis than coverage.py usually does.
For completeness, git bisect says 3cd4db3248fe48c3a531855227a9b2a3846e0110 is when this behavior started.
But to the larger question: is it useful to mark an unfinished generator expression (or any comprehension) as an untaken branch? We also mark an untaken branch of a fully-formed for loop doesn't complete its iteration. Maybe the key here is any() or all(), which are designed to short-circuit, but that feels like a larger analysis than coverage.py usually does.
If it’s possible to mark it as covered if at least one iteration occurs, that seems like a reasonable approach to me.