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?
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!).