Search code examples
scalascalatestboolean-expressionshort-circuitingscalacheck

Have && short circuit with :| in expressions in ScalaCheck


I am attempting to create a property-based test in a Scala Test FlatSpec that uses the ScalaCheck :| operator to give failure messages for different parts of the ending boolean expression.

However, I am running into an issue where the && operator does not short circuit. In this case the earlier part of the expression checks to see if the next part of the expression can be run, otherwise that later section would throw an exception.

Here is an example of what the issue looks like. If decoded is None, then the expression should short circuit on the && so that decoded.get is not run, as it would throw an exception.

val value: Array[Int] = Array.fill(2)(0)
val encoded = encode(value)
val decoded: Option[Array[Int]] = decode(value)

decoded.isDefined :| "decoded None" &&
    value.sameElements(decoded.get)

When I write the boolean without using the :| operator to give a failure message, the test fails on the decoded.isDefined without throwing an exception.

val value: Array[Int] = Array.fill(2)(0)
val encoded = encode(value)
val decoded: Option[Array[Int]] = decode(value)

decoded.isDefined &&
    value.sameElements(decoded.get)

However, when I include a failure message with :|, it fails with a NoSuchElementException on the value.sameElements(decoded.get) line and does not display the failure message for decoded.isDefined even though it would evaluate to false.

The imports and test class declaration I am using are the following:

import org.scalacheck.Prop._
import org.scalatest.prop.Checkers
import org.scalatest.{FlatSpec, Matchers}

class ByteRWTests extends FlatSpec with Matchers with Checkers {

I am writing property checks in the following manner:

it should "be equal" in {
  check(
    forAll { (int: Int) =>
      int == int
    }
  )
}

Is there any way to get the short circuiting for && to work with expressions using :|, or is there a workaround for this issue?


Solution

  • Why you see the difference

    The issue is that while the && for booleans is short-circuiting, the && method on Prop isn't, and whether or not you use a label determines where the implicit conversion from boolean to Prop happens. For example:

    import org.scalacheck.Prop, Prop._
    
    val value: Array[Int] = Array.fill(2)(0)
    val decoded: Option[Array[Int]] = None
    
    val p1: Prop = decoded.isDefined && value.sameElements(decoded.get)
    val p2: Prop = decoded.isDefined :| "decoded None" && value.sameElements(decoded.get)
    

    Here the p1 definition desugars to this:

    Prop.propBoolean(decoded.isDefined && value.sameElements(decoded.get))
    

    While p2 gives you this:

    (Prop.propBoolean(decoded.isDefined) :| "decoded None").&&(
      Prop.propBoolean(value.sameElements(decoded.get))
    )
    

    (For what it's worth this is another example of why I don't like fancy implicit-conversion-based DSLs.)

    Workaround

    Unfortunately it's just not possible to get the && method on Prop to do what you want here, but you can define your own version of conjunction that does:

    def propAnd(p1: => Prop, p2: => Prop) = p1.flatMap { r =>
      if (r.success) Prop.secure(p2) else Prop(_ => r)
    }
    

    And then:

    scala> propAnd(decoded.isDefined :| "decoded None" , value.sameElements(decoded.get))
    res1: org.scalacheck.Prop = Prop
    
    scala> .check
    ! Falsified after 0 passed tests.
    > Labels of failing property: 
    decoded None
    

    This method actually exists in ScalaCheck, but it's not part of the public API. A comment on the implementation does note that it "(Should maybe be in Prop module)", so if you find you're doing this kind of thing a lot, you might try opening a pull request to move the method from Commands to Prop and make it public.

    In the specific case you've given here, though, I'd probably suggest using something like exists on the Option instead of checking whether it's defined and then using get.