prop_check 0.10.4 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.tool-versions +1 -1
- data/.travis.yml +1 -1
- data/CHANGELOG.md +6 -1
- data/README.md +3 -1
- data/bin/rspec +29 -0
- data/lib/prop_check/generator.rb +39 -29
- data/lib/prop_check/generators.rb +108 -24
- data/lib/prop_check/helper.rb +14 -0
- data/lib/prop_check/lazy_tree.rb +15 -18
- data/lib/prop_check/property.rb +34 -23
- data/lib/prop_check/property/configuration.rb +14 -2
- data/lib/prop_check/property/shrinker.rb +2 -1
- data/lib/prop_check/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb51503257a33dbe349da1b2b582be1c0fe38465bbe10fb14f610885591c9f7b
|
4
|
+
data.tar.gz: d357a2221e6b078c58d78826758cb22f4df3b8c34dd746001d854c505dfe2dea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5d757c5c95b19cb3805ff1cc14af811fbb5e4f0cdb0cadffa581f46d33d20f2c37b8093fda66828b6ce3862b0743723ebb5afe01b183acc4bdbe6fc3f96ddb9b
|
7
|
+
data.tar.gz: e9288cedccdaafe609c2557a74176b9eea306fbff4aa2e9b9a4219702755efa33e53fb9f22d13f5ad0d7aa1af0dca732a1fe924d2119c43df39b71d1107be6d4
|
data/.tool-versions
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby 2.
|
1
|
+
ruby 2.7.1
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1 +1,6 @@
|
|
1
|
-
- 0.
|
1
|
+
- 0.13.0 - Adds Generator#resize
|
2
|
+
- 0.12.1 - Fixes shrinking when filtering bug.
|
3
|
+
- 0.12.0 - `PropCheck::Generators#instance`
|
4
|
+
- 0.11.0 - Improved syntax to support Ruby 2.7 and up without deprecation warnings, full support for `#where`.
|
5
|
+
- 0.10.0 - Some bugfixes, support for `#where`
|
6
|
+
- 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
|
-
|
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
|
|
data/bin/rspec
ADDED
@@ -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")
|
data/lib/prop_check/generator.rb
CHANGED
@@ -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(
|
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(
|
27
|
-
next if res == :"
|
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(
|
46
|
-
generate(
|
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,
|
56
|
+
def sample(num_of_samples = 10, **kwargs)
|
53
57
|
num_of_samples.times.map do
|
54
|
-
call(
|
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 {
|
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 |
|
87
|
-
outer_result = self.generate(
|
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(
|
94
|
+
inner_generator.generate(**kwargs)
|
91
95
|
end
|
92
96
|
end
|
93
97
|
end
|
@@ -95,34 +99,40 @@ 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 |
|
102
|
-
result = self.generate(
|
103
|
-
result.map
|
104
|
-
proc.call(*val)
|
105
|
-
end
|
105
|
+
Generator.new do |**kwargs|
|
106
|
+
result = self.generate(**kwargs)
|
107
|
+
result.map(&proc)
|
106
108
|
end
|
107
109
|
end
|
108
110
|
|
109
111
|
##
|
110
112
|
# Creates a new Generator that only produces a value when the block `condition` returns a truthy value.
|
111
113
|
def where(&condition)
|
112
|
-
self.map do
|
113
|
-
if condition.call(*result)
|
114
|
+
self.map do |result|
|
115
|
+
# if condition.call(*result)
|
116
|
+
if PropCheck::Helper.call_splatted(result, &condition)
|
114
117
|
result
|
115
118
|
else
|
116
119
|
:"_PropCheck.filter_me"
|
117
120
|
end
|
118
121
|
end
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
126
136
|
end
|
127
137
|
end
|
128
138
|
end
|
@@ -10,6 +10,7 @@ module PropCheck
|
|
10
10
|
# where you want to use them.
|
11
11
|
module Generators
|
12
12
|
extend self
|
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 |
|
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
|
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.
|
@@ -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 |
|
208
|
+
Generator.new do |**kwargs|
|
200
209
|
LazyTree.zip(generators.map do |generator|
|
201
|
-
generator.generate(
|
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 =
|
@@ -227,17 +236,42 @@ module PropCheck
|
|
227
236
|
# is generated by `element_generator`.
|
228
237
|
#
|
229
238
|
# Shrinks to shorter arrays (with shrunken elements).
|
239
|
+
# Accepted keyword arguments:
|
240
|
+
#
|
241
|
+
# `empty:` When false, behaves the same as `min: 1`
|
242
|
+
# `min:` Ensures at least this many elements are generated. (default: 0)
|
243
|
+
# `max:` Ensures at most this many elements are generated. When nil, an arbitrary count is used instead. (default: nil)
|
244
|
+
#
|
230
245
|
#
|
246
|
+
# >> Generators.array(Generators.positive_integer).sample(5, size: 1, rng: Random.new(42))
|
247
|
+
# => [[2], [2], [2], [1], [2]]
|
231
248
|
# >> Generators.array(Generators.positive_integer).sample(5, size: 10, rng: Random.new(42))
|
232
249
|
# => [[10, 5, 1, 4], [5, 9, 1, 1, 11, 8, 4, 9, 11, 10], [6], [11, 11, 2, 2, 7, 2, 6, 5, 5], [2, 10, 9, 7, 9, 5, 11, 3]]
|
233
|
-
|
234
|
-
|
235
|
-
|
250
|
+
#
|
251
|
+
# >> Generators.array(Generators.positive_integer, empty: true).sample(5, size: 1, rng: Random.new(1))
|
252
|
+
# => [[], [2], [], [], [2]]
|
253
|
+
# >> Generators.array(Generators.positive_integer, empty: false).sample(5, size: 1, rng: Random.new(1))
|
254
|
+
# => [[2], [1], [2], [1], [1]]
|
255
|
+
|
256
|
+
|
257
|
+
def array(element_generator, min: 0, max: nil, empty: true)
|
258
|
+
min = 1 if min.zero? && !empty
|
259
|
+
|
260
|
+
res = proc do |count|
|
261
|
+
count = min + 1 if count < min
|
262
|
+
count += 1 if count == min && min != 0
|
263
|
+
generators = (min...count).map do
|
236
264
|
element_generator.clone
|
237
265
|
end
|
238
266
|
|
239
267
|
tuple(*generators)
|
240
268
|
end
|
269
|
+
|
270
|
+
if max.nil?
|
271
|
+
nonnegative_integer.bind(&res)
|
272
|
+
else
|
273
|
+
res.call(max)
|
274
|
+
end
|
241
275
|
end
|
242
276
|
|
243
277
|
##
|
@@ -249,11 +283,22 @@ module PropCheck
|
|
249
283
|
#
|
250
284
|
# >> Generators.hash(Generators.printable_ascii_string, Generators.positive_integer).sample(5, size: 3, rng: Random.new(42))
|
251
285
|
# => [{""=>2, "g\\4"=>4, "rv"=>2}, {"7"=>2}, {"!"=>1, "E!"=>1}, {"kY5"=>2}, {}]
|
252
|
-
def hash(
|
253
|
-
|
254
|
-
|
286
|
+
def hash(*args, **kwargs)
|
287
|
+
if args.length == 2
|
288
|
+
hash_of(*args, **kwargs)
|
289
|
+
else
|
290
|
+
super
|
291
|
+
end
|
255
292
|
end
|
256
293
|
|
294
|
+
##
|
295
|
+
#
|
296
|
+
# Alias for `#hash` that does not conflict with a possibly overriden `Object#hash`.
|
297
|
+
#
|
298
|
+
def hash_of(key_generator, value_generator, **kwargs)
|
299
|
+
array(tuple(key_generator, value_generator), **kwargs)
|
300
|
+
.map(&:to_h)
|
301
|
+
end
|
257
302
|
|
258
303
|
@alphanumeric_chars = [('a'..'z'), ('A'..'Z'), ('0'..'9')].flat_map(&:to_a).freeze
|
259
304
|
##
|
@@ -276,8 +321,8 @@ module PropCheck
|
|
276
321
|
#
|
277
322
|
# >> Generators.alphanumeric_string.sample(5, size: 10, rng: Random.new(42))
|
278
323
|
# => ["ZCoQ", "8uM", "wkkx0JNx", "v0bxRDLb", "Gl5v8RyWA6"]
|
279
|
-
def alphanumeric_string
|
280
|
-
array(alphanumeric_char).map(&:join)
|
324
|
+
def alphanumeric_string(**kwargs)
|
325
|
+
array(alphanumeric_char, **kwargs).map(&:join)
|
281
326
|
end
|
282
327
|
|
283
328
|
@printable_ascii_chars = (' '..'~').to_a.freeze
|
@@ -302,8 +347,8 @@ module PropCheck
|
|
302
347
|
#
|
303
348
|
# >> Generators.printable_ascii_string.sample(5, size: 10, rng: Random.new(42))
|
304
349
|
# => ["S|.g", "rvjjw7\"5T!", "=", "!_[4@", "Y"]
|
305
|
-
def printable_ascii_string
|
306
|
-
array(printable_ascii_char).map(&:join)
|
350
|
+
def printable_ascii_string(**kwargs)
|
351
|
+
array(printable_ascii_char, **kwargs).map(&:join)
|
307
352
|
end
|
308
353
|
|
309
354
|
@ascii_chars = [
|
@@ -341,8 +386,8 @@ module PropCheck
|
|
341
386
|
#
|
342
387
|
# >> Generators.ascii_string.sample(5, size: 10, rng: Random.new(42))
|
343
388
|
# => ["S|.g", "drvjjw\b\a7\"", "!w=E!_[4@k", "x", "zZI{[o"]
|
344
|
-
def ascii_string
|
345
|
-
array(ascii_char).map(&:join)
|
389
|
+
def ascii_string(**kwargs)
|
390
|
+
array(ascii_char, **kwargs).map(&:join)
|
346
391
|
end
|
347
392
|
|
348
393
|
@printable_chars = [
|
@@ -372,8 +417,8 @@ module PropCheck
|
|
372
417
|
#
|
373
418
|
# >> Generators.printable_string.sample(5, size: 10, rng: Random.new(42))
|
374
419
|
# => ["", "Ȍ", "𐁂", "Ȕ", ""]
|
375
|
-
def printable_string
|
376
|
-
array(printable_char).map(&:join)
|
420
|
+
def printable_string(**kwargs)
|
421
|
+
array(printable_char, **kwargs).map(&:join)
|
377
422
|
end
|
378
423
|
|
379
424
|
##
|
@@ -398,8 +443,8 @@ module PropCheck
|
|
398
443
|
#
|
399
444
|
# >> Generators.string.sample(5, size: 10, rng: Random.new(42))
|
400
445
|
# => ["\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}"]
|
401
|
-
def string
|
402
|
-
array(char).map(&:join)
|
446
|
+
def string(**kwargs)
|
447
|
+
array(char, **kwargs).map(&:join)
|
403
448
|
end
|
404
449
|
|
405
450
|
##
|
@@ -483,5 +528,44 @@ module PropCheck
|
|
483
528
|
def nillable(other_generator)
|
484
529
|
frequency(9 => other_generator, 1 => constant(nil))
|
485
530
|
end
|
531
|
+
|
532
|
+
##
|
533
|
+
# Generates an instance of `klass`
|
534
|
+
# using `args` and/or `kwargs`
|
535
|
+
# as generators for the arguments that are passed to `klass.new`
|
536
|
+
#
|
537
|
+
# ## Example:
|
538
|
+
#
|
539
|
+
# Given a class like this:
|
540
|
+
#
|
541
|
+
#
|
542
|
+
# class User
|
543
|
+
# attr_accessor :name, :age
|
544
|
+
# def initialize(name: , age: )
|
545
|
+
# @name = name
|
546
|
+
# @age = age
|
547
|
+
# end
|
548
|
+
#
|
549
|
+
# def inspect
|
550
|
+
# "<User name: #{@name.inspect}, age: #{@age.inspect}>"
|
551
|
+
# end
|
552
|
+
# end
|
553
|
+
#
|
554
|
+
# >> user_gen = Generators.instance(User, name: Generators.printable_ascii_string, age: Generators.nonnegative_integer)
|
555
|
+
# >> user_gen.sample(3, rng: Random.new(42)).inspect
|
556
|
+
# => "[<User name: \"S|.g\", age: 10>, <User name: \"rvjj\", age: 10>, <User name: \"7\\\"5T!w=\", age: 5>]"
|
557
|
+
def instance(klass, *args, **kwargs)
|
558
|
+
tuple(*args).bind do |vals|
|
559
|
+
fixed_hash(**kwargs).map do |kwvals|
|
560
|
+
if kwvals == {}
|
561
|
+
klass.new(*vals)
|
562
|
+
elsif vals == []
|
563
|
+
klass.new(**kwvals)
|
564
|
+
else
|
565
|
+
klass.new(*vals, **kwvals)
|
566
|
+
end
|
567
|
+
end
|
568
|
+
end
|
569
|
+
end
|
486
570
|
end
|
487
571
|
end
|
data/lib/prop_check/helper.rb
CHANGED
@@ -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
|
data/lib/prop_check/lazy_tree.rb
CHANGED
@@ -7,12 +7,18 @@ module PropCheck
|
|
7
7
|
class LazyTree
|
8
8
|
require 'prop_check/helper'
|
9
9
|
|
10
|
-
|
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
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
# return res.each(&block) if block_given?
|
82
|
-
|
83
|
-
# res
|
77
|
+
squish([])
|
78
|
+
.each(&block)
|
79
|
+
end
|
84
80
|
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
data/lib/prop_check/property.rb
CHANGED
@@ -35,9 +35,9 @@ module PropCheck
|
|
35
35
|
# a Property object is returned, which you can call the other instance methods
|
36
36
|
# of this class on before finally passing a block to it using `#check`.
|
37
37
|
# (so `forall(Generators.integer) do |val| ... end` and forall(Generators.integer).check do |val| ... end` are the same)
|
38
|
-
def self.forall(*bindings, &block)
|
38
|
+
def self.forall(*bindings, **kwbindings, &block)
|
39
39
|
|
40
|
-
property = new(*bindings)
|
40
|
+
property = new(*bindings, **kwbindings)
|
41
41
|
|
42
42
|
return property.check(&block) if block_given?
|
43
43
|
|
@@ -66,8 +66,9 @@ module PropCheck
|
|
66
66
|
def initialize(*bindings, **kwbindings)
|
67
67
|
raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
|
68
68
|
|
69
|
-
@bindings = bindings
|
70
|
-
@kwbindings = kwbindings
|
69
|
+
# @bindings = bindings
|
70
|
+
# @kwbindings = kwbindings
|
71
|
+
@gen = gen_from_bindings(bindings, kwbindings)
|
71
72
|
@condition = proc { true }
|
72
73
|
@config = self.class.configuration
|
73
74
|
@hooks = PropCheck::Hooks.new
|
@@ -106,14 +107,17 @@ module PropCheck
|
|
106
107
|
# you might encounter a GeneratorExhaustedError.
|
107
108
|
# Only filter if you have few inputs to reject. Otherwise, improve your generators.
|
108
109
|
def where(&condition)
|
109
|
-
original_condition = @condition.dup
|
110
|
-
@condition = proc do
|
111
|
-
|
112
|
-
|
110
|
+
# original_condition = @condition.dup
|
111
|
+
# @condition = proc do |val|
|
112
|
+
# call_splatted(val, &original_condition) && call_splatted(val, &condition)
|
113
|
+
# # original_condition.call(val) && condition.call(val)
|
114
|
+
# end
|
115
|
+
@gen = @gen.where(&condition)
|
113
116
|
|
114
117
|
self
|
115
118
|
end
|
116
119
|
|
120
|
+
|
117
121
|
##
|
118
122
|
# Calls `hook` before each time a check is run with new data.
|
119
123
|
#
|
@@ -155,21 +159,11 @@ module PropCheck
|
|
155
159
|
##
|
156
160
|
# Checks the property (after settings have been altered using the other instance methods in this class.)
|
157
161
|
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
162
|
n_runs = 0
|
169
163
|
n_successful = 0
|
170
164
|
|
171
165
|
# Loop stops at first exception
|
172
|
-
attempts_enum(
|
166
|
+
attempts_enum(@gen).each do |generator_result|
|
173
167
|
n_runs += 1
|
174
168
|
check_attempt(generator_result, n_successful, &block)
|
175
169
|
n_successful += 1
|
@@ -178,6 +172,25 @@ module PropCheck
|
|
178
172
|
ensure_not_exhausted!(n_runs)
|
179
173
|
end
|
180
174
|
|
175
|
+
private def gen_from_bindings(bindings, kwbindings)
|
176
|
+
if bindings == [] && kwbindings != {}
|
177
|
+
PropCheck::Generators.fixed_hash(**kwbindings)
|
178
|
+
elsif bindings != [] && kwbindings == {}
|
179
|
+
if bindings.size == 1
|
180
|
+
bindings.first
|
181
|
+
else
|
182
|
+
PropCheck::Generators.tuple(*bindings)
|
183
|
+
end
|
184
|
+
else
|
185
|
+
raise ArgumentError,
|
186
|
+
'Attempted to use both normal and keyword bindings at the same time.
|
187
|
+
This is not supported because of the separation of positional and keyword arguments
|
188
|
+
(the old behaviour is deprecated in Ruby 2.7 and will be removed in 3.0)
|
189
|
+
c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
|
190
|
+
'
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
181
194
|
private def ensure_not_exhausted!(n_runs)
|
182
195
|
return if n_runs >= @config.n_runs
|
183
196
|
|
@@ -196,7 +209,7 @@ module PropCheck
|
|
196
209
|
end
|
197
210
|
|
198
211
|
private def check_attempt(generator_result, n_successful, &block)
|
199
|
-
|
212
|
+
PropCheck::Helper.call_splatted(generator_result.root, &block)
|
200
213
|
|
201
214
|
# immediately stop (without shrinnking) for when the app is asked
|
202
215
|
# to close by outside intervention
|
@@ -236,9 +249,7 @@ module PropCheck
|
|
236
249
|
size = 1
|
237
250
|
(0...@config.max_generate_attempts)
|
238
251
|
.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) }
|
252
|
+
.map { binding_generator.generate(size: size, rng: rng, max_consecutive_attempts: @config.max_consecutive_attempts) }
|
242
253
|
.map do |result|
|
243
254
|
size += 1
|
244
255
|
|
@@ -1,8 +1,20 @@
|
|
1
1
|
module PropCheck
|
2
2
|
class Property
|
3
|
-
Configuration = Struct.new(
|
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(
|
11
|
+
def initialize(
|
12
|
+
verbose: false,
|
13
|
+
n_runs: 1_000,
|
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
|
-
|
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
|
data/lib/prop_check/version.rb
CHANGED
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.
|
4
|
+
version: 0.13.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-
|
11
|
+
date: 2020-08-03 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.
|
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.
|