Open mthuurne opened 1 year ago
Picking this up for the PyCon 2023 sprints.
I need input from @nedbat on if this is actually a bug. Until then, I'm going to articulate the cause of the current behaviour and comparable behaviour that exists elsewhere.
The current bug is that the body of the case
statement is excluded from coverage, therefore it would make sense for the case
itself to be excluded from coverage. This is the case of if ... else
statements, as stated in the original issue, and as such it would make sense to be consistent.
The current bug is caused by an arc between the current case
statement and the previous case
statement, with the expectation that the current case
statement would always be executed as long as the previous case
statement is not catching all of the cases. This is similar to the behaviour of if ... else
where the else
contains a nested if ... else
where the statement within that is excluded. Here's a code example of that with the same exclusion (assert False$
) used in the original issue:
if True:
print("a")
else:
if False:
assert False
This example would result in the else
being flagged as missing coverage even though it contains a block which contains a block which fails. I believe this behaviour is expected, but it shows a similar dependency in logic to that which occurs within match ... case
statements.
The reason why the if
statement within your original example does not fail is because the if
statement is covered, but the branch beneath it is not. For the corresponding case _
though, the case itself is not covered because it is never executed because the match
always detects the first case 1
as being the expected path.
Based on the explanation given for how this works, I believe this is in fact not a bug and is expected behaviour for keeping track of code coverage. Coverage checks do not bubble up to the callers for if
statements, therefore they should not bubble up for case
statements.
Would it make sense to special case case _
to behave like else
here? I think this propagation actually only happens for else
blocks and not if
or elif
anyway.
[tool.coverage.report]
exclude_also = [
"assert False",
]
testcase.py
foo = 1
if foo == 1:
print("foo")
elif foo == 2:
assert False
else:
assert False
match foo:
case 1:
print("foo")
case 2:
assert False
case _:
assert False
Commands:
$ poetry run coverage run -m testcase
foo
foo
$ poetry run coverage report -m --include testcase.py
Name Stmts Miss Cover Missing
-------------------------------------------
testcase.py 9 3 67% 5, 13-15
-------------------------------------------
TOTAL 9 3 67%
Line 5, elif foo == 2:
, is marked as not covered which makes sense as the expression was never evaluated. Similarly line 13 case 2
is marked as not covered.
The difference is line 7, else:
being considered covered while the equivalent line 15 case _:
is not.
Python 3.10 coverage 7.4.4
You can work around this with the new multi-line patterns in 7.6.0:
[tool.coverage.report]
exclude_also = [
"assert False", # For else blocks and functions
"case _:\\n\\s*assert False", # For wildcard cases
]
Describe the bug
If the body of a
case
block inside amatch
statement is excluded from coverage, thecase
line is still marked as not covered.To Reproduce
Code under test -
testcase.py
:Configuration -
.coveragerc
:Commands:
I'm using
coverage
version 7.2.0 with Python 3.10 on Ubuntu Linux 22.04.Expected behavior
I would expect the exclusion to be propagated to the surrounding block for
case
blocks similarly to how it does forif
blocks.Note that when enabling branch coverage measurement, the
case 1:
line is reported as partially covered, but I expect that is a direct consequence of line 8 being considered uncovered, so not a separate issue.