coveragepy icon indicating copy to clipboard operation
coveragepy copied to clipboard

any() + generator expression regression in 6.0

Open adamchainz opened this issue 2 years ago • 2 comments

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)

adamchainz avatar Mar 01 '22 08:03 adamchainz

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.

ikonst avatar Mar 15 '22 04:03 ikonst

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'])

ikonst avatar Mar 15 '22 05:03 ikonst

@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.

nedbat avatar Nov 04 '22 11:11 nedbat

For completeness, git bisect says 3cd4db3248fe48c3a531855227a9b2a3846e0110 is when this behavior started.

nedbat avatar Nov 04 '22 11:11 nedbat

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.

adamchainz avatar Nov 09 '22 13:11 adamchainz