-
Notifications
You must be signed in to change notification settings - Fork 624
/
builders.kt
401 lines (353 loc) · 16.4 KB
/
builders.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
package io.kotest.property.arbitrary
import io.kotest.property.Arb
import io.kotest.property.Classifier
import io.kotest.property.RandomSource
import io.kotest.property.Sample
import io.kotest.property.Shrinker
import io.kotest.property.sampleOf
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.RestrictsSuspension
import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
import kotlin.coroutines.resume
/**
* Creates a new [Arb] that performs no shrinking, has no edge cases and
* generates values from the given function.
*/
fun <A> arbitrary(fn: suspend ArbitraryBuilderContext.(RandomSource) -> A): Arb<A> =
arbitraryBuilder { rs -> fn(rs) }
/**
* Creates a new [Arb] that performs shrinking using the supplied [Shrinker], has no edge cases and
* generates values from the given function.
*/
fun <A> arbitrary(shrinker: Shrinker<A>, fn: suspend ArbitraryBuilderContext.(RandomSource) -> A): Arb<A> =
arbitraryBuilder(shrinker) { rs -> fn(rs) }
/**
* Creates a new [Arb] that classifies the generated values using the supplied [Classifier], has no edge cases and
* generates values from the given function.
*/
fun <A> arbitrary(classifier: Classifier<A>, fn: suspend ArbitraryBuilderContext.(RandomSource) -> A): Arb<A> =
arbitraryBuilder(null, classifier) { rs -> fn(rs) }
/**
* Creates a new [Arb] that performs shrinking using the supplied [Shrinker],
* classifies the generated values using the supplied [Classifier], has no edge cases and
* generates values from the given function.
*/
fun <A> arbitrary(
shrinker: Shrinker<A>,
classifier: Classifier<A>,
fn: suspend ArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> =
arbitraryBuilder(shrinker, classifier) { rs -> fn(rs) }
/**
* Creates a new [Arb] that performs no shrinking, uses the given edge cases and
* generates values from the given function.
*/
fun <A> arbitrary(edgecases: List<A>, fn: suspend ArbitraryBuilderContext.(RandomSource) -> A): Arb<A> =
object : Arb<A>() {
override fun edgecase(rs: RandomSource): A? = if (edgecases.isEmpty()) null else edgecases.random(rs.random)
override fun sample(rs: RandomSource): Sample<A> = delegate.sample(rs)
private val delegate = arbitraryBuilder { rs -> fn(rs) }
}
/**
* Creates a new [Arb] that performs shrinking using the supplied [Shrinker], uses the given edge cases and
* generates values from the given function.
*/
fun <A> arbitrary(
edgecases: List<A>,
shrinker: Shrinker<A>,
fn: suspend ArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> = object : Arb<A>() {
override fun edgecase(rs: RandomSource): A? = if (edgecases.isEmpty()) null else edgecases.random(rs.random)
override fun sample(rs: RandomSource): Sample<A> = delegate.sample(rs)
private val delegate = arbitraryBuilder(shrinker) { rs -> fn(rs) }
}
/**
* Creates a new [Arb] that generates edge cases from the given [edgecaseFn] function
* and generates samples from the given [sampleFn] function.
*/
fun <A> arbitrary(
edgecaseFn: (RandomSource) -> A?,
sampleFn: suspend ArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> =
object : Arb<A>() {
override fun edgecase(rs: RandomSource): A? = edgecaseFn(rs)
override fun sample(rs: RandomSource): Sample<A> = delegate.sample(rs)
private val delegate: Arb<A> = arbitraryBuilder { rs -> sampleFn(rs) }
}
/**
* Creates a new [Arb] that generates edge cases from the given [edgecaseFn] function,
* performs shrinking using the supplied [Shrinker], and generates samples from the given [sampleFn] function.
*/
fun <A> arbitrary(
edgecaseFn: (RandomSource) -> A?,
shrinker: Shrinker<A>,
sampleFn: suspend ArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> =
object : Arb<A>() {
override fun edgecase(rs: RandomSource): A? = edgecaseFn(rs)
override fun sample(rs: RandomSource): Sample<A> = delegate.sample(rs)
private val delegate: Arb<A> = arbitraryBuilder(shrinker) { rs -> sampleFn(rs) }
}
/**
* Creates a new [Arb] that performs no shrinking, has no edge cases and
* generates values from the given function.
*/
suspend inline fun <A> generateArbitrary(
crossinline fn: suspend GenerateArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> = suspendArbitraryBuilder { rs -> fn(rs) }
/**
* Creates a new [Arb] that performs shrinking using the supplied [Shrinker], has no edge cases and
* generates values from the given function.
*/
suspend inline fun <A> generateArbitrary(
shrinker: Shrinker<A>,
crossinline fn: suspend GenerateArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> = suspendArbitraryBuilder(shrinker, null) { rs -> fn(rs) }
/**
* Creates a new [Arb] that classifies the generated values using the supplied [Classifier], has no edge cases and
* generates values from the given function.
*/
suspend inline fun <A> generateArbitrary(
classifier: Classifier<A>,
crossinline fn: suspend GenerateArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> = suspendArbitraryBuilder(null, classifier) { rs -> fn(rs) }
/**
* Creates a new [Arb] that performs shrinking using the supplied [Shrinker],
* classifies the generated values using the supplied [Classifier], has no edge cases and
* generates values from the given function.
*/
suspend inline fun <A> generateArbitrary(
shrinker: Shrinker<A>,
classifier: Classifier<A>,
crossinline fn: suspend GenerateArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> = suspendArbitraryBuilder(shrinker, classifier) { rs -> fn(rs) }
/**
* Creates a new [Arb] that performs no shrinking, uses the given edge cases and
* generates values from the given function.
*/
suspend inline fun <A> generateArbitrary(
edgecases: List<A>,
crossinline fn: suspend GenerateArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> = suspendArbitraryBuilder(null, null,
if (edgecases.isEmpty()) null else { rs -> edgecases.random(rs.random) }
) { rs -> fn(rs) }
/**
* Creates a new [Arb] that performs shrinking using the supplied [Shrinker], uses the given edge cases and
* generates values from the given function.
*/
suspend inline fun <A> generateArbitrary(
edgecases: List<A>,
shrinker: Shrinker<A>,
crossinline fn: suspend GenerateArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> = suspendArbitraryBuilder(
shrinker,
null,
if (edgecases.isEmpty()) null else { rs -> edgecases.random(rs.random) }
) { rs -> fn(rs) }
/**
* Creates a new [Arb] that generates edge cases from the given [edgecaseFn] function
* and generates samples from the given [sampleFn] function.
*/
suspend inline fun <A> generateArbitrary(
crossinline edgecaseFn: (RandomSource) -> A?,
crossinline sampleFn: suspend GenerateArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> {
val delegate: Arb<A> = suspendArbitraryBuilder { rs -> sampleFn(rs) }
return object : Arb<A>() {
override fun edgecase(rs: RandomSource): A? = edgecaseFn(rs)
override fun sample(rs: RandomSource): Sample<A> = delegate.sample(rs)
}
}
/**
* Creates a new [Arb] that generates edge cases from the given [edgecaseFn] function,
* performs shrinking using the supplied [Shrinker], and generates samples from the given [sampleFn] function.
*/
suspend inline fun <A> generateArbitrary(
crossinline edgecaseFn: (RandomSource) -> A?,
shrinker: Shrinker<A>,
crossinline sampleFn: suspend GenerateArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> {
val delegate: Arb<A> = suspendArbitraryBuilder(shrinker) { rs -> sampleFn(rs) }
return object : Arb<A>() {
override fun edgecase(rs: RandomSource): A? = edgecaseFn(rs)
override fun sample(rs: RandomSource): Sample<A> = delegate.sample(rs)
}
}
/**
* Creates a new [Arb] using [Continuation] using a stateless [builderFn].
*
* This function accepts an optional [shrinker], [classifier], and [edgecaseFn]. These parameters
* will be passed to [ArbitraryBuilder].
*/
fun <A> arbitraryBuilder(
shrinker: Shrinker<A>? = null,
classifier: Classifier<A>? = null,
edgecaseFn: EdgecaseFn<A>? = null,
builderFn: suspend ArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> = object : Arb<A>() {
override fun edgecase(rs: RandomSource): A? = singleShotArb(SingleShotGenerationMode.Edgecase, rs).edgecase(rs)
override fun sample(rs: RandomSource): Sample<A> = singleShotArb(SingleShotGenerationMode.Sample, rs).sample(rs)
override val classifier: Classifier<out A>? = classifier
/**
* This function generates a new instance of a single shot arb.
* DO NOT CACHE THE [Arb] returned by this function.
*
* This needs to be a function because at time of writing, Kotlin 1.5's [Continuation] is single shot.
* With arbs, we ideally need multishot. To rerun [builderFn], we need to "reset" the continuation.
*
* The current way we do it is to recreate a fresh [SingleShotArbContinuation] instance that
* will provide another single shot Arb. Hence the reason why this function is invoked
* on every call to [sample] / [edgecase].
*/
private fun singleShotArb(mode: SingleShotGenerationMode, rs: RandomSource): Arb<A> {
val restrictedContinuation = SingleShotArbContinuation.Restricted(mode, rs) {
/**
* At the end of the suspension we got a generated value [A] as a comprehension result.
* This value can either be a sample, or an edgecase.
*/
val value: A = builderFn(rs)
/**
* Here we point A into an Arb<A> with the appropriate enrichments including
* [Shrinker], [Classifier], and [EdgecaseFn]. When edgecase returns null, we pass the generated value
* to the edgecase function so to make sure we retain all arbs' edgecases inside the comprehension.
*/
ArbitraryBuilder({ value }, classifier, shrinker, { rs -> edgecaseFn?.invoke(rs) ?: value }).build()
}
return with(restrictedContinuation) {
this@with.createSingleShotArb()
}
}
}
/**
* Creates a new suspendable [Arb] using [Continuation] using a stateless [fn].
*
* This function accepts an optional [shrinker], [classifier], and [edgecaseFn]. These parameters
* will be passed to [ArbitraryBuilder].
*/
suspend fun <A> suspendArbitraryBuilder(
shrinker: Shrinker<A>? = null,
classifier: Classifier<A>? = null,
edgecaseFn: EdgecaseFn<A>? = null,
fn: suspend GenerateArbitraryBuilderContext.(RandomSource) -> A
): Arb<A> = suspendCoroutineUninterceptedOrReturn { cont ->
val arb = object : Arb<A>() {
override fun edgecase(rs: RandomSource): A? = singleShotArb(SingleShotGenerationMode.Edgecase, rs).edgecase(rs)
override fun sample(rs: RandomSource): Sample<A> = singleShotArb(SingleShotGenerationMode.Sample, rs).sample(rs)
override val classifier: Classifier<out A>? = classifier
/**
* This function generates a new instance of a single shot arb.
* DO NOT CACHE THE [Arb] returned by this function.
*
* This needs to be a function because at time of writing, Kotlin 1.5's [Continuation] is single shot.
* With arbs, we ideally need multishot. To rerun [fn], we need to "reset" the continuation.
*
* The current way we do it is to recreate a fresh [SingleShotArbContinuation] instance that
* will provide another single shot Arb. Hence the reason why this function is invoked
* on every call to [sample] / [edgecase].
*/
private fun singleShotArb(genMode: SingleShotGenerationMode, rs: RandomSource): Arb<A> {
val suspendableContinuation = SingleShotArbContinuation.Suspendedable(genMode, rs, cont.context) {
/**
* At the end of the suspension we got a generated value [A] as a comprehension result.
* This value can either be a sample, or an edgecase.
*/
val value: A = fn(rs)
/**
* Here we point A into an Arb<A> with the appropriate enrichments including
* [Shrinker], [Classifier], and [EdgecaseFn]. When edgecase returns null, we pass the generated value
* to the edgecase function so to make sure we retain all arbs' edgecases inside the comprehension.
*/
ArbitraryBuilder({ value }, classifier, shrinker, { rs -> edgecaseFn?.invoke(rs) ?: value }).build()
}
return with(suspendableContinuation) {
this@with.createSingleShotArb()
}
}
}
cont.resume(arb)
}
typealias SampleFn<A> = (RandomSource) -> A
typealias EdgecaseFn<A> = (RandomSource) -> A?
class ArbitraryBuilder<A>(
private val sampleFn: SampleFn<A>,
private val classifier: Classifier<A>?,
private val shrinker: Shrinker<A>?,
private val edgecaseFn: EdgecaseFn<A>?,
) {
companion object {
fun <A> create(f: (RandomSource) -> A): ArbitraryBuilder<A> = ArbitraryBuilder(f, null, null, null)
}
fun withClassifier(classifier: Classifier<A>) = ArbitraryBuilder(sampleFn, classifier, shrinker, edgecaseFn)
fun withShrinker(shrinker: Shrinker<A>) = ArbitraryBuilder(sampleFn, classifier, shrinker, edgecaseFn)
fun withEdgecaseFn(edgecaseFn: EdgecaseFn<A>) = ArbitraryBuilder(sampleFn, classifier, shrinker, edgecaseFn)
fun withEdgecases(edgecases: List<A>) = ArbitraryBuilder(sampleFn, classifier, shrinker) {
if (edgecases.isEmpty()) null else edgecases.random(it.random)
}
fun build() = object : Arb<A>() {
override val classifier: Classifier<out A>? = this@ArbitraryBuilder.classifier
override fun edgecase(rs: RandomSource): A? = edgecaseFn?.invoke(rs)
override fun sample(rs: RandomSource): Sample<A> {
val sample = sampleFn(rs)
return if (shrinker == null) Sample(sample) else sampleOf(sample, shrinker)
}
}
}
interface BaseArbitraryBuilderSyntax {
/**
* [bind] returns the generated value of an arb. This can either be a sample or an edgecase.
*/
suspend fun <T> Arb<T>.bind(): T
}
@RestrictsSuspension
interface ArbitraryBuilderContext : BaseArbitraryBuilderSyntax
interface GenerateArbitraryBuilderContext : BaseArbitraryBuilderSyntax
enum class SingleShotGenerationMode { Edgecase, Sample }
sealed class SingleShotArbContinuation<F : BaseArbitraryBuilderSyntax, A>(
override val context: CoroutineContext,
private val generationMode: SingleShotGenerationMode,
private val randomSource: RandomSource,
private val fn: suspend F.() -> Arb<A>
) : Continuation<Arb<A>>, BaseArbitraryBuilderSyntax {
class Restricted<A>(
genMode: SingleShotGenerationMode,
rs: RandomSource,
fn: suspend ArbitraryBuilderContext.() -> Arb<A>
) : SingleShotArbContinuation<ArbitraryBuilderContext, A>(EmptyCoroutineContext, genMode, rs, fn),
ArbitraryBuilderContext
class Suspendedable<A>(
genMode: SingleShotGenerationMode,
rs: RandomSource,
override val context: CoroutineContext,
fn: suspend GenerateArbitraryBuilderContext.() -> Arb<A>
) : SingleShotArbContinuation<GenerateArbitraryBuilderContext, A>(context, genMode, rs, fn),
GenerateArbitraryBuilderContext
private lateinit var returnedArb: Arb<A>
private var hasExecuted: Boolean = false
override fun resumeWith(result: Result<Arb<A>>) {
hasExecuted = true
result.map { resultArb -> returnedArb = resultArb }.getOrThrow()
}
override suspend fun <T> Arb<T>.bind(): T = when (generationMode) {
SingleShotGenerationMode.Edgecase -> this.edgecase(randomSource) ?: this.sample(randomSource).value
SingleShotGenerationMode.Sample -> this.sample(randomSource).value
}
/**
* It's important to understand that at the time of writing (Kotlin 1.5) [Continuation] is single shot,
* i.e. it can only be resumed once. When it's possible to create multishot continuations in the future, we
* might be able to simplify this further.
*
* The aforementioned limitation means the [Arb] that we construct through this mechanism can only be used
* to generate exactly one value. Hence, to recycle and rerun the specified composed transformation,
* we need to recreate the [SingleShotArbContinuation] instance and call [createSingleShotArb] again.
*/
fun F.createSingleShotArb(): Arb<A> {
require(!hasExecuted) { "continuation has already been executed, if you see this error please raise a bug report" }
val result = fn.startCoroutineUninterceptedOrReturn(this@createSingleShotArb, this@SingleShotArbContinuation)
@Suppress("UNCHECKED_CAST")
returnedArb = result as Arb<A>
return returnedArb
}
}