prop_check 0.11.0 → 0.14.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: 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.