prop_check 0.10.4 → 0.13.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 +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.
|