scalascalacheck

How to generate two sets of the same size using ScalaCheck


I am trying to develop property-based tests for a matching algorithm and I need to generate two inputs sets of the same size to feed into the algorithm. My current attempt at a solution is the following.

case class Man(id: Long, quality: Long, ordering: Ordering[Woman])
case class Woman(id: Long, quality: Long, ordering: Ordering[Man])

val man: Gen[Man] = {
  for {
    id <- Gen.posNum[Long]
    quality <- Gen.posNum[Long]
  } yield Man(id, quality, Man.womanByQuality)
}

val woman: Gen[Woman] = {
  for {
    id <- Gen.posNum[Long]
    quality <- Gen.posNum[Long]
  } yield Woman(id, quality, Woman.manByQuality)
}  

def setOfN[T](n: Int, g: Gen[T]): Gen[Set[T]] = {
  Gen.containerOfN[Set, T](n, g)
}

def unMatched: Gen[(Set[Man], Set[Woman])] = Gen.sized {
  n => setOfN(n, man).flatMap(ms => setOfN(ms.size, woman).map(ws => (ms, ws)))
}

This generates tuples of input sets as required, but they are not guaranteed to be the same size. When I run the test using...

property("all men and women are matched") = forAll(unMatched) {
  case (ms, ws) =>
    println((ms.size, ws.size))
    val matches = DeferredAcceptance.weaklyStableMatching(ms, ws)
    (matches.size == ms.size) && (matches.size == ws.size)
}

The console will print something like...

(0,0)
(1,1)
(2,2)
(3,2)
(1,2)
(0,2)
(0,1)
(0,0)
! marriage-market.all men and women are matched: Exception raised on proper
  ty evaluation.
> ARG_0: (Set(),Set(Woman(1,1,scala.math.Ordering$$anon$10@3d8314f0)))
> ARG_0_ORIGINAL: (Set(Man(3,1,scala.math.Ordering$$anon$10@2bea5ab4), Man(
  2,1,scala.math.Ordering$$anon$10@2bea5ab4), Man(2,3,scala.math.Ordering$$
  anon$10@2bea5ab4)),Set(Woman(1,1,scala.math.Ordering$$anon$10@3d8314f0), 
  Woman(3,2,scala.math.Ordering$$anon$10@3d8314f0)))
> Exception: java.lang.IllegalArgumentException: requirement failed
scala.Predef$.require(Predef.scala:264)
org.economicsl.matching.DeferredAcceptance$.weaklyStableMatching(DeferredAc
  ceptance.scala:97)
org.economicsl.matching.MarriageMarketSpecification$.$anonfun$new$2(Marriag
  eMarketSpecification.scala:54)
org.economicsl.matching.MarriageMarketSpecification$.$anonfun$new$2$adapted
  (MarriageMarketSpecification.scala:51)
org.scalacheck.Prop$.$anonfun$forAllShrink$2(Prop.scala:761)
Found 1 failing properties.

Process finished with exit code 1

The test fails because I have included a requirement that the two input sets must be of equal size. My intent is that the generator should supply valid input data.

Thoughts?


Solution

  • I have stumbled across the following solution.

    def unMatched: Gen[(Set[Man], Set[Woman])] = Gen.sized {
      n => setOfN(n, man).flatMap(ms => setOfN(ms.size, woman).map(ws => (ms, ws))).suchThat { case (ms, ws) => ms.size == ws.size }
    }
    

    But I don't think it should be necessary to use the suchThat combinator. The issue seems to be that the size parameter is treated as an upper bound for size of the container (rather than an equality constraint).

    Updated based on comments from @FlorianK

    I discovered that the problem was with my specification of the Man and Woman generators. These generators were not generator distinct values. Instead of using a positive Long to represent the unique id I switched to using a Java UUID. Correct generators are

    val man: Gen[Man] = {
      for {
        id <- Gen.uuid
        quality <- Gen.posNum[Long]
      } yield Man(id, quality, Man.womanByQuality)
    }
    
    val woman: Gen[Woman] = {
      for {
        id <- Gen.uuid
        quality <- Gen.posNum[Long]
      } yield Woman(id, quality, Woman.manByQuality)
    }
    

    I am not quite sure why the original generators did not work as expected. It was certainly possible for them to generate non-unique instances but I thought it should have been exceedingly rare (guess I was wrong!).