prop_check 0.14.0 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +52 -14
- data/lib/prop_check/generator.rb +23 -7
- data/lib/prop_check/generators.rb +275 -28
- data/lib/prop_check/helper.rb +3 -6
- data/lib/prop_check/property/configuration.rb +29 -9
- data/lib/prop_check/property.rb +27 -18
- data/lib/prop_check/version.rb +1 -1
- data/prop_check.gemspec +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6d76933200bf114634aaa2d53f4c0832d6c362a2051e12d760d59a8ccae14e7
|
4
|
+
data.tar.gz: 452bcebdfb1b5c0113519840ce22aec73be64a5ab60c20dacd3a537eb860bb8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa86451cf41305a0c9cb511b39b210768365becd0494f83e285f8cb2b7b7abf667df87ed033fb1faf730037edc116747b75f9d0ab8efce834de6e3cf0b499d6a
|
7
|
+
data.tar.gz: 215c2f13f9523f93b550de01f48087385a6ae8892357248691e7556d80cea9b3fc06e7d1dd113a4d088fa4225c99247b8309614dab6ef8198c7dc96e635d7aba
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
- 0.15.0
|
2
|
+
- Features:
|
3
|
+
- Generators for `Date`, `Time` and `DateTime`.
|
4
|
+
- Basic work done by @Haniyya. Thank you very much!
|
5
|
+
- Extra functions to generate dates/times/datetimes in the future or the past.
|
6
|
+
- Allow overriding the epoch that is used.
|
7
|
+
- A new option in `PropCheck::Property::Configuration` to set the default epoch.
|
8
|
+
- Generator to generate `Set`s.
|
9
|
+
- New builtin float generators (positive, negative, nonzero, nonnegative, nonpositive). Both in 'normal' flavor and in 'real' flavor (that will never generate infinity or other special values).
|
10
|
+
- `PropCheck::Generator#with_config` which enables the possibility to inspect and act on the current `PropCheck::Property::Configuration` while generating values.
|
11
|
+
- Fixes:
|
12
|
+
- Preserve backwards compatibility with Ruby 2.5 by not using infinite ranges internally (c.f. #8, thank you, @hlaf!)
|
13
|
+
- Make a flaky test deterministic by fixing the RNG. (c.f. #9, thank you, @hlaf!)
|
14
|
+
- Fix a crash when using a hash where not all keys are symbols. (c.f. #7, thank you, @Haniyya!)
|
15
|
+
- Fix situations in which `PropCheck::Generators.array` would for certain config values never generate empty arrays.
|
16
|
+
- 0.14.1 - Swap `awesome_print` for `amazing_print` which is a fork of the former that is actively maintained.
|
1
17
|
- 0.14.0 - Adds `uniq: true` option to `Generators.array`. Makes `PropCheck::Property` an immutable object that returns copies that have changes whenever reconfiguring, allowing re-usable configuration.
|
2
18
|
- 0.13.0 - Adds Generator#resize
|
3
19
|
- 0.12.1 - Fixes shrinking when filtering bug.
|
data/README.md
CHANGED
@@ -9,16 +9,47 @@ PropCheck allows you to do Property Testing in Ruby.
|
|
9
9
|
|
10
10
|
It features:
|
11
11
|
|
12
|
-
- Generators for common datatypes.
|
13
|
-
- An easy DSL to define your own generators (by combining existing ones,
|
12
|
+
- Generators for most common Ruby datatypes.
|
13
|
+
- An easy DSL to define your own generators (by combining existing ones, as well as completely custom ones).
|
14
14
|
- Shrinking to a minimal counter-example on failure.
|
15
|
+
- Hooks to perform extra set-up/cleanup logic before/after every example case.
|
15
16
|
|
17
|
+
## What is PropCheck?
|
16
18
|
|
17
|
-
|
19
|
+
PropCheck is a Ruby library to create unit tests which are simpler to write and more powerful when run, finding edge-cases in your code you wouldn't have thought to look for.
|
18
20
|
|
19
|
-
|
21
|
+
It works by letting you write tests that assert that something should be true for _every_ case, rather than just the ones you happen to think of.
|
20
22
|
|
21
|
-
|
23
|
+
|
24
|
+
A normal unit test looks something like the following:
|
25
|
+
|
26
|
+
1. Set up some data.
|
27
|
+
2. Perform some operations on the data.
|
28
|
+
3. Assert something about the result
|
29
|
+
|
30
|
+
PropCheck lets you write tests which instead look like this:
|
31
|
+
|
32
|
+
1. For all data matching some specification.
|
33
|
+
2. Perform some operations on the data.
|
34
|
+
3. Assert something about the result.
|
35
|
+
|
36
|
+
This is often called property-based testing. It was popularised by the Haskell library [QuickCheck](https://hackage.haskell.org/package/QuickCheck).
|
37
|
+
PropCheck takes further inspiration from Erlang's [PropEr](https://hex.pm/packages/proper), Elixir's [StreamData](https://hex.pm/packages/stream_data) and Python's [Hypothesis](https://hypothesis.works/).
|
38
|
+
|
39
|
+
It works by generating arbitrary data matching your specification and checking that your assertions still hold in that case. If it finds an example where they do not, it takes that example and shrinks it down, simplifying it to find the smallest example that still causes the problem.
|
40
|
+
|
41
|
+
Writing these kinds of tests usually consists of deciding on guarantees that your code should have -- properties that should always hold true, regardless of wat the world throws at you. Some examples are:
|
42
|
+
|
43
|
+
- Your code should not throw an exception, or only a particular type of exception.
|
44
|
+
- If you remove an object, you can no longer see it
|
45
|
+
- If you serialize and then deserializea value, you get the same value back.
|
46
|
+
|
47
|
+
|
48
|
+
## Implemented and still missing features
|
49
|
+
|
50
|
+
Before releasing v1.0, we want to finish the following:
|
51
|
+
|
52
|
+
- [x] Finalize the testing DSL.
|
22
53
|
- [x] Testing the library itself (against known 'true' axiomatically correct Ruby code.)
|
23
54
|
- [x] Customization of common settings
|
24
55
|
- [x] Filtering generators.
|
@@ -28,15 +59,17 @@ Before releasing this gem on Rubygems, the following things need to be finished:
|
|
28
59
|
- [x] Good, unicode-compliant, string generators.
|
29
60
|
- [x] Filtering generator outputs.
|
30
61
|
- [x] Before/after/around hooks to add setup/teardown logic to be called before/after/around each time a check is run with new data.
|
62
|
+
- [x] Possibility to resize generators.
|
31
63
|
- [x] `#instance` generator to allow the easy creation of generators for custom datatypes.
|
32
64
|
- [ ] A usage guide.
|
65
|
+
- [ ] A simple way to create recursive generators
|
66
|
+
- [x] Builtin generation of `Set`s
|
67
|
+
- [x] Builtin generation of `Date`s, `Time`s and `DateTime`s.
|
68
|
+
- [ ] Configuration option to resize all generators given to a particular Property instance.
|
33
69
|
|
34
|
-
|
70
|
+
## Nice-to-haves
|
35
71
|
|
36
|
-
-
|
37
|
-
- [ ] `aggregate` , `resize` and similar generator-modifying calls (c.f. PropEr's variants of these) which will help with introspection/metrics.
|
38
|
-
- [ ] Integration with other Ruby test frameworks.
|
39
|
-
- Stateful property testing. If implemented at some point, will probably happen in a separate add-on library.
|
72
|
+
- Stateful property testing. If implemented at some point, will probably happen in a separate add-on library.
|
40
73
|
|
41
74
|
|
42
75
|
## Installation
|
@@ -61,10 +94,8 @@ Or install it yourself as:
|
|
61
94
|
### Using PropCheck for basic testing
|
62
95
|
|
63
96
|
Propcheck exposes the `forall` method.
|
64
|
-
It takes generators as keyword arguments
|
65
|
-
|
66
|
-
|
67
|
-
_(to be precise: a method on the execution context is defined which returns the current generated value for that name)_
|
97
|
+
It takes any number of generators as arguments (or keyword arguments), as well as a block to run.
|
98
|
+
The value(s) generated from the generator(s) passed to the `forall` will be given to the block as arguments.
|
68
99
|
|
69
100
|
Raise an exception from the block if there is a problem. If there is no problem, just return normally.
|
70
101
|
|
@@ -257,3 +288,10 @@ Everyone interacting in the PropCheck project’s codebases, issue trackers, cha
|
|
257
288
|
I want to thank the original creators of QuickCheck (Koen Claessen, John Hughes) as well as the authors of many great property testing libraries that I was/am able to use as inspiration.
|
258
289
|
I also want to greatly thank Thomasz Kowal who made me excited about property based testing [with his great talk about stateful property testing](https://www.youtube.com/watch?v=q0wZzFUYCuM),
|
259
290
|
as well as Fred Herbert for his great book [Property-Based Testing with PropEr, Erlang and Elixir](https://propertesting.com/) which is really worth the read (regardless of what language you are using).
|
291
|
+
|
292
|
+
The implementation and API of PropCheck takes a lot of inspiration from the following pre-existing libraries:
|
293
|
+
|
294
|
+
- Haskell's [QuickCheck](https://hackage.haskell.org/package/QuickCheck) and [Hedgehog](https://hackage.haskell.org/package/hedgehog);
|
295
|
+
- Erlang's [PropEr](https://hex.pm/packages/proper);
|
296
|
+
- Elixir's [StreamData](https://hex.pm/packages/stream_data);
|
297
|
+
- Python's [Hypothesis](https://hypothesis.works/).
|
data/lib/prop_check/generator.rb
CHANGED
@@ -11,7 +11,8 @@ module PropCheck
|
|
11
11
|
@@default_size = 10
|
12
12
|
@@default_rng = Random.new
|
13
13
|
@@max_consecutive_attempts = 100
|
14
|
-
@@default_kwargs = {size: @@default_size, rng: @@default_rng,
|
14
|
+
@@default_kwargs = { size: @@default_size, rng: @@default_rng,
|
15
|
+
max_consecutive_attempts: @@max_consecutive_attempts }
|
15
16
|
|
16
17
|
##
|
17
18
|
# Being a special kind of Proc, a Generator wraps a block.
|
@@ -33,11 +34,11 @@ module PropCheck
|
|
33
34
|
return res
|
34
35
|
end
|
35
36
|
|
36
|
-
raise Errors::GeneratorExhaustedError, "
|
37
|
+
raise Errors::GeneratorExhaustedError, ''"
|
37
38
|
Exhausted #{max_consecutive_attempts} consecutive generation attempts.
|
38
39
|
|
39
40
|
Probably too few generator results were adhering to a `where` condition.
|
40
|
-
"
|
41
|
+
"''
|
41
42
|
end
|
42
43
|
|
43
44
|
##
|
@@ -88,7 +89,7 @@ module PropCheck
|
|
88
89
|
# end.flatten
|
89
90
|
# end
|
90
91
|
Generator.new do |**kwargs|
|
91
|
-
outer_result =
|
92
|
+
outer_result = generate(**kwargs)
|
92
93
|
outer_result.bind do |outer_val|
|
93
94
|
inner_generator = generator_proc.call(outer_val)
|
94
95
|
inner_generator.generate(**kwargs)
|
@@ -103,15 +104,30 @@ module PropCheck
|
|
103
104
|
# => "S"
|
104
105
|
def map(&proc)
|
105
106
|
Generator.new do |**kwargs|
|
106
|
-
result =
|
107
|
+
result = generate(**kwargs)
|
107
108
|
result.map(&proc)
|
108
109
|
end
|
109
110
|
end
|
110
111
|
|
112
|
+
##
|
113
|
+
# Turns a generator returning `x` into a generator returning `[x, config]`
|
114
|
+
# where `config` is the current `PropCheck::Property::Configuration`.
|
115
|
+
# This can be used to inspect the configuration inside a `#map` or `#where`
|
116
|
+
# and act on it.
|
117
|
+
#
|
118
|
+
# >> Generators.choose(0..100).with_config.map { |int, conf| Date.jd(conf[:default_epoch].jd + int) }.call(size: 10, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
119
|
+
# => Date.new(2023, 01, 10)
|
120
|
+
def with_config
|
121
|
+
Generator.new do |**kwargs|
|
122
|
+
result = generate(**kwargs)
|
123
|
+
result.map { |val| [val, kwargs[:config]] }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
111
127
|
##
|
112
128
|
# Creates a new Generator that only produces a value when the block `condition` returns a truthy value.
|
113
129
|
def where(&condition)
|
114
|
-
|
130
|
+
map do |result|
|
115
131
|
# if condition.call(*result)
|
116
132
|
if PropCheck::Helper.call_splatted(result, &condition)
|
117
133
|
result
|
@@ -131,7 +147,7 @@ module PropCheck
|
|
131
147
|
def resize(&proc)
|
132
148
|
Generator.new do |size:, **other_kwargs|
|
133
149
|
new_size = proc.call(size)
|
134
|
-
|
150
|
+
generate(**other_kwargs, size: new_size)
|
135
151
|
end
|
136
152
|
end
|
137
153
|
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
# coding: utf-8
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
3
|
+
require 'date'
|
4
4
|
require 'prop_check/generator'
|
5
5
|
require 'prop_check/lazy_tree'
|
6
6
|
module PropCheck
|
@@ -136,7 +136,8 @@ module PropCheck
|
|
136
136
|
# that is: no infinity, no NaN,
|
137
137
|
# no numbers testing the limits of floating-point arithmetic.
|
138
138
|
#
|
139
|
-
# Shrinks
|
139
|
+
# Shrinks towards zero.
|
140
|
+
# The shrinking strategy also moves towards 'simpler' floats (like `1.0`) from 'complicated' floats (like `3.76543`).
|
140
141
|
#
|
141
142
|
# >> Generators.real_float().sample(10, size: 10, rng: Random.new(42))
|
142
143
|
# => [-2.2, -0.2727272727272727, 4.0, 1.25, -3.7272727272727275, -8.833333333333334, -8.090909090909092, 1.1428571428571428, 0.0, 8.0]
|
@@ -146,20 +147,130 @@ module PropCheck
|
|
146
147
|
end
|
147
148
|
end
|
148
149
|
|
149
|
-
|
150
|
+
##
|
151
|
+
# Generates any real floating-point numbers,
|
152
|
+
# but will never generate zero.
|
153
|
+
# c.f. #real_float
|
154
|
+
#
|
155
|
+
# >> Generators.real_nonzero_float().sample(10, size: 10, rng: Random.new(43))
|
156
|
+
# => [-7.25, 7.125, -7.636363636363637, -3.0, -8.444444444444445, -6.857142857142857, 2.4545454545454546, 3.0, -7.454545454545455, -6.25]
|
157
|
+
def real_nonzero_float
|
158
|
+
real_float.where { |val| val != 0.0 }
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Generates real floating-point numbers which are never negative.
|
163
|
+
# Shrinks towards 0
|
164
|
+
# c.f. #real_float
|
165
|
+
#
|
166
|
+
# >> Generators.real_nonnegative_float().sample(10, size: 10, rng: Random.new(43))
|
167
|
+
# => [7.25, 7.125, 7.636363636363637, 3.0, 8.444444444444445, 0.0, 6.857142857142857, 2.4545454545454546, 3.0, 7.454545454545455]
|
168
|
+
def real_nonnegative_float
|
169
|
+
real_float.map(&:abs)
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Generates real floating-point numbers which are never positive.
|
174
|
+
# Shrinks towards 0
|
175
|
+
# c.f. #real_float
|
176
|
+
#
|
177
|
+
# >> Generators.real_nonpositive_float().sample(10, size: 10, rng: Random.new(44))
|
178
|
+
# => [-9.125, -2.3636363636363638, -8.833333333333334, -1.75, -8.4, -2.4, -3.5714285714285716, -1.0, -6.111111111111111, -4.0]
|
179
|
+
def real_nonpositive_float
|
180
|
+
real_nonnegative_float.map(&:-@)
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Generates real floating-point numbers which are always positive
|
185
|
+
# Shrinks towards Float::MIN
|
186
|
+
#
|
187
|
+
# Does not consider denormals.
|
188
|
+
# c.f. #real_float
|
189
|
+
#
|
190
|
+
# >> Generators.real_positive_float().sample(10, size: 10, rng: Random.new(42))
|
191
|
+
# => [2.2, 0.2727272727272727, 4.0, 1.25, 3.7272727272727275, 8.833333333333334, 8.090909090909092, 1.1428571428571428, 2.2250738585072014e-308, 8.0]
|
192
|
+
def real_positive_float
|
193
|
+
real_nonnegative_float.map { |val| val + Float::MIN }
|
194
|
+
end
|
195
|
+
|
196
|
+
##
|
197
|
+
# Generates real floating-point numbers which are always negative
|
198
|
+
# Shrinks towards -Float::MIN
|
199
|
+
#
|
200
|
+
# Does not consider denormals.
|
201
|
+
# c.f. #real_float
|
202
|
+
#
|
203
|
+
# >> Generators.real_negative_float().sample(10, size: 10, rng: Random.new(42))
|
204
|
+
# => [-2.2, -0.2727272727272727, -4.0, -1.25, -3.7272727272727275, -8.833333333333334, -8.090909090909092, -1.1428571428571428, -2.2250738585072014e-308, -8.0]
|
205
|
+
def real_negative_float
|
206
|
+
real_positive_float.map(&:-@)
|
207
|
+
end
|
208
|
+
|
209
|
+
@@special_floats = [Float::NAN,
|
210
|
+
Float::INFINITY,
|
211
|
+
-Float::INFINITY,
|
212
|
+
Float::MAX,
|
213
|
+
-Float::MAX,
|
214
|
+
Float::MIN,
|
215
|
+
-Float::MIN,
|
216
|
+
Float::EPSILON,
|
217
|
+
-Float::EPSILON,
|
218
|
+
0.0.next_float,
|
219
|
+
0.0.prev_float]
|
150
220
|
##
|
151
221
|
# Generates floating-point numbers
|
152
222
|
# Will generate NaN, Infinity, -Infinity,
|
153
223
|
# as well as Float::EPSILON, Float::MAX, Float::MIN,
|
154
224
|
# 0.0.next_float, 0.0.prev_float,
|
155
225
|
# to test the handling of floating-point edge cases.
|
156
|
-
# Approx. 1/
|
226
|
+
# Approx. 1/50 generated numbers is a special one.
|
157
227
|
#
|
158
228
|
# Shrinks to smaller, real floats.
|
159
229
|
# >> Generators.float().sample(10, size: 10, rng: Random.new(42))
|
160
|
-
#
|
230
|
+
# >> Generators.float().sample(10, size: 10, rng: Random.new(4))
|
231
|
+
# => [-8.0, 2.0, 2.7142857142857144, -4.0, -10.2, -6.666666666666667, -Float::INFINITY, -10.2, 2.1818181818181817, -6.2]
|
161
232
|
def float
|
162
|
-
frequency(
|
233
|
+
frequency(49 => real_float, 1 => one_of(*@@special_floats.map(&method(:constant))))
|
234
|
+
end
|
235
|
+
|
236
|
+
##
|
237
|
+
# Generates any nonzerno floating-point number.
|
238
|
+
# Will generate special floats (except NaN) from time to time.
|
239
|
+
# c.f. #float
|
240
|
+
def nonzero_float
|
241
|
+
float.where { |val| val != 0.0 && val }
|
242
|
+
end
|
243
|
+
|
244
|
+
##
|
245
|
+
# Generates nonnegative floating point numbers
|
246
|
+
# Will generate special floats (except NaN) from time to time.
|
247
|
+
# c.f. #float
|
248
|
+
def nonnegative_float
|
249
|
+
float.map(&:abs).where { |val| val != Float::NAN }
|
250
|
+
end
|
251
|
+
|
252
|
+
##
|
253
|
+
# Generates nonpositive floating point numbers
|
254
|
+
# Will generate special floats (except NaN) from time to time.
|
255
|
+
# c.f. #float
|
256
|
+
def nonpositive_float
|
257
|
+
nonnegative_float.map(&:-@)
|
258
|
+
end
|
259
|
+
|
260
|
+
##
|
261
|
+
# Generates positive floating point numbers
|
262
|
+
# Will generate special floats (except NaN) from time to time.
|
263
|
+
# c.f. #float
|
264
|
+
def positive_float
|
265
|
+
nonnegative_float.where { |val| val != 0.0 && val }
|
266
|
+
end
|
267
|
+
|
268
|
+
##
|
269
|
+
# Generates positive floating point numbers
|
270
|
+
# Will generate special floats (except NaN) from time to time.
|
271
|
+
# c.f. #float
|
272
|
+
def negative_float
|
273
|
+
positive_float.map(&:-@).where { |val| val != 0.0 }
|
163
274
|
end
|
164
275
|
|
165
276
|
##
|
@@ -269,7 +380,7 @@ module PropCheck
|
|
269
380
|
if max.nil?
|
270
381
|
nonnegative_integer.bind { |count| make_array(element_generator, min, count, uniq) }
|
271
382
|
else
|
272
|
-
make_array(element_generator, min,
|
383
|
+
choose(min..max).bind { |count| make_array(element_generator, min, count, uniq) }
|
273
384
|
end
|
274
385
|
end
|
275
386
|
|
@@ -298,31 +409,57 @@ module PropCheck
|
|
298
409
|
arr = []
|
299
410
|
uniques = Set.new
|
300
411
|
count = 0
|
301
|
-
(0..).lazy.map do
|
302
|
-
elem = element_generator.clone.generate(**kwargs)
|
303
|
-
if uniques.add?(uniq_fun.call(elem.root))
|
304
|
-
arr.push(elem)
|
305
|
-
count = 0
|
306
|
-
else
|
307
|
-
count += 1
|
308
|
-
end
|
309
412
|
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
413
|
+
if amount == 0
|
414
|
+
LazyTree.new([])
|
415
|
+
else
|
416
|
+
0.step.lazy.map do
|
417
|
+
elem = element_generator.clone.generate(**kwargs)
|
418
|
+
if uniques.add?(uniq_fun.call(elem.root))
|
419
|
+
arr.push(elem)
|
420
|
+
count = 0
|
314
421
|
else
|
315
|
-
|
422
|
+
count += 1
|
423
|
+
end
|
424
|
+
|
425
|
+
if count > kwargs[:max_consecutive_attempts]
|
426
|
+
if arr.size >= min
|
427
|
+
# Give up and return shorter array in this case
|
428
|
+
amount = min
|
429
|
+
else
|
430
|
+
raise Errors::GeneratorExhaustedError, "Too many consecutive elements filtered by 'uniq:'."
|
431
|
+
end
|
316
432
|
end
|
317
433
|
end
|
318
|
-
|
319
|
-
|
320
|
-
.force
|
434
|
+
.take_while { arr.size < amount }
|
435
|
+
.force
|
321
436
|
|
322
|
-
|
437
|
+
LazyTree.zip(arr).map { |array| array.uniq(&uniq_fun) }
|
438
|
+
end
|
323
439
|
end
|
324
440
|
end
|
325
441
|
|
442
|
+
##
|
443
|
+
# Generates a set of elements, where each of the elements
|
444
|
+
# is generated by `element_generator`.
|
445
|
+
#
|
446
|
+
# Shrinks to smaller sets (with shrunken elements).
|
447
|
+
# Accepted keyword arguments:
|
448
|
+
#
|
449
|
+
# `empty:` When false, behaves the same as `min: 1`
|
450
|
+
# `min:` Ensures at least this many elements are generated. (default: 0)
|
451
|
+
# `max:` Ensures at most this many elements are generated. When nil, an arbitrary count is used instead. (default: nil)
|
452
|
+
#
|
453
|
+
# In the set, elements are always unique.
|
454
|
+
# If it is not possible to generate another unique value after the configured `max_consecutive_attempts`
|
455
|
+
# a `PropCheck::Errors::GeneratorExhaustedError` will be raised.
|
456
|
+
#
|
457
|
+
# >> Generators.set(Generators.positive_integer).sample(5, size: 4, rng: Random.new(42))
|
458
|
+
# => [Set[2, 4], Set[], Set[3, 4], Set[], Set[4]]
|
459
|
+
def set(element_generator, min: 0, max: nil, empty: true)
|
460
|
+
array(element_generator, min: min, max: max, empty: empty, uniq: true).map(&:to_set)
|
461
|
+
end
|
462
|
+
|
326
463
|
##
|
327
464
|
# Generates a hash of key->values,
|
328
465
|
# where each of the keys is made using the `key_generator`
|
@@ -559,8 +696,8 @@ module PropCheck
|
|
559
696
|
#
|
560
697
|
# Shrinks towards simpler terms, like `true`, an empty array, a single character or an integer.
|
561
698
|
#
|
562
|
-
# >> Generators.truthy.sample(5, size:
|
563
|
-
# => [[
|
699
|
+
# >> Generators.truthy.sample(5, size: 2, rng: Random.new(42))
|
700
|
+
# => [[2], {:gz=>0, :""=>0}, [1.0, 0.5], 0.6666666666666667, {"𦐺\u{9FDDB}"=>1, ""=>1}]
|
564
701
|
def truthy
|
565
702
|
one_of(constant(true),
|
566
703
|
constant([]),
|
@@ -574,8 +711,7 @@ module PropCheck
|
|
574
711
|
array(string),
|
575
712
|
hash(simple_symbol, integer),
|
576
713
|
hash(string, integer),
|
577
|
-
hash(string, string)
|
578
|
-
)
|
714
|
+
hash(string, string))
|
579
715
|
end
|
580
716
|
|
581
717
|
##
|
@@ -588,6 +724,117 @@ module PropCheck
|
|
588
724
|
frequency(9 => other_generator, 1 => constant(nil))
|
589
725
|
end
|
590
726
|
|
727
|
+
##
|
728
|
+
# Generates `Date` objects.
|
729
|
+
# DateTimes start around the given `epoch:` and deviate more when `size` increases.
|
730
|
+
# when no epoch is set, `PropCheck::Property::Configuration.default_epoch` is used, which defaults to `DateTime.now.to_date`.
|
731
|
+
#
|
732
|
+
# >> Generators.date(epoch: Date.new(2022, 01, 01)).sample(2, rng: Random.new(42))
|
733
|
+
# => [Date.new(2021, 12, 28), Date.new(2022, 01, 10)]
|
734
|
+
def date(epoch: nil)
|
735
|
+
date_from_offset(integer, epoch: epoch)
|
736
|
+
end
|
737
|
+
|
738
|
+
##
|
739
|
+
# variant of #date that only generates dates in the future (relative to `:epoch`).
|
740
|
+
#
|
741
|
+
# >> Generators.future_date(epoch: Date.new(2022, 01, 01)).sample(2, rng: Random.new(42))
|
742
|
+
# => [Date.new(2022, 01, 06), Date.new(2022, 01, 11)]
|
743
|
+
def future_date(epoch: Date.today)
|
744
|
+
date_from_offset(positive_integer, epoch: epoch)
|
745
|
+
end
|
746
|
+
|
747
|
+
##
|
748
|
+
# variant of #date that only generates dates in the past (relative to `:epoch`).
|
749
|
+
#
|
750
|
+
# >> Generators.past_date(epoch: Date.new(2022, 01, 01)).sample(2, rng: Random.new(42))
|
751
|
+
# => [Date.new(2021, 12, 27), Date.new(2021, 12, 22)]
|
752
|
+
def past_date(epoch: Date.today)
|
753
|
+
date_from_offset(negative_integer, epoch: epoch)
|
754
|
+
end
|
755
|
+
|
756
|
+
private def date_from_offset(offset_gen, epoch:)
|
757
|
+
if epoch
|
758
|
+
offset_gen.map { |offset| Date.jd(epoch.jd + offset) }
|
759
|
+
else
|
760
|
+
offset_gen.with_config.map do |offset, config|
|
761
|
+
puts config.inspect
|
762
|
+
epoch = config.default_epoch.to_date
|
763
|
+
Date.jd(epoch.jd + offset)
|
764
|
+
end
|
765
|
+
end
|
766
|
+
end
|
767
|
+
|
768
|
+
##
|
769
|
+
# Generates `DateTime` objects.
|
770
|
+
# DateTimes start around the given `epoch:` and deviate more when `size` increases.
|
771
|
+
# when no epoch is set, `PropCheck::Property::Configuration.default_epoch` is used, which defaults to `DateTime.now`.
|
772
|
+
#
|
773
|
+
# >> PropCheck::Generators.datetime.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
774
|
+
# => [DateTime.parse("2022-11-17 07:11:59.999983907 +0000"), DateTime.parse("2022-11-19 05:27:16.363618076 +0000")]
|
775
|
+
def datetime(epoch: nil)
|
776
|
+
datetime_from_offset(real_float, epoch: epoch)
|
777
|
+
end
|
778
|
+
|
779
|
+
##
|
780
|
+
# alias for `#datetime`, for backwards compatibility.
|
781
|
+
# Prefer using `datetime`!
|
782
|
+
def date_time(epoch: nil)
|
783
|
+
datetime(epoch: epoch)
|
784
|
+
end
|
785
|
+
|
786
|
+
##
|
787
|
+
# Variant of `#datetime` that only generates datetimes in the future (relative to `:epoch`).
|
788
|
+
#
|
789
|
+
# >> PropCheck::Generators.future_datetime.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new).map(&:inspect)
|
790
|
+
# => ["#<DateTime: 2022-11-21T16:48:00+00:00 ((2459905j,60480s,16093n),+0s,2299161j)>", "#<DateTime: 2022-11-19T18:32:43+00:00 ((2459903j,66763s,636381924n),+0s,2299161j)>"]
|
791
|
+
def future_datetime(epoch: nil)
|
792
|
+
datetime_from_offset(real_positive_float, epoch: epoch)
|
793
|
+
end
|
794
|
+
|
795
|
+
##
|
796
|
+
# Variant of `#datetime` that only generates datetimes in the past (relative to `:epoch`).
|
797
|
+
#
|
798
|
+
# >> PropCheck::Generators.past_datetime.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
799
|
+
# => [DateTime.parse("2022-11-17 07:11:59.999983907 +0000"), DateTime.parse("2022-11-19 05:27:16.363618076 +0000")]
|
800
|
+
def past_datetime(epoch: nil)
|
801
|
+
datetime_from_offset(real_negative_float, epoch: epoch)
|
802
|
+
end
|
803
|
+
|
804
|
+
##
|
805
|
+
# Generates `Time` objects.
|
806
|
+
# Times start around the given `epoch:` and deviate more when `size` increases.
|
807
|
+
# when no epoch is set, `PropCheck::Property::Configuration.default_epoch` is used, which defaults to `DateTime.now`.
|
808
|
+
#
|
809
|
+
# >> PropCheck::Generators.time.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
810
|
+
# => [DateTime.parse("2022-11-17 07:11:59.999983907 +0000").to_time, DateTime.parse("2022-11-19 05:27:16.363618076 +0000").to_time]
|
811
|
+
def time(epoch: nil)
|
812
|
+
datetime(epoch: epoch).map(&:to_time)
|
813
|
+
end
|
814
|
+
|
815
|
+
##
|
816
|
+
# Variant of `#time` that only generates datetimes in the future (relative to `:epoch`).
|
817
|
+
def future_time(epoch: nil)
|
818
|
+
future_datetime(epoch: epoch).map(&:to_time)
|
819
|
+
end
|
820
|
+
|
821
|
+
##
|
822
|
+
# Variant of `#time` that only generates datetimes in the past (relative to `:epoch`).
|
823
|
+
def past_time(epoch: nil)
|
824
|
+
past_datetime(epoch: epoch).map(&:to_time)
|
825
|
+
end
|
826
|
+
|
827
|
+
private def datetime_from_offset(offset_gen, epoch:)
|
828
|
+
if epoch
|
829
|
+
offset_gen.map { |offset| DateTime.jd(epoch.ajd + offset) }
|
830
|
+
else
|
831
|
+
offset_gen.with_config.map do |offset, config|
|
832
|
+
epoch = config.default_epoch.to_date
|
833
|
+
DateTime.jd(epoch.ajd + offset)
|
834
|
+
end
|
835
|
+
end
|
836
|
+
end
|
837
|
+
|
591
838
|
##
|
592
839
|
# Generates an instance of `klass`
|
593
840
|
# using `args` and/or `kwargs`
|
data/lib/prop_check/helper.rb
CHANGED
@@ -33,12 +33,9 @@ module PropCheck
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def call_splatted(val, &block)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
else
|
40
|
-
block.call(val)
|
41
|
-
end
|
36
|
+
return block.call(**val) if val.is_a?(Hash) && val.keys.all? { |k| k.is_a?(Symbol) }
|
37
|
+
|
38
|
+
block.call(val)
|
42
39
|
# if kwval != {}
|
43
40
|
# block.call(**kwval)
|
44
41
|
# else
|
@@ -1,25 +1,45 @@
|
|
1
1
|
module PropCheck
|
2
2
|
class Property
|
3
|
+
## Configure PropCheck
|
4
|
+
#
|
5
|
+
# Configurations can be set globally,
|
6
|
+
# but also overridden on a per-generator basis.
|
7
|
+
# c.f. PropCheck.configure, PropCheck.configuration and PropCheck::Property#with_config
|
8
|
+
#
|
9
|
+
# ## Available options
|
10
|
+
# - `verbose:` When true, shows detailed options of the data generation and shrinking process. (Default: false)
|
11
|
+
# - `n_runs:` The amount of iterations each `forall` is being run.
|
12
|
+
# - `max_generate_attempts:` The amount of times the library tries a generator in total
|
13
|
+
# before raising `Errors::GeneratorExhaustedError`. c.f. `PropCheck::Generator#where`. (Default: 10_000)
|
14
|
+
# - `max_shrink_steps:` The amount of times shrinking is attempted. (Default: 10_000)
|
15
|
+
# - `max_consecutive_attempts:`
|
16
|
+
# - `max_consecutive_attempts:` The amount of times the library tries a filtered generator consecutively
|
17
|
+
# again before raising `Errors::GeneratorExhaustedError`. c.f. `PropCheck::Generator#where`. (Default: 10_000)
|
18
|
+
# - `default_epoch:` The 'base' value to use for date/time generators like
|
19
|
+
# `PropCheck::Generators#date` `PropCheck::Generators#future_date` `PropCheck::Generators#time`, etc.
|
20
|
+
# (Default: `DateTime.now`)
|
3
21
|
Configuration = Struct.new(
|
4
22
|
:verbose,
|
5
23
|
:n_runs,
|
6
24
|
:max_generate_attempts,
|
7
25
|
:max_shrink_steps,
|
8
26
|
:max_consecutive_attempts,
|
9
|
-
|
10
|
-
|
27
|
+
:default_epoch,
|
28
|
+
keyword_init: true
|
29
|
+
) do
|
11
30
|
def initialize(
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
31
|
+
verbose: false,
|
32
|
+
n_runs: 100,
|
33
|
+
max_generate_attempts: 10_000,
|
34
|
+
max_shrink_steps: 10_000,
|
35
|
+
max_consecutive_attempts: 30,
|
36
|
+
default_epoch: DateTime.now
|
37
|
+
)
|
18
38
|
super
|
19
39
|
end
|
20
40
|
|
21
41
|
def merge(other)
|
22
|
-
Configuration.new(**
|
42
|
+
Configuration.new(**to_h.merge(other.to_h))
|
23
43
|
end
|
24
44
|
end
|
25
45
|
end
|
data/lib/prop_check/property.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'stringio'
|
2
|
-
require
|
2
|
+
require 'amazing_print'
|
3
3
|
|
4
4
|
require 'prop_check/property/configuration'
|
5
5
|
require 'prop_check/property/output_formatter'
|
@@ -102,7 +102,7 @@ module PropCheck
|
|
102
102
|
# you can immediately pass a block to this method.
|
103
103
|
# (so `forall(a: Generators.integer).with_config(verbose: true) do ... end` is the same as `forall(a: Generators.integer).with_config(verbose: true).check do ... end`)
|
104
104
|
def with_config(**config, &block)
|
105
|
-
duplicate =
|
105
|
+
duplicate = dup
|
106
106
|
duplicate.instance_variable_set(:@config, @config.merge(config))
|
107
107
|
duplicate.freeze
|
108
108
|
|
@@ -114,7 +114,7 @@ module PropCheck
|
|
114
114
|
def with_bindings(*bindings, **kwbindings)
|
115
115
|
raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
|
116
116
|
|
117
|
-
duplicate =
|
117
|
+
duplicate = dup
|
118
118
|
duplicate.instance_variable_set(:@gen, gen_from_bindings(bindings, kwbindings))
|
119
119
|
duplicate.freeze
|
120
120
|
duplicate
|
@@ -129,22 +129,24 @@ module PropCheck
|
|
129
129
|
# you might encounter a GeneratorExhaustedError.
|
130
130
|
# Only filter if you have few inputs to reject. Otherwise, improve your generators.
|
131
131
|
def where(&condition)
|
132
|
-
|
132
|
+
unless @gen
|
133
|
+
raise ArgumentError,
|
134
|
+
'No generator bindings specified! #where should be called after `#forall` or `#with_bindings`.'
|
135
|
+
end
|
133
136
|
|
134
|
-
duplicate =
|
137
|
+
duplicate = dup
|
135
138
|
duplicate.instance_variable_set(:@gen, @gen.where(&condition))
|
136
139
|
duplicate.freeze
|
137
140
|
duplicate
|
138
141
|
end
|
139
142
|
|
140
|
-
|
141
143
|
##
|
142
144
|
# Calls `hook` before each time a check is run with new data.
|
143
145
|
#
|
144
146
|
# This is useful to add setup logic
|
145
147
|
# When called multiple times, earlier-added hooks will be called _before_ `hook` is called.
|
146
148
|
def before(&hook)
|
147
|
-
duplicate =
|
149
|
+
duplicate = dup
|
148
150
|
duplicate.instance_variable_set(:@hooks, @hooks.add_before(&hook))
|
149
151
|
duplicate.freeze
|
150
152
|
duplicate
|
@@ -156,7 +158,7 @@ module PropCheck
|
|
156
158
|
# This is useful to add teardown logic
|
157
159
|
# When called multiple times, earlier-added hooks will be called _after_ `hook` is called.
|
158
160
|
def after(&hook)
|
159
|
-
duplicate =
|
161
|
+
duplicate = dup
|
160
162
|
duplicate.instance_variable_set(:@hooks, @hooks.add_after(&hook))
|
161
163
|
duplicate.freeze
|
162
164
|
duplicate
|
@@ -176,7 +178,7 @@ module PropCheck
|
|
176
178
|
# it is possible for the code after `yield` not to be called.
|
177
179
|
# So make sure that cleanup logic is wrapped with the `ensure` keyword.
|
178
180
|
def around(&hook)
|
179
|
-
duplicate =
|
181
|
+
duplicate = dup
|
180
182
|
duplicate.instance_variable_set(:@hooks, @hooks.add_around(&hook))
|
181
183
|
duplicate.freeze
|
182
184
|
duplicate
|
@@ -223,15 +225,15 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
|
|
223
225
|
raise_generator_exhausted!
|
224
226
|
end
|
225
227
|
|
226
|
-
private def raise_generator_exhausted!
|
227
|
-
raise Errors::GeneratorExhaustedError, "
|
228
|
+
private def raise_generator_exhausted!
|
229
|
+
raise Errors::GeneratorExhaustedError, ''"
|
228
230
|
Could not perform `n_runs = #{@config.n_runs}` runs,
|
229
231
|
(exhausted #{@config.max_generate_attempts} tries)
|
230
232
|
because too few generator results were adhering to
|
231
233
|
the `where` condition.
|
232
234
|
|
233
235
|
Try refining your generators instead.
|
234
|
-
"
|
236
|
+
"''
|
235
237
|
end
|
236
238
|
|
237
239
|
private def check_attempt(generator_result, n_successful, &block)
|
@@ -246,7 +248,8 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
|
|
246
248
|
# so we can shrink to find their cause.
|
247
249
|
# don't worry: they all get reraised
|
248
250
|
rescue Exception => e
|
249
|
-
output, shrunken_result, shrunken_exception, n_shrink_steps = show_problem_output(e, generator_result,
|
251
|
+
output, shrunken_result, shrunken_exception, n_shrink_steps = show_problem_output(e, generator_result,
|
252
|
+
n_successful, &block)
|
250
253
|
output_string = output.is_a?(StringIO) ? output.string : e.message
|
251
254
|
|
252
255
|
e.define_singleton_method :prop_check_info do
|
@@ -264,9 +267,7 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
|
|
264
267
|
end
|
265
268
|
|
266
269
|
private def attempts_enum(binding_generator)
|
267
|
-
|
268
|
-
|
269
|
-
@hooks
|
270
|
+
@hooks
|
270
271
|
.wrap_enum(raw_attempts_enum(binding_generator))
|
271
272
|
.lazy
|
272
273
|
.take(@config.n_runs)
|
@@ -277,7 +278,14 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
|
|
277
278
|
size = 1
|
278
279
|
(0...@config.max_generate_attempts)
|
279
280
|
.lazy
|
280
|
-
.map
|
281
|
+
.map do
|
282
|
+
binding_generator.generate(
|
283
|
+
size: size,
|
284
|
+
rng: rng,
|
285
|
+
max_consecutive_attempts: @config.max_consecutive_attempts,
|
286
|
+
config: @config
|
287
|
+
)
|
288
|
+
end
|
281
289
|
.map do |result|
|
282
290
|
size += 1
|
283
291
|
|
@@ -289,7 +297,8 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
|
|
289
297
|
output = @config.verbose ? STDOUT : StringIO.new
|
290
298
|
output = PropCheck::Property::OutputFormatter.pre_output(output, n_successful, generator_results.root, problem)
|
291
299
|
shrunken_result, shrunken_exception, n_shrink_steps = shrink(generator_results, output, &block)
|
292
|
-
output = PropCheck::Property::OutputFormatter.post_output(output, n_shrink_steps, shrunken_result,
|
300
|
+
output = PropCheck::Property::OutputFormatter.post_output(output, n_shrink_steps, shrunken_result,
|
301
|
+
shrunken_exception)
|
293
302
|
|
294
303
|
[output, shrunken_result, shrunken_exception, n_shrink_steps]
|
295
304
|
end
|
data/lib/prop_check/version.rb
CHANGED
data/prop_check.gemspec
CHANGED
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prop_check
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Qqwy/Wiebe-Marten Wijnja
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: amazing_print
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
26
|
+
version: '1.2'
|
27
27
|
description: PropCheck allows you to do property-based testing, including shrinking.
|
28
28
|
(akin to Haskell's QuickCheck, Erlang's PropEr, Elixir's StreamData). This means
|
29
29
|
that your test are run many times with different, autogenerated inputs, and as soon
|
@@ -84,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
84
|
- !ruby/object:Gem::Version
|
85
85
|
version: '0'
|
86
86
|
requirements: []
|
87
|
-
rubygems_version: 3.1.
|
87
|
+
rubygems_version: 3.1.6
|
88
88
|
signing_key:
|
89
89
|
specification_version: 4
|
90
90
|
summary: PropCheck allows you to do property-based testing, including shrinking.
|