prop_check 0.11.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6938e33edff09860d467d560675d04173ac8f44c487f65f22a4253bcad01a465
4
- data.tar.gz: 1ed4f6b097d008461e251f285ad52fcd32dfce27cc306862b250cea45e0b954a
3
+ metadata.gz: 013ca5101f31799feb0c86533c4100a781c451f937d6d8497a30476073e27e4b
4
+ data.tar.gz: f592ec086ca3018cfca1016baa1690619a5783274f02c910eb2ad03e5861a53d
5
5
  SHA512:
6
- metadata.gz: e8d7498c00b09f17ec90fced0b130158e1919270837bad1af82ca7f4140f8904816c377cdd3489aea38d6ef36b911a128a92ca956c598daf9f0f88654ca4ac64
7
- data.tar.gz: b05e4c9364808468af3c5b9807cc8cc8e036a780e449efd0468f7b577853bd3a286371a3312073c0cf5952c5debb12dd6b13ad2d3e8d0fed0952839db680757c
6
+ metadata.gz: f49b2514fe4e179013bd8c4a4683c1210b65d6852901156c6c5373f26305343fa4cae40860f13386a8f1fda33406927bf20a357f88cd5b99936318410837ba00
7
+ data.tar.gz: 261b980fb68c2d1af8baccb09919a838f91c1f6ae33f8c8a79923b2cb50490e4406e848ffbf6a75361d13d33719907a3342b4a7eed88ebdf37d1bdd8cd981ee4
@@ -1,3 +1,5 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
1
3
  Metrics/LineLength:
2
4
  Max: 120
3
5
  Style/AccessModifierDeclarations:
@@ -1 +1 @@
1
- ruby 2.6.5
1
+ ruby 2.7.1
@@ -3,7 +3,7 @@ sudo: false
3
3
  language: ruby
4
4
  cache: bundler
5
5
  rvm:
6
- - 2.5.1
6
+ - 2.6.5
7
7
  before_install: gem install bundler -v 2.0.2
8
8
  env:
9
9
  global:
@@ -1 +1,7 @@
1
- - 0.8.0 New syntax that is more explicit, passng generated values to blocks as parameters.
1
+ - 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
+ - 0.13.0 - Adds Generator#resize
3
+ - 0.12.1 - Fixes shrinking when filtering bug.
4
+ - 0.12.0 - `PropCheck::Generators#instance`
5
+ - 0.11.0 - Improved syntax to support Ruby 2.7 and up without deprecation warnings, full support for `#where`.
6
+ - 0.10.0 - Some bugfixes, support for `#where`
7
+ - 0.8.0 - New syntax that is more explicit, passng generated values to blocks as parameters.
data/README.md CHANGED
@@ -24,10 +24,12 @@ Before releasing this gem on Rubygems, the following things need to be finished:
24
24
  - [x] Filtering generators.
25
25
  - [x] Customize the max. of samples to run.
26
26
  - [x] Stop after a ludicrous amount of generator runs, to prevent malfunctioning (infinitely looping) generators from blowing up someone's computer.
27
- - [x] Look into customization of settings from e.g. command line arguments.
27
+ - [x] Look into customization of settings from e.g. command line arguments.
28
28
  - [x] Good, unicode-compliant, string generators.
29
29
  - [x] Filtering generator outputs.
