Why does coverage.py not report a for loop where the body is not skipped when running it with –branch?

Question:

Consider the following example file test.py:

def fun(l):
    for i in l:
        a = 1
    print(a)

if __name__ == '__main__':
    fun([1])

Testing branch coverage with

coverage erase; coverage run --branch test.py ; coverage html

There are no complains about the for loop although it is not tested with an empty list.
If it was the program would crash with "UnboundLocalError: local variable ‘a’ referenced before assignment".
Since I have asked coverage for branch coverage instead of statement coverage I would expect this to be reported. Why does coverage not report this?

screenshot of HTML report created by coverage

Asked By: jakun

||

Answers:

Maybe it helps to understand how branch coverage works:

When measuring branches, coverage.py collects pairs of line numbers, a source and destination for each transition from one line to another. Static analysis of the source provides a list of possible transitions. Comparing the measured to the possible indicates missing branches.

1| def fun(l):
2|     for i in l:
3|         a = 1
4|     print(a)

There are two branches here:

  • L2 can proceed to L3
  • L2 can jump to L4

The fun([1]) call in the test actually fully covers both branches. On the first iteration of the loop, we have line 2->3, and on the final iteration, we have jump 2->4.

Remember that 100% coverage does not imply the code actually works in all cases, it just means that all those lines and branches have been visited during the test suite.

If you modify this code so that only one of the branches of the for gets visited, you will see a partial cover highlighted on L3:

enter image description here

Answered By: wim