prop_check 0.11.0 → 0.11.1
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/lib/prop_check/generator.rb +23 -27
- data/lib/prop_check/generators.rb +22 -11
- data/lib/prop_check/helper.rb +14 -0
- data/lib/prop_check/lazy_tree.rb +9 -17
- 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 +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '099a3bafe1fae426f476e3add7fb93ab333506670c72c35bb78e5e505b9f0764'
|
4
|
+
data.tar.gz: d1c433ef97043f38c84a5bf8768fb46aa5fcc97f00518da831fbebc36142d2e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 782411f48a77c4bc2213d6ecaecd70d7d7c3609c13bab2125ce853dedd7d3029fd8801ac882154d8c924cea212698740d8e330f8e2b90f460a5a87484413e560
|
7
|
+
data.tar.gz: dd18135801b66e2f073e82066506f8bb26cf031a4bd8d2fc208864acd7ef3a7ec564f8364628d34e4ab40429a5444a1dbc41f329bb4c7d3a608011407bb0a923
|
data/.tool-versions
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby 2.
|
1
|
+
ruby 2.7.1
|
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,12 +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
|
-
(
|
26
|
-
|
27
|
-
next if res == :"PropCheck.filter_me"
|
25
|
+
def generate(**kwargs)
|
26
|
+
kwargs = @@default_kwargs.merge(kwargs)
|
27
|
+
max_consecutive_attempts = kwargs[:max_consecutive_attempts]
|
28
28
|
|
29
|
-
|
29
|
+
(0..max_consecutive_attempts).each do
|
30
|
+
res = @block.call(**kwargs)
|
31
|
+
return res unless res.root == :"_PropCheck.filter_me"
|
30
32
|
end
|
31
33
|
|
32
34
|
raise Errors::GeneratorExhaustedError, """
|
@@ -40,18 +42,18 @@ module PropCheck
|
|
40
42
|
# Generates a value, and only return this value
|
41
43
|
# (drop information for shrinking)
|
42
44
|
#
|
43
|
-
# >> Generators.integer.call(1000, Random.new(42))
|
45
|
+
# >> Generators.integer.call(size: 1000, rng: Random.new(42))
|
44
46
|
# => 126
|
45
|
-
def call(
|
46
|
-
generate(
|
47
|
+
def call(**kwargs)
|
48
|
+
generate(**@@default_kwargs.merge(kwargs)).root
|
47
49
|
end
|
48
50
|
|
49
51
|
##
|
50
52
|
# Returns `num_of_samples` values from calling this Generator.
|
51
53
|
# This is mostly useful for debugging if a generator behaves as you intend it to.
|
52
|
-
def sample(num_of_samples = 10,
|
54
|
+
def sample(num_of_samples = 10, **kwargs)
|
53
55
|
num_of_samples.times.map do
|
54
|
-
call(
|
56
|
+
call(**@@default_kwargs.merge(kwargs))
|
55
57
|
end
|
56
58
|
end
|
57
59
|
|
@@ -61,10 +63,10 @@ module PropCheck
|
|
61
63
|
#
|
62
64
|
# Keen readers may notice this as the Monadic 'pure'/'return' implementation for Generators.
|
63
65
|
#
|
64
|
-
# >> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(100, Random.new(42))
|
66
|
+
# >> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(size: 100, rng: Random.new(42))
|
65
67
|
# => [2, 79]
|
66
68
|
def self.wrap(val)
|
67
|
-
Generator.new {
|
69
|
+
Generator.new { LazyTree.wrap(val) }
|
68
70
|
end
|
69
71
|
|
70
72
|
##
|
@@ -73,7 +75,7 @@ module PropCheck
|
|
73
75
|
#
|
74
76
|
# Keen readers may notice this as the Monadic 'bind' (sometimes known as '>>=') implementation for Generators.
|
75
77
|
#
|
76
|
-
# >> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(100, Random.new(42))
|
78
|
+
# >> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(size: 100, rng: Random.new(42))
|
77
79
|
# => [2, 79]
|
78
80
|
def bind(&generator_proc)
|
79
81
|
# Generator.new do |size, rng|
|
@@ -83,11 +85,11 @@ module PropCheck
|
|
83
85
|
# inner_generator.generate(size, rng)
|
84
86
|
# end.flatten
|
85
87
|
# end
|
86
|
-
Generator.new do |
|
87
|
-
outer_result = self.generate(
|
88
|
+
Generator.new do |**kwargs|
|
89
|
+
outer_result = self.generate(**kwargs)
|
88
90
|
outer_result.bind do |outer_val|
|
89
91
|
inner_generator = generator_proc.call(outer_val)
|
90
|
-
inner_generator.generate(
|
92
|
+
inner_generator.generate(**kwargs)
|
91
93
|
end
|
92
94
|
end
|
93
95
|
end
|
@@ -95,11 +97,11 @@ module PropCheck
|
|
95
97
|
##
|
96
98
|
# Creates a new Generator that returns a value by running `proc` on the output of the current Generator.
|
97
99
|
#
|
98
|
-
# >> Generators.choose(32..128).map(&:chr).call(10, Random.new(42))
|
100
|
+
# >> Generators.choose(32..128).map(&:chr).call(size: 10, rng: Random.new(42))
|
99
101
|
# => "S"
|
100
102
|
def map(&proc)
|
101
|
-
Generator.new do |
|
102
|
-
result = self.generate(
|
103
|
+
Generator.new do |**kwargs|
|
104
|
+
result = self.generate(**kwargs)
|
103
105
|
result.map(&proc)
|
104
106
|
end
|
105
107
|
end
|
@@ -108,19 +110,13 @@ module PropCheck
|
|
108
110
|
# Creates a new Generator that only produces a value when the block `condition` returns a truthy value.
|
109
111
|
def where(&condition)
|
110
112
|
self.map do |result|
|
111
|
-
if condition.call(result)
|
113
|
+
# if condition.call(*result)
|
114
|
+
if PropCheck::Helper.call_splatted(result, &condition)
|
112
115
|
result
|
113
116
|
else
|
114
117
|
:"_PropCheck.filter_me"
|
115
118
|
end
|
116
119
|
end
|
117
|
-
# self.map do |*result|
|
118
|
-
# if condition.call(*result)
|
119
|
-
# result
|
120
|
-
# else
|
121
|
-
# :'_PropCheck.filter_me'
|
122
|
-
# end
|
123
|
-
# end
|
124
120
|
end
|
125
121
|
end
|
126
122
|
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,14 +74,14 @@ 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:, **|
|
84
85
|
val = rng.rand(-size..size)
|
85
86
|
LazyTree.new(val, integer_shrink(val))
|
86
87
|
end
|
@@ -193,12 +194,12 @@ module PropCheck
|
|
193
194
|
#
|
194
195
|
# Shrinks element generators, one at a time (trying last one first).
|
195
196
|
#
|
196
|
-
# >> Generators.tuple(Generators.integer, Generators.real_float).call(10, Random.new(42))
|
197
|
+
# >> Generators.tuple(Generators.integer, Generators.real_float).call(size: 10, rng: Random.new(42))
|
197
198
|
# => [-4, 13.0]
|
198
199
|
def tuple(*generators)
|
199
|
-
Generator.new do |
|
200
|
+
Generator.new do |**kwargs|
|
200
201
|
LazyTree.zip(generators.map do |generator|
|
201
|
-
generator.generate(
|
202
|
+
generator.generate(**kwargs)
|
202
203
|
end)
|
203
204
|
end
|
204
205
|
end
|
@@ -210,7 +211,7 @@ module PropCheck
|
|
210
211
|
#
|
211
212
|
# Shrinks element generators.
|
212
213
|
#
|
213
|
-
# >> Generators.fixed_hash(a: Generators.integer(), b: Generators.real_float(), c: Generators.integer()).call(10, Random.new(42))
|
214
|
+
# >> Generators.fixed_hash(a: Generators.integer(), b: Generators.real_float(), c: Generators.integer()).call(size: 10, rng: Random.new(42))
|
214
215
|
# => {:a=>-4, :b=>13.0, :c=>-3}
|
215
216
|
def fixed_hash(hash)
|
216
217
|
keypair_generators =
|
@@ -265,7 +266,6 @@ module PropCheck
|
|
265
266
|
end
|
266
267
|
end
|
267
268
|
|
268
|
-
|
269
269
|
##
|
270
270
|
# Generates a hash of key->values,
|
271
271
|
# where each of the keys is made using the `key_generator`
|
@@ -275,12 +275,23 @@ module PropCheck
|
|
275
275
|
#
|
276
276
|
# >> Generators.hash(Generators.printable_ascii_string, Generators.positive_integer).sample(5, size: 3, rng: Random.new(42))
|
277
277
|
# => [{""=>2, "g\\4"=>4, "rv"=>2}, {"7"=>2}, {"!"=>1, "E!"=>1}, {"kY5"=>2}, {}]
|
278
|
-
def hash(
|
278
|
+
def hash(*args, **kwargs)
|
279
|
+
if args.length == 2
|
280
|
+
hash_of(*args, **kwargs)
|
281
|
+
else
|
282
|
+
super
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
##
|
287
|
+
#
|
288
|
+
# Alias for `#hash` that does not conflict with a possibly overriden `Object#hash`.
|
289
|
+
#
|
290
|
+
def hash_of(key_generator, value_generator, **kwargs)
|
279
291
|
array(tuple(key_generator, value_generator), **kwargs)
|
280
292
|
.map(&:to_h)
|
281
293
|
end
|
282
294
|
|
283
|
-
|
284
295
|
@alphanumeric_chars = [('a'..'z'), ('A'..'Z'), ('0'..'9')].flat_map(&:to_a).freeze
|
285
296
|
##
|
286
297
|
# Generates a single-character string
|
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,6 +7,8 @@ module PropCheck
|
|
7
7
|
class LazyTree
|
8
8
|
require 'prop_check/helper'
|
9
9
|
|
10
|
+
include Enumerable
|
11
|
+
|
10
12
|
attr_accessor :root, :children
|
11
13
|
def initialize(root, children = [].lazy)
|
12
14
|
@root = root
|
@@ -66,25 +68,15 @@ module PropCheck
|
|
66
68
|
# >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).each.force
|
67
69
|
# => [1, 4, 2, 3]
|
68
70
|
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, [])
|
71
|
+
self.to_enum(:each) unless block_given?
|
76
72
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
# return res.each(&block) if block_given?
|
82
|
-
|
83
|
-
# res
|
73
|
+
squish([])
|
74
|
+
.each(&block)
|
75
|
+
end
|
84
76
|
|
85
|
-
|
86
|
-
|
87
|
-
|
77
|
+
protected def squish(arr)
|
78
|
+
new_children = self.children.reduce(arr) { |acc, elem| elem.squish(acc) }
|
79
|
+
PropCheck::Helper.lazy_append([self.root], new_children)
|
88
80
|
end
|
89
81
|
|
90
82
|
##
|
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,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prop_check
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.11.
|
4
|
+
version: 0.11.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Qqwy/Wiebe-Marten Wijnja
|
@@ -83,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
83
|
- !ruby/object:Gem::Version
|
84
84
|
version: '0'
|
85
85
|
requirements: []
|
86
|
-
rubygems_version: 3.
|
86
|
+
rubygems_version: 3.1.2
|
87
87
|
signing_key:
|
88
88
|
specification_version: 4
|
89
89
|
summary: PropCheck allows you to do property-based testing, including shrinking.
|