Skip to content

Branch coverage of try/except #877

Open
@bensimner

Description

@bensimner

Is your feature request related to a problem? Please describe.
Consider the following code:

def f(x):
  try:
    y = 1/x
  except ZeroDivisionError:
    y = 0
  return y


def test_exception_branch_coverage():
  f(0)


test_exception_branch_coverage()

Here f(0) clearly misses a path: the case where 1/x did not raise an exception isn't covered. However Coverage.py (version 4.5.4) reports 100% statement and branch coverage.

Describe the solution you'd like
A try/except is like a branch. Consider the following:

def g(x):
  try:
    y = 1/x
  except ZeroDivisionError:
    y = 0
  else:
    pass  # this line wasn't executed, but coverage doesn't report it as missed
  return y

g(0)

The else represents a branch target that was never taken. Coverage could know about the branch intrinsic in the try/except/else itself: whether an exception was raised or not.

Describe alternatives you've considered
The else block does not always represent a missed branch:

def h(x):
  try:
    return 1/x
  except ZeroDivisionError:
    return 0
  else:
    pass  # never executed, but this does not imply a missed branch ...

h(3)
h(0)

Instead of just looking at whether the whole try threw an exception, one could consider each line individually:

def i(x):
  try:
    a = 1/(x+1)
    b = 1/(x+2)
    return a + b
  except ZeroDivisionError:
    return 0

i(0)
i(-1)

At first, it appears that all branches were covered by the above. i(0) executes the entire try block and throws no exception. i(-1) throws an exception and executes the except block.
However, both a = ... and b = ... could raise exceptions and only one of them was tested.

Keeping track of each line like this is tracking too much: in real code, not every line can raise interesting exceptions and those that don't shouldn't be considered missed branches:

def j(x):
  try: 
    y = x[0]
    return y + 1  # this line never raised an IndexError, but that's ok
  except IndexError:
    return None

j([])
j([1])

The return y + 1 line cannot raise an IndexError and therefore isn't a missed branch.

Additional context
It's not clear whether coverage tools should consider these "branches", or whether they should even report this kind of thing. One would expect that most of the time, statement coverage will catch a try/except that misses one of these cases, and it's only in the situation where there's a one-line try where later code is not conditional on the exception happening/not happening.

Maybe if added behaviours like this should be hidden behind flags for those that want more pedantic coverage metrics.

With thanks to @edk0 who originally pointed this out and for comments.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions