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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 013ca5101f31799feb0c86533c4100a781c451f937d6d8497a30476073e27e4b
4
- data.tar.gz: f592ec086ca3018cfca1016baa1690619a5783274f02c910eb2ad03e5861a53d
3
+ metadata.gz: c6d76933200bf114634aaa2d53f4c0832d6c362a2051e12d760d59a8ccae14e7
4
+ data.tar.gz: 452bcebdfb1b5c0113519840ce22aec73be64a5ab60c20dacd3a537eb860bb8e
5
5
  SHA512:
6
- metadata.gz: f49b2514fe4e179013bd8c4a4683c1210b65d6852901156c6c5373f26305343fa4cae40860f13386a8f1fda33406927bf20a357f88cd5b99936318410837ba00
7
- data.tar.gz: 261b980fb68c2d1af8baccb09919a838f91c1f6ae33f8c8a79923b2cb50490e4406e848ffbf6a75361d13d33719907a3342b4a7eed88ebdf37d1bdd8cd981ee4
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, or completely custom).
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
- ## TODOs before stable release
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
- Before releasing this gem on Rubygems, the following things need to be finished:
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
- - [x] Finalize the testing DSL.
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
- # Nice-to-haves
70
+ ## Nice-to-haves
35
71
 
36
- - [x] Basic integration with RSpec. See also https://groups.google.com/forum/#!msg/rspec/U-LmL0OnO-Y/iW_Jcd6JBAAJ for progress on this.
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 and a block to run.
65
- Inside the block, each of the names in the keyword-argument-list is available by its name.
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/).
@@ -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, max_consecutive_attempts: @@max_consecutive_attempts}
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 = self.generate(**kwargs)
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 = self.generate(**kwargs)
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
- self.map do |result|
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
- self.generate(**other_kwargs, size: new_size)
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 to numbers closer to zero.
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
- @@special_floats = [Float::NAN, Float::INFINITY, -Float::INFINITY, Float::MAX, Float::MIN, 0.0.next_float, 0.0.prev_float]
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/100 generated numbers is a special one.
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
- # => [4.0, 9.555555555555555, 0.0, -Float::INFINITY, 5.5, -5.818181818181818, 1.1428571428571428, 0.0, 8.0, 7.857142857142858]
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(99 => real_float, 1 => one_of(*@@special_floats.map(&method(:constant))))
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, max, uniq)
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
- if count > kwargs[:max_consecutive_attempts]
311
- if arr.size >= min
312
- # Give up and return shorter array in this case
313
- amount = min
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
- raise Errors::GeneratorExhaustedError, "Too many consecutive elements filtered by 'uniq:'."
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
- end
319
- .take_while { arr.size < amount }
320
- .force
434
+ .take_while { arr.size < amount }
435
+ .force
321
436
 
322
- LazyTree.zip(arr).map { |array| array.uniq(&uniq_fun) }
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: 10, rng: Random.new(42))
563
- # => [[4, 0, -3, 10, -4, 8, 0, 0, 10], -3, [5.5, -5.818181818181818, 1.1428571428571428, 0.0, 8.0, 7.857142857142858, -0.6666666666666665, 5.25], [], ["\u{9E553}\u{DD56E}\u{A5BBB}\u{8BDAB}\u{3E9FC}\u{C4307}\u{DAFAE}\u{1A022}\u{938CD}\u{70631}", "\u{C4C01}\u{32D85}\u{425DC}"]]
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`
@@ -33,12 +33,9 @@ module PropCheck
33
33
  end
34
34
 
35
35
  def call_splatted(val, &block)
36
- case val
37
- when Hash
38
- block.call(**val)
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
- keyword_init: true) do
10
-
27
+ :default_epoch,
28
+ keyword_init: true
29
+ ) do
11
30
  def initialize(
12
- verbose: false,
13
- n_runs: 100,
14
- max_generate_attempts: 10_000,
15
- max_shrink_steps: 10_000,
16
- max_consecutive_attempts: 30
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(**self.to_h.merge(other.to_h))
42
+ Configuration.new(**to_h.merge(other.to_h))
23
43
  end
24
44
  end
25
45
  end
@@ -1,5 +1,5 @@
1
1
  require 'stringio'
2
- require "awesome_print"
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 = self.dup
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 = self.dup
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
- raise ArgumentError, 'No generator bindings specified! #where should be called after `#forall` or `#with_bindings`.' unless @gen
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 = self.dup
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 = self.dup
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 = self.dup
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 = self.dup
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, n_successful, &block)
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
- ap @hooks
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 { binding_generator.generate(size: size, rng: rng, max_consecutive_attempts: @config.max_consecutive_attempts) }
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, shrunken_exception)
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
@@ -1,3 +1,3 @@
1
1
  module PropCheck
2
- VERSION = '0.14.0'
2
+ VERSION = '0.15.0'
3
3
  end
data/prop_check.gemspec CHANGED
@@ -36,5 +36,5 @@ Gem::Specification.new do |spec|
36
36
 
37
37
  spec.required_ruby_version = '>= 2.5.1'
38
38
 
39
- spec.add_dependency 'awesome_print', '~> 1.8'
39
+ spec.add_dependency 'amazing_print', '~> 1.2'
40
40
  end
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.14.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: 2020-08-09 00:00:00.000000000 Z
11
+ date: 2022-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: awesome_print
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.8'
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.8'
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.2
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.