Scoverage: enforce 100% branch coverage on "formally infinite" `while(true)` loops

1k views Asked by At

The following simple code snippet contains a while-loop that looks as if it could be infinite:

  def findDivisor(n: Int): Int = {
    require(n >= 2)
    var i = 2
    while (true) {
      if (n % i == 0) {
        return i
      } else {
        // do-nothing branch
      }
      i += 1
    }
    // $COVERAGE-OFF$
    throw new Error("unreachable")
    // $COVERAGE-ON$
  }

Basic math guarantees that this method always terminates (even if it cannot find a proper divisor, it must stop at n).

Despite the $COVERAGE-OFF$ right after the while-loop, Scoverage (and maybe some other coverage tools) will complain, and compute only 75% branch coverage (because while counts as a branch point, and the false branch is never taken before return).

Moving the // $COVERAGE-OFF$ around, e.g. before the closing } of the while-body does not help either.

How do I force it to ignore the impossible branch?

2

There are 2 answers

0
Andrey Tyukin On BEST ANSWER

Just wrap the while(true) { loop head into a separate pair of $COVERAGE-OFF$-$COVERAGE-ON$ comments:

  def findDivisor(n: Int): Int = {
    require(n >= 2)
    var i = 2
    // $COVERAGE-OFF$
    while (true) {
      // $COVERAGE-ON$
      if (n % i == 0) {
        return i
      } else {
        // do-nothing branch
      }
      i += 1
      // $COVERAGE-OFF$
    }
    throw new Error("unreachable")
    // $COVERAGE-ON$
  }

Now Scoverage makes sure that each statement in the body of the while-loop is covered, but it ignores the false-branch, and reports 100% test coverage e.g. after the following simple test:

  "Whatever" should "do something" in {
    MyObjectName.findDivisor(57) should be(3)
  }
1
Brian McCutchon On

I'd suggest that, instead of working against the compiler, you provide your code in terms the compiler can understand. The compiler understands infinite recursion.

@tailrec def forever(op: => Unit): Nothing = {
  op
  forever(op)
}

def findDivisor(n: Int): Int = {
  require(n >= 2)
  var i = 2
  forever {
    if (n % i == 0) {
      return i
    } else {
      // do-nothing branch
    }
    i += 1
  }
}

forever has no branches, so your coverage tool ought to be happy, and, as a bonus, you no longer need a dummy exception.