30
30
  - [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.
31
+ - [x] `#instance` generator to allow the easy creation of generators for custom datatypes.
32
+ - [ ] A usage guide.
31
33
 
32
34
  # Nice-to-haves
33
35
 
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -11,6 +11,7 @@ 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
15
 
15
16
  ##
16
17
  # Being a special kind of Proc, a Generator wraps a block.
@@ -21,10 +22,13 @@ module PropCheck
21
22
  ##
22
23
  # Given a `size` (integer) and a random number generator state `rng`,
23
24
  # generate a LazyTree.
24
- def generate(size = @@default_size, rng = @@default_rng, max_consecutive_attempts = @@max_consecutive_attempts)
25
+ def generate(**kwargs)
26
+ kwargs = @@default_kwargs.merge(kwargs)
27
+ max_consecutive_attempts = kwargs[:max_consecutive_attempts]
28
+
25
29
  (0..max_consecutive_attempts).each do
26
- res = @block.call(size, rng)
27
- next if res == :"PropCheck.filter_me"
30
+ res = @block.call(**kwargs)
31
+ next if res.root == :"_PropCheck.filter_me"
28
32
 
29
33
  return res
30
34
  end
@@ -40,18 +44,18 @@ module PropCheck
40
44
  # Generates a value, and only return this value
41
45
  # (drop information for shrinking)
42
46
  #
43
- # >> Generators.integer.call(1000, Random.new(42))
47
+ # >> Generators.integer.call(size: 1000, rng: Random.new(42))
44
48
  # => 126
45
- def call(size = @@default_size, rng = @@default_rng)
46
- generate(size, rng).root
49
+ def call(**kwargs)
50
+ generate(**@@default_kwargs.merge(kwargs)).root
47
51
  end
48
52
 
49
53
  ##
50
54
  # Returns `num_of_samples` values from calling this Generator.
51
55
  # This is mostly useful for debugging if a generator behaves as you intend it to.
52
- def sample(num_of_samples = 10, size: @@default_size, rng: @@default_rng)
56
+ def sample(num_of_samples = 10, **kwargs)
53
57
  num_of_samples.times.map do
54
- call(size, rng)
58
+ call(**@@default_kwargs.merge(kwargs))
55
59
  end
56
60
  end
57
61
 
@@ -61,10 +65,10 @@ module PropCheck
61
65
  #
62
66
  # Keen readers may notice this as the Monadic 'pure'/'return' implementation for Generators.
63
67
  #
64
- # >> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(100, Random.new(42))
68
+ # >> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(size: 100, rng: Random.new(42))
65
69
  # => [2, 79]
66
70
  def self.wrap(val)
67
- Generator.new { |_size, _rng| LazyTree.wrap(val) }
71
+ Generator.new { LazyTree.wrap(val) }
68
72
  end
69
73
 
70
74
  ##
@@ -73,7 +77,7 @@ module PropCheck
73
77
  #
74
78
  # Keen readers may notice this as the Monadic 'bind' (sometimes known as '>>=') implementation for Generators.
75
79
  #
76
- # >> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(100, Random.new(42))
80
+ # >> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(size: 100, rng: Random.new(42))
77
81
  # => [2, 79]
78
82
  def bind(&generator_proc)
79
83
  # Generator.new do |size, rng|
@@ -83,11 +87,11 @@ module PropCheck
83
87
  # inner_generator.generate(size, rng)
84
88
  # end.flatten
85
89
  # end
86
- Generator.new do |size, rng|
87
- outer_result = self.generate(size, rng)
90
+ Generator.new do |**kwargs|
91
+ outer_result = self.generate(**kwargs)
88
92
  outer_result.bind do |outer_val|
89
93
  inner_generator = generator_proc.call(outer_val)
90
- inner_generator.generate(size, rng)
94
+ inner_generator.generate(**kwargs)
91
95
  end
92
96
  end
93
97
  end
@@ -95,11 +99,11 @@ module PropCheck
95
99
  ##
96
100
  # Creates a new Generator that returns a value by running `proc` on the output of the current Generator.
97
101
  #
98
- # >> Generators.choose(32..128).map(&:chr).call(10, Random.new(42))
102
+ # >> Generators.choose(32..128).map(&:chr).call(size: 10, rng: Random.new(42))
99
103
  # => "S"
100
104
  def map(&proc)
101
- Generator.new do |size, rng|
102
- result = self.generate(size, rng)
105
+ Generator.new do |**kwargs|
106
+ result = self.generate(**kwargs)
103
107
  result.map(&proc)
104
108
  end
105
109
  end
@@ -108,19 +112,27 @@ module PropCheck
108
112
  # Creates a new Generator that only produces a value when the block `condition` returns a truthy value.
109
113
  def where(&condition)
110
114
  self.map do |result|
111
- if condition.call(result)
115
+ # if condition.call(*result)
116
+ if PropCheck::Helper.call_splatted(result, &condition)
112
117
  result
113
118
  else
114
119
  :"_PropCheck.filter_me"
115
120
  end
116
121
  end
117
- # self.map do |*result|
118
- # if condition.call(*result)
119
- # result
120
- # else
121
- # :'_PropCheck.filter_me'
122
- # end
123
- # end
122
+ end
123
+
124
+ ##
125
+ # Resizes the generator to either grow faster or smaller than normal.
126
+ #
127
+ # `proc` takes the current size as input and is expected to return the new size.
128
+ # a size should always be a nonnegative integer.
129
+ #
130
+ # >> Generators.integer.resize{}
131
+ def resize(&proc)
132
+ Generator.new do |size:, **other_kwargs|
133
+ new_size = proc.call(size)
134
+ self.generate(**other_kwargs, size: new_size)
135
+ end
124
136
  end
125
137
  end
126
138
  end
@@ -9,7 +9,8 @@ module PropCheck
9
9
  # Use this module by including it in the class (e.g. in your test suite)
10
10
  # where you want to use them.
11
11
  module Generators
12
- extend self
12
+ module_function
13
+
13
14
  ##
14
15
  # Always returns the same value, regardless of `size` or `rng` (random number generator state)
15
16
  #
@@ -59,7 +60,7 @@ module PropCheck
59
60
  # >> r = Random.new(42); Generators.choose(0..5).sample(size: 20000, rng: r)
60
61
  # => [3, 4, 2, 4, 4, 1, 2, 2, 2, 4]
61
62
  def choose(range)
62
- Generator.new do |_size, rng|
63
+ Generator.new do |rng:, **|
63
64
  val = rng.rand(range)
64
65
  LazyTree.new(val, integer_shrink(val))
65
66
  end
@@ -73,19 +74,27 @@ module PropCheck
73
74
  #
74
75
  # Shrinks to integers closer to zero.
75
76
  #
76
- # >> Generators.integer.call(2, Random.new(42))
77
+ # >> Generators.integer.call(size: 2, rng: Random.new(42))
77
78
  # => 1
78
- # >> Generators.integer.call(10000, Random.new(42))
79
+ # >> Generators.integer.call(size: 10000, rng: Random.new(42))
79
80
  # => 5795
80
81
  # >> r = Random.new(42); Generators.integer.sample(size: 20000, rng: r)
81
82
  # => [-4205, -19140, 18158, -8716, -13735, -3150, 17194, 1962, -3977, -18315]
82
83
  def integer
83
- Generator.new do |size, rng|
84
+ Generator.new do |size:, rng:, **|
85
+ ensure_proper_size!(size)
86
+
84
87
  val = rng.rand(-size..size)
85
88
  LazyTree.new(val, integer_shrink(val))
86
89
  end
87
90
  end
88
91
 
92
+ private def ensure_proper_size!(size)
93
+ return if size.is_a?(Integer) && size >= 0
94
+
95
+ raise ArgumentError, "`size:` should be a nonnegative integer but got `#{size.inspect}`"
96
+ end
97
+
89
98
  ##
90
99
  # Only returns integers that are zero or larger.
91
100
  # See `integer` for more information.
@@ -137,7 +146,7 @@ module PropCheck
137
146
  end
138
147
  end
139
148
 
140
- @special_floats = [Float::NAN, Float::INFINITY, -Float::INFINITY, Float::MAX, Float::MIN, 0.0.next_float, 0.0.prev_float]
149
+ @@special_floats = [Float::NAN, Float::INFINITY, -Float::INFINITY, Float::MAX, Float::MIN, 0.0.next_float, 0.0.prev_float]
141
150
  ##
142
151
  # Generates floating-point numbers
143
152
  # Will generate NaN, Infinity, -Infinity,
@@ -150,7 +159,7 @@ module PropCheck
150
159
  # >> Generators.float().sample(10, size: 10, rng: Random.new(42))
151
160
  # => [4.0, 9.555555555555555, 0.0, -Float::INFINITY, 5.5, -5.818181818181818, 1.1428571428571428, 0.0, 8.0, 7.857142857142858]
152
161
  def float
153
- frequency(99 => real_float, 1 => one_of(*@special_floats.map(&method(:constant))))
162
+ frequency(99 => real_float, 1 => one_of(*@@special_floats.map(&method(:constant))))
154
163
  end
155
164
 
156
165
  ##
@@ -193,12 +202,12 @@ module PropCheck
193
202
  #
194
203
  # Shrinks element generators, one at a time (trying last one first).
195
204
  #
196
- # >> Generators.tuple(Generators.integer, Generators.real_float).call(10, Random.new(42))
205
+ # >> Generators.tuple(Generators.integer, Generators.real_float).call(size: 10, rng: Random.new(42))
197
206
  # => [-4, 13.0]
198
207
  def tuple(*generators)
199
- Generator.new do |size, rng|
208
+ Generator.new do |**kwargs|
200
209
  LazyTree.zip(generators.map do |generator|
201
- generator.generate(size, rng)
210
+ generator.generate(**kwargs)
202
211
  end)
203
212
  end
204
213
  end
@@ -210,7 +219,7 @@ module PropCheck
210
219
  #
211
220
  # Shrinks element generators.
212
221
  #
213
- # >> Generators.fixed_hash(a: Generators.integer(), b: Generators.real_float(), c: Generators.integer()).call(10, Random.new(42))
222
+ # >> Generators.fixed_hash(a: Generators.integer(), b: Generators.real_float(), c: Generators.integer()).call(size: 10, rng: Random.new(42))
214
223
  # => {:a=>-4, :b=>13.0, :c=>-3}
215
224
  def fixed_hash(hash)
216
225
  keypair_generators =
@@ -232,6 +241,12 @@ module PropCheck
232
241
  # `empty:` When false, behaves the same as `min: 1`
233
242
  # `min:` Ensures at least this many elements are generated. (default: 0)
234
243
  # `max:` Ensures at most this many elements are generated. When nil, an arbitrary count is used instead. (default: nil)
244
+ # `uniq:` When `true`, ensures that all elements in the array are unique.
245
+ # When given a proc, uses the result of this proc to check for uniqueness.
246
+ # (matching the behaviour of `Array#uniq`)
247
+ # If it is not possible to generate another unique value after the configured `max_consecutive_attempts`
248
+ # an `PropCheck::Errors::GeneratorExhaustedError` will be raised.
249
+ # (default: `false`)
235
250
  #
236
251
  #
237
252
  # >> Generators.array(Generators.positive_integer).sample(5, size: 1, rng: Random.new(42))
@@ -243,28 +258,70 @@ module PropCheck
243
258
  # => [[], [2], [], [], [2]]
244
259
  # >> Generators.array(Generators.positive_integer, empty: false).sample(5, size: 1, rng: Random.new(1))
245
260
  # => [[2], [1], [2], [1], [1]]
261
+ #
262
+ # >> Generators.array(Generators.boolean, uniq: true).sample(5, rng: Random.new(1))
263
+ # => [[true, false], [false, true], [true, false], [false, true], [false, true]]
246
264
 
247
-
248
- def array(element_generator, min: 0, max: nil, empty: true)
265
+ def array(element_generator, min: 0, max: nil, empty: true, uniq: false)
249
266
  min = 1 if min.zero? && !empty
250
-
251
- res = proc do |count|
252
- count = min + 1 if count < min
253
- count += 1 if count == min && min != 0
254
- generators = (min...count).map do
255
- element_generator.clone
256
- end
257
-
258
- tuple(*generators)
259
- end
267
+ uniq = proc { |x| x } if uniq == true
260
268
 
261
269
  if max.nil?
262
- nonnegative_integer.bind(&res)
270
+ nonnegative_integer.bind { |count| make_array(element_generator, min, count, uniq) }
263
271
  else
264
- proc.call(max)
272
+ make_array(element_generator, min, max, uniq)
265
273
  end
266
274
  end
267
275
 
276
+ private def make_array(element_generator, min, count, uniq)
277
+ amount = min if count < min
278
+ amount = min if count == min && min != 0
279
+ amount ||= (count - min)
280
+
281
+ # Simple, optimized implementation:
282
+ return make_array_simple(element_generator, amount) unless uniq
283
+
284
+ # More complex implementation that filters duplicates
285
+ make_array_uniq(element_generator, min, amount, uniq)
286
+ end
287
+
288
+ private def make_array_simple(element_generator, amount)
289
+ generators = amount.times.map do
290
+ element_generator.clone
291
+ end
292
+
293
+ tuple(*generators)
294
+ end
295
+
296
+ private def make_array_uniq(element_generator, min, amount, uniq_fun)
297
+ Generator.new do |**kwargs|
298
+ arr = []
299
+ uniques = Set.new
300
+ 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
+
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
314
+ else
315
+ raise Errors::GeneratorExhaustedError, "Too many consecutive elements filtered by 'uniq:'."
316
+ end
317
+ end
318
+ end
319
+ .take_while { arr.size < amount }
320
+ .force
321
+
322
+ LazyTree.zip(arr).map { |array| array.uniq(&uniq_fun) }
323
+ end
324
+ end
268
325
 
269
326
  ##
270
327
  # Generates a hash of key->values,
@@ -275,13 +332,24 @@ module PropCheck
275
332
  #
276
333
  # >> Generators.hash(Generators.printable_ascii_string, Generators.positive_integer).sample(5, size: 3, rng: Random.new(42))
277
334
  # => [{""=>2, "g\\4"=>4, "rv"=>2}, {"7"=>2}, {"!"=>1, "E!"=>1}, {"kY5"=>2}, {}]
278
- def hash(key_generator, value_generator, **kwargs)
335
+ def hash(*args, **kwargs)
336
+ if args.length == 2
337
+ hash_of(*args, **kwargs)
338
+ else
339
+ super
340
+ end
341
+ end
342
+
343
+ ##
344
+ #
345
+ # Alias for `#hash` that does not conflict with a possibly overriden `Object#hash`.
346
+ #
347
+ def hash_of(key_generator, value_generator, **kwargs)
279
348
  array(tuple(key_generator, value_generator), **kwargs)
280
349
  .map(&:to_h)
281
350
  end
282
351
 
283
-
284
- @alphanumeric_chars = [('a'..'z'), ('A'..'Z'), ('0'..'9')].flat_map(&:to_a).freeze
352
+ @@alphanumeric_chars = [('a'..'z'), ('A'..'Z'), ('0'..'9')].flat_map(&:to_a).freeze
285
353
  ##
286
354
  # Generates a single-character string
287
355
  # containing one of a..z, A..Z, 0..9
@@ -291,7 +359,7 @@ module PropCheck
291
359
  # >> Generators.alphanumeric_char.sample(5, size: 10, rng: Random.new(42))
292
360
  # => ["M", "Z", "C", "o", "Q"]
293
361
  def alphanumeric_char
294
- one_of(*@alphanumeric_chars.map(&method(:constant)))
362
+ one_of(*@@alphanumeric_chars.map(&method(:constant)))
295
363
  end
296
364
 
297
365
  ##
@@ -302,11 +370,13 @@ module PropCheck
302
370
  #
303
371
  # >> Generators.alphanumeric_string.sample(5, size: 10, rng: Random.new(42))
304
372
  # => ["ZCoQ", "8uM", "wkkx0JNx", "v0bxRDLb", "Gl5v8RyWA6"]
373
+ #
374
+ # Accepts the same options as `array`
305
375
  def alphanumeric_string(**kwargs)
306
376
  array(alphanumeric_char, **kwargs).map(&:join)
307
377
  end
308
378
 
309
- @printable_ascii_chars = (' '..'~').to_a.freeze
379
+ @@printable_ascii_chars = (' '..'~').to_a.freeze
310
380
 
311
381
  ##
312
382
  # Generates a single-character string
@@ -317,7 +387,7 @@ module PropCheck
317
387
  # >> Generators.printable_ascii_char.sample(size: 10, rng: Random.new(42))
318
388
  # => ["S", "|", ".", "g", "\\", "4", "r", "v", "j", "j"]
319
389
  def printable_ascii_char
320
- one_of(*@printable_ascii_chars.map(&method(:constant)))
390
+ one_of(*@@printable_ascii_chars.map(&method(:constant)))
321
391
  end
322
392
 
323
393
  ##
@@ -328,12 +398,14 @@ module PropCheck
328
398
  #
329
399
  # >> Generators.printable_ascii_string.sample(5, size: 10, rng: Random.new(42))
330
400
  # => ["S|.g", "rvjjw7\"5T!", "=", "!_[4@", "Y"]
401
+ #
402
+ # Accepts the same options as `array`
331
403
  def printable_ascii_string(**kwargs)
332
404
  array(printable_ascii_char, **kwargs).map(&:join)
333
405
  end
334
406
 
335
- @ascii_chars = [
336
- @printable_ascii_chars,
407
+ @@ascii_chars = [
408
+ @@printable_ascii_chars,
337
409
  [
338
410
  "\n",
339
411
  "\r",
@@ -356,7 +428,7 @@ module PropCheck
356
428
  # >> Generators.ascii_char.sample(size: 10, rng: Random.new(42))
357
429
  # => ["d", "S", "|", ".", "g", "\\", "4", "d", "r", "v"]
358
430
  def ascii_char
359
- one_of(*@ascii_chars.map(&method(:constant)))
431
+ one_of(*@@ascii_chars.map(&method(:constant)))
360
432
  end
361
433
 
362
434
  ##
@@ -367,12 +439,14 @@ module PropCheck
367
439
  #
368
440
  # >> Generators.ascii_string.sample(5, size: 10, rng: Random.new(42))
369
441
  # => ["S|.g", "drvjjw\b\a7\"", "!w=E!_[4@k", "x", "zZI{[o"]
442
+ #
443
+ # Accepts the same options as `array`
370
444
  def ascii_string(**kwargs)
371
445
  array(ascii_char, **kwargs).map(&:join)
372
446
  end
373
447
 
374
- @printable_chars = [
375
- @ascii_chars,
448
+ @@printable_chars = [
449
+ @@ascii_chars,
376
450
  "\u{A0}".."\u{D7FF}",
377
451
  "\u{E000}".."\u{FFFD}",
378
452
  "\u{10000}".."\u{10FFFF}"
@@ -387,7 +461,7 @@ module PropCheck
387
461
  # >> Generators.printable_char.sample(size: 10, rng: Random.new(42))
388
462
  # => ["吏", "", "", "", "", "", "", "", "", "Ȍ"]
389
463
  def printable_char
390
- one_of(*@printable_chars.map(&method(:constant)))
464
+ one_of(*@@printable_chars.map(&method(:constant)))
391
465
  end
392
466
 
393
467
  ##
@@ -398,6 +472,8 @@ module PropCheck
398
472
  #
399
473
  # >> Generators.printable_string.sample(5, size: 10, rng: Random.new(42))
400
474
  # => ["", "Ȍ", "𐁂", "Ȕ", ""]
475
+ #
476
+ # Accepts the same options as `array`
401
477
  def printable_string(**kwargs)
402
478
  array(printable_char, **kwargs).map(&:join)
403
479
  end
@@ -424,6 +500,8 @@ module PropCheck
424
500
  #
425
501
  # >> Generators.string.sample(5, size: 10, rng: Random.new(42))
426
502
  # => ["\u{A3DB3}𠍜\u{3F46A}\u{1AEBC}", "􍙦𡡹󴇒\u{DED74}𪱣\u{43E97}ꂂ\u{50695}􏴴\u{C0301}", "\u{4FD9D}", "\u{C14BF}\u{193BB}𭇋󱣼\u{76B58}", "𦐺\u{9FDDB}\u{80ABB}\u{9E3CF}𐂽\u{14AAE}"]
503
+ #
504
+ # Accepts the same options as `array`
427
505
  def string(**kwargs)
428
506
  array(char, **kwargs).map(&:join)
429
507
  end
@@ -509,5 +587,44 @@ module PropCheck
509
587
  def nillable(other_generator)
510
588
  frequency(9 => other_generator, 1 => constant(nil))
511
589
  end
590
+
591
+ ##
592
+ # Generates an instance of `klass`
593
+ # using `args` and/or `kwargs`
594
+ # as generators for the arguments that are passed to `klass.new`
595
+ #
596
+ # ## Example:
597
+ #
598
+ # Given a class like this:
599
+ #
600
+ #
601
+ # class User
602
+ # attr_accessor :name, :age
603
+ # def initialize(name: , age: )
604
+ # @name = name
605
+ # @age = age
606
+ # end
607
+ #
608
+ # def inspect
609
+ # "<User name: #{@name.inspect}, age: #{@age.inspect}>"
610
+ # end
611
+ # end
612
+ #
613
+ # >> user_gen = Generators.instance(User, name: Generators.printable_ascii_string, age: Generators.nonnegative_integer)
614
+ # >> user_gen.sample(3, rng: Random.new(42)).inspect
615
+ # => "[<User name: \"S|.g\", age: 10>, <User name: \"rvjj\", age: 10>, <User name: \"7\\\"5T!w=\", age: 5>]"
616
+ def instance(klass, *args, **kwargs)
617
+ tuple(*args).bind do |vals|
618
+ fixed_hash(**kwargs).map do |kwvals|
619
+ if kwvals == {}
620
+ klass.new(*vals)
621
+ elsif vals == []
622
+ klass.new(**kwvals)
623
+ else
624
+ klass.new(*vals, **kwvals)
625
+ end
626
+ end
627
+ end
628
+ end
512
629
  end
513
630
  end
@@ -31,5 +31,19 @@ module PropCheck
31
31
  def lazy_append(this_enumerator, other_enumerator)
32
32
  [this_enumerator, other_enumerator].lazy.flat_map(&:lazy)
33
33
  end
34
+
35
+ def call_splatted(val, &block)
36
+ case val
37
+ when Hash
38
+ block.call(**val)
39
+ else
40
+ block.call(val)
41
+ end
42
+ # if kwval != {}
43
+ # block.call(**kwval)
44
+ # else
45
+ # block.call(*val)
46
+ # end
47
+ end
34
48
  end
35
49
  end
@@ -19,10 +19,11 @@
19
19
  # wrapping the elements of an enumerable with hooks.
20
20
  class PropCheck::Hooks
21
21
  # attr_reader :before, :after, :around
22
- def initialize()
23
- @before = proc {}
24
- @after = proc {}
25
- @around = proc { |*args, &block| block.call(*args) }
22
+ def initialize(before: proc {}, after: proc {}, around: proc { |*args, &block| block.call(*args) })
23
+ @before = before
24
+ @after = after
25
+ @around = around
26
+ freeze
26
27
  end
27
28
 
28
29
  def wrap_enum(enumerable)
@@ -59,34 +60,40 @@ class PropCheck::Hooks
59
60
  # Adds `hook` to the `before` proc.
60
61
  # It is called after earlier-added `before` procs.
61
62
  def add_before(&hook)
62
- old_before = @before
63
- @before = proc {
64
- old_before.call
63
+ # old_before = @before
64
+ new_before = proc {
65
+ @before.call
65
66
  hook.call
66
67
  }
68
+ # self
69
+ self.class.new(before: new_before, after: @after, around: @around)
67
70
  end
68
71
 
69
72
  ##
70
73
  # Adds `hook` to the `after` proc.
71
74
  # It is called before earlier-added `after` procs.
72
75
  def add_after(&hook)
73
- old_after = @after
74
- @after = proc {
76
+ # old_after = @after
77
+ new_after = proc {
75
78
  hook.call
76
- old_after.call
79
+ @after.call
77
80
  }
81
+ # self
82
+ self.class.new(before: @before, after: new_after, around: @around)
78
83
  end
79
84
 
80
85
  ##
81
86
  # Adds `hook` to the `around` proc.
82
87
  # It is called _inside_ earlier-added `around` procs.
83
88
  def add_around(&hook)
84
- old_around = @around
85
- @around = proc do |&block|
86
- old_around.call do |*args|
89
+ # old_around = @around
90
+ new_around = proc do |&block|
91
+ @around.call do |*args|
87
92
  hook.call(*args, &block)
88
93
  end
89
94
  end
95
+ # self
96
+ self.class.new(before: @before, after: @after, around: new_around)
90
97
  end
91
98
 
92
99
  ##
@@ -7,12 +7,18 @@ module PropCheck
7
7
  class LazyTree
8
8
  require 'prop_check/helper'
9
9
 
10
- attr_accessor :root, :children
10
+ include Enumerable
11
+
12
+ attr_accessor :root
11
13
  def initialize(root, children = [].lazy)
12
14
  @root = root
13
15
  @children = children
14
16
  end
15
17
 
18
+ def children
19
+ @children.reject { |child| child.root == :"_PropCheck.filter_me" }
20
+ end
21
+
16
22
  ##
17
23
  # Maps `block` eagerly over `root` and lazily over `children`, returning a new LazyTree as result.
18
24
  #
@@ -66,25 +72,15 @@ module PropCheck
66
72
  # >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).each.force
67
73
  # => [1, 4, 2, 3]
68
74
  def each(&block)
69
- squish = lambda do |tree, list|
70
- new_children = tree.children.reduce(list) { |acc, elem| squish.call(elem, acc) }
71
- PropCheck::Helper.lazy_append([tree.root], new_children)
72
- end
73
-
74
- squish
75
- .call(self, [])
75
+ self.to_enum(:each) unless block_given?
76
76
 
77
- # base = [root]
78
- # recursive = children.map(&:each)
79
- # res = PropCheck::Helper.lazy_append(base, recursive)
80
-
81
- # return res.each(&block) if block_given?
82
-
83
- # res
77
+ squish([])
78
+ .each(&block)
79
+ end
84
80
 
85
- # res = [[root], children.flat_map(&:each)].lazy.flat_map(&:lazy)
86
- # res = res.map(&block) if block_given?
87
- # res
81
+ protected def squish(arr)
82
+ new_children = self.children.reduce(arr) { |acc, elem| elem.squish(acc) }
83
+ PropCheck::Helper.lazy_append([self.root], new_children)
88
84
  end
89
85
 
90
86
  ##
@@ -129,5 +125,6 @@ module PropCheck
129
125
  end
130
126
  end
131
127
  end
128
+
132
129
  end
133
130
  end
@@ -7,9 +7,15 @@ require 'prop_check/property/shrinker'
7
7
  require 'prop_check/hooks'
8
8
  module PropCheck
9
9
  ##
10
- # Run properties
10
+ # Create and run property-checks.
11
+ #
12
+ # For simple usage, see `.forall`.
13
+ #
14
+ # For advanced usage, call `PropCheck::Property.new(...)` and then configure it to your liking
15
+ # using e.g. `#with_config`, `#before`, `#after`, `#around` etc.
16
+ # Each of these methods will return a new `Property`, so earlier properties are not mutated.
17
+ # This allows you to re-use configuration and hooks between multiple tests.
11
18
  class Property
12
-
13
19
  ##
14
20
  # Main entry-point to create (and possibly immediately run) a property-test.
15
21
  #
@@ -35,9 +41,8 @@ module PropCheck
35
41
  # a Property object is returned, which you can call the other instance methods
36
42
  # of this class on before finally passing a block to it using `#check`.
37
43
  # (so `forall(Generators.integer) do |val| ... end` and forall(Generators.integer).check do |val| ... end` are the same)
38
- def self.forall(*bindings, &block)
39
-
40
- property = new(*bindings)
44
+ def self.forall(*bindings, **kwbindings, &block)
45
+ property = new(*bindings, **kwbindings)
41
46
 
42
47
  return property.check(&block) if block_given?
43
48
 
@@ -61,18 +66,25 @@ module PropCheck
61
66
  yield(configuration)
62
67
  end
63
68
 
64
- attr_reader :bindings, :condition
65
-
66
69
  def initialize(*bindings, **kwbindings)
67
- raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
68
-
69
- @bindings = bindings
70
- @kwbindings = kwbindings
71
- @condition = proc { true }
72
70
  @config = self.class.configuration
73
71
  @hooks = PropCheck::Hooks.new
72
+
73
+ @gen = gen_from_bindings(bindings, kwbindings) unless bindings.empty? && kwbindings.empty?
74
+ freeze
74
75
  end
75
76
 
77
+ # [:condition, :config, :hooks, :gen].each do |symbol|
78
+ # define_method(symbol) do
79
+ # self.instance_variable_get("@#{symbol}")
80
+ # end
81
+
82
+ # protected define_method("#{symbol}=") do |value|
83
+ # duplicate = self.dup
84
+ # duplicate.instance_variable_set("@#{symbol}", value)
85
+ # duplicate
86
+ # end
87
+
76
88
  ##
77
89
  # Returns the configuration of this property
78
90
  # for introspection.
@@ -90,11 +102,22 @@ module PropCheck
90
102
  # you can immediately pass a block to this method.
91
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`)
92
104
  def with_config(**config, &block)
93
- @config = @config.merge(config)
105
+ duplicate = self.dup
106
+ duplicate.instance_variable_set(:@config, @config.merge(config))
107
+ duplicate.freeze
108
+
109
+ return duplicate.check(&block) if block_given?
94
110
 
95
- return self.check(&block) if block_given?
111
+ duplicate
112
+ end
113
+
114
+ def with_bindings(*bindings, **kwbindings)
115
+ raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
96
116
 
97
- self
117
+ duplicate = self.dup
118
+ duplicate.instance_variable_set(:@gen, gen_from_bindings(bindings, kwbindings))
119
+ duplicate.freeze
120
+ duplicate
98
121
  end
99
122
 
100
123
  ##
@@ -106,22 +129,25 @@ module PropCheck
106
129
  # you might encounter a GeneratorExhaustedError.
107
130
  # Only filter if you have few inputs to reject. Otherwise, improve your generators.
108
131
  def where(&condition)
109
- original_condition = @condition.dup
110
- @condition = proc do |*args|
111
- original_condition.call(*args) && condition.call(*args)
112
- end
132
+ raise ArgumentError, 'No generator bindings specified! #where should be called after `#forall` or `#with_bindings`.' unless @gen
113
133
 
114
- self
134
+ duplicate = self.dup
135
+ duplicate.instance_variable_set(:@gen, @gen.where(&condition))
136
+ duplicate.freeze
137
+ duplicate
115
138
  end
116
139
 
140
+
117
141
  ##
118
142
  # Calls `hook` before each time a check is run with new data.
119
143
  #
120
144
  # This is useful to add setup logic
121
145
  # When called multiple times, earlier-added hooks will be called _before_ `hook` is called.
122
146
  def before(&hook)
123
- @hooks.add_before(&hook)
124
- self
147
+ duplicate = self.dup
148
+ duplicate.instance_variable_set(:@hooks, @hooks.add_before(&hook))
149
+ duplicate.freeze
150
+ duplicate
125
151
  end
126
152
 
127
153
  ##
@@ -130,8 +156,10 @@ module PropCheck
130
156
  # This is useful to add teardown logic
131
157
  # When called multiple times, earlier-added hooks will be called _after_ `hook` is called.
132
158
  def after(&hook)
133
- @hooks.add_after(&hook)
134
- self
159
+ duplicate = self.dup
160
+ duplicate.instance_variable_set(:@hooks, @hooks.add_after(&hook))
161
+ duplicate.freeze
162
+ duplicate
135
163
  end
136
164
 
137
165
  ##
@@ -148,28 +176,20 @@ module PropCheck
148
176
  # it is possible for the code after `yield` not to be called.
149
177
  # So make sure that cleanup logic is wrapped with the `ensure` keyword.
150
178
  def around(&hook)
151
- @hooks.add_around(&hook)
152
- self
179
+ duplicate = self.dup
180
+ duplicate.instance_variable_set(:@hooks, @hooks.add_around(&hook))
181
+ duplicate.freeze
182
+ duplicate
153
183
  end
154
184
 
155
185
  ##
156
186
  # Checks the property (after settings have been altered using the other instance methods in this class.)
157
187
  def check(&block)
158
- gens =
159
- if @kwbindings != {}
160
- kwbinding_generator = PropCheck::Generators.fixed_hash(**@kwbindings)
161
- @bindings + [kwbinding_generator]
162
- else
163
- @bindings
164
- end
165
- binding_generator = PropCheck::Generators.tuple(*gens)
166
- # binding_generator = PropCheck::Generators.fixed_hash(**@kwbindings)
167
-
168
188
  n_runs = 0
169
189
  n_successful = 0
170
190
 
171
191
  # Loop stops at first exception
172
- attempts_enum(binding_generator).each do |generator_result|
192
+ attempts_enum(@gen).each do |generator_result|
173
193
  n_runs += 1
174
194
  check_attempt(generator_result, n_successful, &block)
175
195
  n_successful += 1
@@ -178,6 +198,25 @@ module PropCheck
178
198
  ensure_not_exhausted!(n_runs)
179
199
  end
180
200
 
201
+ private def gen_from_bindings(bindings, kwbindings)
202
+ if bindings == [] && kwbindings != {}
203
+ PropCheck::Generators.fixed_hash(**kwbindings)
204
+ elsif bindings != [] && kwbindings == {}
205
+ if bindings.size == 1
206
+ bindings.first
207
+ else
208
+ PropCheck::Generators.tuple(*bindings)
209
+ end
210
+ else
211
+ raise ArgumentError,
212
+ 'Attempted to use both normal and keyword bindings at the same time.
213
+ This is not supported because of the separation of positional and keyword arguments
214
+ (the old behaviour is deprecated in Ruby 2.7 and will be removed in 3.0)
215
+ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
216
+ '
217
+ end
218
+ end
219
+
181
220
  private def ensure_not_exhausted!(n_runs)
182
221
  return if n_runs >= @config.n_runs
183
222
 
@@ -196,7 +235,7 @@ module PropCheck
196
235
  end
197
236
 
198
237
  private def check_attempt(generator_result, n_successful, &block)
199
- block.call(*generator_result.root)
238
+ PropCheck::Helper.call_splatted(generator_result.root, &block)
200
239
 
201
240
  # immediately stop (without shrinnking) for when the app is asked
202
241
  # to close by outside intervention
@@ -225,6 +264,8 @@ module PropCheck
225
264
  end
226
265
 
227
266
  private def attempts_enum(binding_generator)
267
+ ap @hooks
268
+
228
269
  @hooks
229
270
  .wrap_enum(raw_attempts_enum(binding_generator))
230
271
  .lazy
@@ -236,9 +277,7 @@ module PropCheck
236
277
  size = 1
237
278
  (0...@config.max_generate_attempts)
238
279
  .lazy
239
- .map { binding_generator.generate(size, rng) }
240
- .reject { |val| val.root.any? { |elem| elem == :"_PropCheck.filter_me" }}
241
- .select { |val| @condition.call(*val.root) }
280
+ .map { binding_generator.generate(size: size, rng: rng, max_consecutive_attempts: @config.max_consecutive_attempts) }
242
281
  .map do |result|
243
282
  size += 1
244
283
 
@@ -1,8 +1,20 @@
1
1
  module PropCheck
2
2
  class Property
3
- Configuration = Struct.new(:verbose, :n_runs, :max_generate_attempts, :max_shrink_steps, keyword_init: true) do
3
+ Configuration = Struct.new(
4
+ :verbose,
5
+ :n_runs,
6
+ :max_generate_attempts,
7
+ :max_shrink_steps,
8
+ :max_consecutive_attempts,
9
+ keyword_init: true) do
4
10
 
5
- def initialize(verbose: false, n_runs: 1_000, max_generate_attempts: 10_000, max_shrink_steps: 10_000)
11
+ 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
+ )
6
18
  super
7
19
  end
8
20
 
@@ -1,3 +1,4 @@
1
+ require 'prop_check/helper'
1
2
  class PropCheck::Property::Shrinker
2
3
  def initialize(bindings_tree, io, hooks, config)
3
4
  @problem_child = bindings_tree
@@ -62,7 +63,7 @@ class PropCheck::Property::Shrinker
62
63
 
63
64
  private def safe_call_block(sibling, &block)
64
65
  begin
65
- block.call(*sibling.root)
66
+ PropCheck::Helper.call_splatted(sibling.root, &block)
66
67
  # It is correct that we want to rescue _all_ Exceptions
67
68
  # not only 'StandardError's
68
69
  rescue Exception => e
@@ -1,3 +1,3 @@
1
1
  module PropCheck
2
- VERSION = '0.11.0'
2
+ VERSION = '0.14.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prop_check
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.14.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-02 00:00:00.000000000 Z
11
+ date: 2020-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_print
@@ -47,6 +47,7 @@ files:
47
47
  - README.md
48
48
  - Rakefile
49
49
  - bin/console
50
+ - bin/rspec
50
51
  - bin/setup
51
52
  - lib/prop_check.rb
52
53
  - lib/prop_check/generator.rb
@@ -83,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
84
  - !ruby/object:Gem::Version
84
85
  version: '0'
85
86
  requirements: []
86
- rubygems_version: 3.0.3
87
+ rubygems_version: 3.1.2
87
88
  signing_key:
88
89
  specification_version: 4
89
90
  summary: PropCheck allows you to do property-based testing, including shrinking.