Scoverage:对 "formally infinite" `while(true)` 循环执行 100% 的分支覆盖

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

下面的简单代码片段包含一个 while 循环,看起来好像是无限的:

  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$
  }

基础数学保证此方法始终终止(即使找不到合适的除数,它也必须在 n 处停止)。

尽管 $COVERAGE-OFF$ 紧接在 while 循环之后,Scoverage(可能还有其他一些覆盖工具)会报错,并且只计算 75% 的分支覆盖(因为 while 计数作为一个分支点,并且 false 分支在 return).

之前从未被采用

四处移动 // $COVERAGE-OFF$,例如在 while-body 结束 } 之前也无济于事。

如何强制它忽略不可能的分支?

只需将 while(true) { 循环头包装成一对单独的 $COVERAGE-OFF$-$COVERAGE-ON$ 评论:

  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$
  }

现在 Scoverage 确保 while-循环主体中的每个语句都被覆盖,但它忽略 false-分支,并报告 100% 测试覆盖率,例如经过以下简单测试:

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

我建议,不要与编译器对抗,而是以编译器可以理解的方式提供代码。编译器理解无限递归。

@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 没有分支,因此您的覆盖工具应该会很满意,而且,作为奖励,您不再需要虚拟异常。