coveragepy
coveragepy copied to clipboard
Exclusion of body doesn't propagate to `case` block
Describe the bug
If the body of a case block inside a match statement is excluded from coverage, the case line is still marked as not covered.
To Reproduce
Code under test - testcase.py:
if 1 != 1:
assert False
match 1:
case 1:
print("all is well")
case _: # reported as uncovered
assert False
Configuration - .coveragerc:
[report]
exclude_lines =
assert False$
Commands:
$ coverage run --source=. -m testcase
all is well
$ coverage report -m
Name Stmts Miss Cover Missing
-------------------------------------------
testcase.py 5 1 80% 7
-------------------------------------------
TOTAL 5 1 80%
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 for if 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.
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