Skip to content

Commit 309d9d3

Browse files
committed
Implement labelledExamples
1 parent c790104 commit 309d9d3

File tree

5 files changed

+150
-52
lines changed

5 files changed

+150
-52
lines changed

core/shared/src/main/scala/hedgehog/core/Coverage.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ object Coverage {
8989
def empty[A]: Coverage[A] =
9090
Coverage(Map.empty[LabelName, Label[A]])
9191

92+
def labels(cv: Coverage[_]): List[LabelName] =
93+
cv.labels.keysIterator.toList
94+
95+
def covers(cv: Coverage[Cover], name: LabelName): Boolean =
96+
cv.labels.get(name).exists(_.annotation match {
97+
case Cover.Cover =>
98+
true
99+
case Cover.NoCover =>
100+
false
101+
})
102+
92103
def count(cv: Coverage[Cover]): Coverage[CoverCount] =
93104
cv.copy(labels = cv.labels.map { case (k, l) =>
94105
k -> l.copy(annotation = CoverCount.fromCover(l.annotation))
@@ -105,3 +116,16 @@ object Coverage {
105116
}
106117
}
107118

119+
case class Examples(examples: Map[LabelName, List[Log]])
120+
121+
object Examples {
122+
123+
def empty[A]: Examples =
124+
Examples(Map.empty[LabelName, List[Log]])
125+
126+
def addTo(examples: Examples, labels: List[LabelName])(seek: LabelName => List[Log]): Examples = {
127+
Examples(labels.foldLeft(examples.examples) { (m, name) =>
128+
m.updated(name, m.get(name).filter(_.nonEmpty).getOrElse(seek(name)))
129+
})
130+
}
131+
}

core/shared/src/main/scala/hedgehog/core/PropertyT.scala

Lines changed: 88 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,13 @@ case class PropertyConfig(
4646
testLimit: SuccessCount
4747
, discardLimit: DiscardCount
4848
, shrinkLimit: ShrinkLimit
49+
, withExamples: WithExamples
4950
)
5051

5152
object PropertyConfig {
5253

5354
def default: PropertyConfig =
54-
PropertyConfig(SuccessCount(100), DiscardCount(100), ShrinkLimit(1000))
55+
PropertyConfig(SuccessCount(100), DiscardCount(100), ShrinkLimit(1000), WithExamples.NoExamples)
5556
}
5657

5758
case class PropertyT[A](
@@ -132,38 +133,51 @@ object PropertyT {
132133
trait PropertyTReporting {
133134

134135
@annotation.tailrec
135-
final def takeSmallest(n: ShrinkCount, slimit: ShrinkLimit, t: Tree[Option[(List[Log], Option[Result])]]): Status =
136-
t.value match {
136+
final def takeSmallestG[A, B](n: ShrinkCount, slimit: ShrinkLimit, t: Tree[A])(p: A => Boolean)(e: (ShrinkCount, A) => B): B = {
137+
if (n.value < slimit.value && p(t.value)) {
138+
findMap(t.children.value)(m => if (p(m.value)) some(m) else Option.empty) match {
139+
case None =>
140+
e(n, t.value)
141+
142+
case Some(m) =>
143+
takeSmallestG(n.inc, slimit, m)(p)(e)
144+
}
145+
} else {
146+
e(n, t.value)
147+
}
148+
}
149+
150+
def takeSmallest(n: ShrinkCount, slimit: ShrinkLimit, t: Tree[Option[(Journal, Option[Result])]]): Status =
151+
takeSmallestG(n, slimit, t) {
137152
case None =>
153+
false
154+
155+
case Some((_, r)) =>
156+
r.forall(!_.success)
157+
} {
158+
case (_, None) =>
138159
Status.gaveUp
139160

140-
case Some((w, r)) =>
141-
if (r.forall(!_.success)) {
142-
if (n.value >= slimit.value) {
143-
Status.failed(n, w ++ r.map(_.logs).getOrElse(Nil))
144-
} else {
145-
findMap(t.children.value)(m =>
146-
m.value match {
147-
case Some((_, None)) =>
148-
some(m)
149-
case Some((_, Some(r2))) =>
150-
if (!r2.success)
151-
some(m)
152-
else
153-
Option.empty
154-
case None =>
155-
Option.empty
156-
}
157-
) match {
158-
case Some(m) =>
159-
takeSmallest(n.inc, slimit, m)
160-
case None =>
161-
Status.failed(n, w ++ r.map(_.logs).getOrElse(Nil))
162-
}
163-
}
164-
} else {
161+
case (n, Some((j, r))) =>
162+
if (r.exists(_.success))
165163
Status.ok
166-
}
164+
else
165+
Status.failed(n, j.logs ++ r.map(_.logs).getOrElse(Nil))
166+
}
167+
168+
def takeSmallestExample(n: ShrinkCount, slimit: ShrinkLimit, name: LabelName, t: Tree[Option[(Journal, Option[Result])]]): List[Log] =
169+
takeSmallestG(n, slimit, t) {
170+
case None =>
171+
false
172+
173+
case Some((j, r)) =>
174+
r.exists(_.success) && Coverage.covers(j.coverage, name)
175+
} {
176+
case (_, None) =>
177+
Nil
178+
179+
case (_, Some((j, _))) =>
180+
j.logs
167181
}
168182

169183
def report(config: PropertyConfig, size0: Option[Size], seed0: Seed, p: PropertyT[Result]): Report = {
@@ -172,17 +186,25 @@ trait PropertyTReporting {
172186
// Start the size at whatever remainder we have to ensure we run with "max" at least once
173187
val sizeInit = Size((Size.max % Math.min(config.testLimit.value, Size.max)) + sizeInc.value)
174188
@annotation.tailrec
175-
def loop(successes: SuccessCount, discards: DiscardCount, size: Size, seed: Seed, coverage: Coverage[CoverCount]): Report =
189+
def loop(successes: SuccessCount, discards: DiscardCount, size: Size, seed: Seed, coverage: Coverage[CoverCount], examples: Examples): Report =
176190
if (successes.value >= config.testLimit.value)
177191
// we've hit the test limit
178192
Coverage.split(coverage, successes) match {
179193
case (_, Nil) =>
180-
Report(successes, discards, coverage, OK)
194+
config.withExamples match {
195+
case WithExamples.WithExamples =>
196+
if (examples.examples.exists(_._2.isEmpty))
197+
Report(successes, discards, coverage, examples, Status.failed(ShrinkCount(0), List("Insufficient examples.")))
198+
else
199+
Report(successes, discards, coverage, examples, OK)
200+
case WithExamples.NoExamples =>
201+
Report(successes, discards, coverage, examples, OK)
202+
}
181203
case _ =>
182-
Report(successes, discards, coverage, Status.failed(ShrinkCount(0), List("Insufficient coverage.")))
204+
Report(successes, discards, coverage, examples, Status.failed(ShrinkCount(0), List("Insufficient coverage.")))
183205
}
184206
else if (discards.value >= config.discardLimit.value)
185-
Report(successes, discards, coverage, GaveUp)
207+
Report(successes, discards, coverage, examples, GaveUp)
186208
else {
187209
val x =
188210
try {
@@ -191,23 +213,33 @@ trait PropertyTReporting {
191213
case e: Exception =>
192214
Property.error(e).run.run(size, seed)
193215
}
194-
val t = x.map(_._2.map { case (l, r) => (l.logs, r) })
216+
val t = x.map(_._2)
195217
x.value._2 match {
196218
case None =>
197-
loop(successes, discards.inc, size.incBy(sizeInc), x.value._1, coverage)
198-
199-
case Some((_, None)) =>
200-
Report(successes, discards, coverage, takeSmallest(ShrinkCount(0), config.shrinkLimit, t))
201-
202-
case Some((j, Some(r))) =>
203-
if (!r.success){
204-
Report(successes, discards, coverage, takeSmallest(ShrinkCount(0), config.shrinkLimit, t))
205-
} else
206-
loop(successes.inc, discards, size.incBy(sizeInc), x.value._1,
207-
Coverage.union(Coverage.count(j.coverage), coverage)(_ + _))
219+
loop(successes, discards.inc, size.incBy(sizeInc), x.value._1, coverage, examples)
220+
221+
case Some((j, r)) =>
222+
if (r.forall(!_.success)) {
223+
Report(successes, discards, coverage, examples, takeSmallest(ShrinkCount(0), config.shrinkLimit, t))
224+
} else {
225+
val coverage2 = Coverage.union(Coverage.count(j.coverage), coverage)(_ + _)
226+
val examples2 =
227+
config.withExamples match {
228+
case WithExamples.WithExamples =>
229+
Examples.addTo(examples, Coverage.labels(j.coverage)) { name =>
230+
if (Coverage.covers(j.coverage, name))
231+
takeSmallestExample(ShrinkCount(0), config.shrinkLimit, name, t)
232+
else
233+
Nil
234+
}
235+
case WithExamples.NoExamples =>
236+
examples
237+
}
238+
loop(successes.inc, discards, size.incBy(sizeInc), x.value._1, coverage2, examples2)
239+
}
208240
}
209241
}
210-
loop(SuccessCount(0), DiscardCount(0), size0.getOrElse(sizeInit), seed0, Coverage.empty)
242+
loop(SuccessCount(0), DiscardCount(0), size0.getOrElse(sizeInit), seed0, Coverage.empty, Examples.empty)
211243
}
212244

213245
def recheck(config: PropertyConfig, size: Size, seed: Seed)(p: PropertyT[Result]): Report =
@@ -247,6 +279,15 @@ case class DiscardCount(value: Int) {
247279
DiscardCount(value + 1)
248280
}
249281

282+
/** Whether the report should include an example for each label. */
283+
sealed trait WithExamples
284+
285+
object WithExamples {
286+
287+
case object WithExamples extends WithExamples
288+
case object NoExamples extends WithExamples
289+
}
290+
250291
/**
251292
* The status of a property test run.
252293
*
@@ -270,4 +311,4 @@ object Status {
270311
OK
271312
}
272313

273-
case class Report(tests: SuccessCount, discards: DiscardCount, coverage: Coverage[CoverCount], status: Status)
314+
case class Report(tests: SuccessCount, discards: DiscardCount, coverage: Coverage[CoverCount], examples: Examples, status: Status)

test/shared/src/test/scala/hedgehog/GenTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ object GenTest extends Properties {
7070

7171
def testFromSomeSome: Result = {
7272
val r = Property.checkRandom(PropertyConfig.default, Gen.fromSome(Gen.constant(Result.success).option).forAll)
73-
r ==== Report(SuccessCount(100), DiscardCount(0), Coverage.empty, OK)
73+
r ==== Report(SuccessCount(100), DiscardCount(0), Coverage.empty, Examples.empty, OK)
7474
}
7575

7676
def testFromSomeNone: Result = {
7777
val r = Property.checkRandom(PropertyConfig.default, Gen.fromSome(Gen.constant(Option.empty[Result])).forAll)
78-
r ==== Report(SuccessCount(0), DiscardCount(100), Coverage.empty, GaveUp)
78+
r ==== Report(SuccessCount(0), DiscardCount(100), Coverage.empty, Examples.empty, GaveUp)
7979
}
8080

8181
def testApplicative: Result = {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package hedgehog
2+
3+
import hedgehog.core._
4+
import hedgehog.runner._
5+
6+
object LabelledExamplesTest extends Properties {
7+
8+
def tests: List[Test] =
9+
List(
10+
property("testLabelledExamples", testLabelledExamples)
11+
)
12+
13+
def prop: Property =
14+
for {
15+
_ <- Gen.int(Range.linear(0, 10)).list(Range.linear(0, 10)).forAll
16+
.classify("empty", _.isEmpty)
17+
.classify("nonempty", _.nonEmpty)
18+
} yield Result.success
19+
20+
def testLabelledExamples: Property = {
21+
for {
22+
examples <- Gen.generate { (size, seed) =>
23+
val config = PropertyConfig.default.copy(withExamples = WithExamples.WithExamples)
24+
val labelledExamples = Property.report(config, Some(size), seed, prop)
25+
Seed(seed.seed.next) -> labelledExamples.examples
26+
}.forAll
27+
} yield
28+
examples ==== Examples(Map(
29+
LabelName("empty") -> List(Info("List()"))
30+
, LabelName("nonempty") -> List(Info("List(0)"))
31+
))
32+
}
33+
}

test/shared/src/test/scala/hedgehog/PropertyTest.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ object PropertyTest extends Properties {
2323
y <- int(Range.linear(0, 50)).log("y")
2424
_ <- if (y % 2 == 0) Property.discard else Property.point(())
2525
} yield Result.assert(y < 87 && x <= 'r'), seed)
26-
r ==== Report(SuccessCount(2), DiscardCount(4), Coverage.empty, Failed(ShrinkCount(2), List(
26+
r ==== Report(SuccessCount(2), DiscardCount(4), Coverage.empty, Examples.empty, Failed(ShrinkCount(2), List(
2727
ForAll("x", "s")
2828
, ForAll("y", "1"))
2929
))
@@ -39,7 +39,7 @@ object PropertyTest extends Properties {
3939
(if (y % 2 == 0) Property.discard else Property.point(())).map(_ =>
4040
Result.assert(y < 87 && x <= 'r')
4141
)}, seed)
42-
r ==== Report(SuccessCount(2), DiscardCount(4), Coverage.empty, Failed(ShrinkCount(2), List(
42+
r ==== Report(SuccessCount(2), DiscardCount(4), Coverage.empty, Examples.empty, Failed(ShrinkCount(2), List(
4343
ForAll("x", "s")
4444
, ForAll("y", "1"))
4545
))
@@ -107,7 +107,7 @@ object PropertyTest extends Properties {
107107
y <- order(expensive).log("expensive")
108108
} yield Result.assert(merge(x, y).total.value == x.total.value + y.total.value)
109109
, seed)
110-
r ==== Report(SuccessCount(1), DiscardCount(0), Coverage.empty, Failed(ShrinkCount(4), List(
110+
r ==== Report(SuccessCount(1), DiscardCount(0), Coverage.empty, Examples.empty, Failed(ShrinkCount(4), List(
111111
ForAll("cheap", "Order(List())")
112112
, ForAll("expensive", "Order(List(Item(oculus,USD(1000))))"
113113
))))

0 commit comments

Comments
 (0)