prop_check 0.9.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 +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +4 -0
- data/.tool-versions +1 -0
- data/.travis.yml +18 -0
- data/CHANGELOG.md +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +50 -0
- data/LICENSE.txt +21 -0
- data/README.md +242 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/prop_check.rb +36 -0
- data/lib/prop_check/generator.rb +114 -0
- data/lib/prop_check/generators.rb +487 -0
- data/lib/prop_check/helper.rb +27 -0
- data/lib/prop_check/helper/lazy_append.rb +18 -0
- data/lib/prop_check/lazy_tree.rb +135 -0
- data/lib/prop_check/property.rb +285 -0
- data/lib/prop_check/property/configuration.rb +14 -0
- data/lib/prop_check/version.rb +3 -0
- data/prop_check.gemspec +42 -0
- metadata +116 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
module PropCheck
|
2
|
+
##
|
3
|
+
# Helper functions that have no other place to live
|
4
|
+
module Helper
|
5
|
+
extend self
|
6
|
+
##
|
7
|
+
# Creates a (potentially lazy) Enumerator
|
8
|
+
# starting with `elem`
|
9
|
+
# with each consecutive element obtained
|
10
|
+
# by calling `operation` on the previous element.
|
11
|
+
#
|
12
|
+
# >> Helper.scanl(0, &:next).take(10).force
|
13
|
+
# => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
14
|
+
# >> Helper.scanl([0, 1]) { |curr, next_elem| [next_elem, curr + next_elem] }.map(&:first).take(10).force
|
15
|
+
# => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
|
16
|
+
def scanl(elem, &operation)
|
17
|
+
Enumerator.new do |yielder|
|
18
|
+
acc = elem
|
19
|
+
loop do
|
20
|
+
# p acc
|
21
|
+
yielder << acc
|
22
|
+
acc = operation.call(acc)
|
23
|
+
end
|
24
|
+
end.lazy
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module PropCheck
|
2
|
+
module Helper
|
3
|
+
##
|
4
|
+
# A refinement for enumerators
|
5
|
+
# to allow lazy appending of two (potentially lazy) enumerators:
|
6
|
+
# >> [1,2,3].lazy_append([4,5.6]).to_a
|
7
|
+
# => [1,2,3,4,5,6]
|
8
|
+
module LazyAppend
|
9
|
+
refine Enumerable do
|
10
|
+
## >> [1,2,3].lazy_append([4,5.6]).to_a
|
11
|
+
## => [1,2,3,4,5,6]
|
12
|
+
def lazy_append(other_enumerator)
|
13
|
+
[self, other_enumerator].lazy.flat_map(&:lazy)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'prop_check/helper/lazy_append'
|
4
|
+
|
5
|
+
module PropCheck
|
6
|
+
##
|
7
|
+
# A Rose tree with the root being eager,
|
8
|
+
# and the children computed lazily, on demand.
|
9
|
+
class LazyTree
|
10
|
+
using PropCheck::Helper::LazyAppend
|
11
|
+
|
12
|
+
attr_accessor :root, :children
|
13
|
+
def initialize(root, children = [].lazy)
|
14
|
+
@root = root
|
15
|
+
@children = children
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Maps `block` eagerly over `root` and lazily over `children`, returning a new LazyTree as result.
|
20
|
+
#
|
21
|
+
# >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).map(&:next).to_a
|
22
|
+
# => LazyTree.new(2, [LazyTree.new(3, [LazyTree.new(4)]), LazyTree.new(5)]).to_a
|
23
|
+
def map(&block)
|
24
|
+
new_root = block.call(root)
|
25
|
+
new_children = children.map { |child_tree| child_tree.map(&block) }
|
26
|
+
LazyTree.new(new_root, new_children)
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Turns a tree of trees
|
31
|
+
# in a single flattened tree, with subtrees that are closer to the root
|
32
|
+
# and the left subtree earlier in the list of children.
|
33
|
+
# TODO: Check for correctness
|
34
|
+
# def flatten
|
35
|
+
# root_tree = root
|
36
|
+
# root_root = root_tree.root
|
37
|
+
|
38
|
+
# root_children = root_tree.children
|
39
|
+
# flattened_children = children.map(&:flatten)
|
40
|
+
|
41
|
+
# combined_children = root_children.lazy_append(flattened_children)
|
42
|
+
|
43
|
+
# LazyTree.new(root_root, combined_children)
|
44
|
+
# end
|
45
|
+
|
46
|
+
def self.wrap(val)
|
47
|
+
LazyTree.new(val)
|
48
|
+
end
|
49
|
+
|
50
|
+
def bind(&fun)
|
51
|
+
inner_tree = fun.call(root)
|
52
|
+
inner_root = inner_tree.root
|
53
|
+
inner_children = inner_tree.children
|
54
|
+
mapped_children = children.map { |child| child.bind(&fun) }
|
55
|
+
|
56
|
+
combined_children = inner_children.lazy_append(mapped_children)
|
57
|
+
|
58
|
+
LazyTree.new(inner_root, combined_children)
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Turns a LazyTree in a long lazy enumerable, with the root first followed by its children
|
63
|
+
# (and the first children's result before later children; i.e. a depth-first traversal.)
|
64
|
+
#
|
65
|
+
# Be aware that this lazy enumerable is potentially infinite,
|
66
|
+
# possibly uncountably so.
|
67
|
+
#
|
68
|
+
# >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).each.force
|
69
|
+
# => [1, 4, 2, 3]
|
70
|
+
def each(&block)
|
71
|
+
squish = lambda do |tree, list|
|
72
|
+
new_children = tree.children.reduce(list) { |acc, elem| squish.call(elem, acc) }
|
73
|
+
[tree.root].lazy_append(new_children)
|
74
|
+
end
|
75
|
+
|
76
|
+
squish
|
77
|
+
.call(self, [])
|
78
|
+
|
79
|
+
# base = [root]
|
80
|
+
# recursive = children.map(&:each)
|
81
|
+
# res = base.lazy_append(recursive)
|
82
|
+
|
83
|
+
# return res.each(&block) if block_given?
|
84
|
+
|
85
|
+
# res
|
86
|
+
|
87
|
+
# res = [[root], children.flat_map(&:each)].lazy.flat_map(&:lazy)
|
88
|
+
# res = res.map(&block) if block_given?
|
89
|
+
# res
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Fully evaluate the LazyTree into an eager array, with the root first followed by its children
|
94
|
+
# (and the first children's result before later children; i.e. a depth-first traversal.)
|
95
|
+
#
|
96
|
+
# Be aware that calling this might make Ruby attempt to evaluate an infinite collection.
|
97
|
+
# Therefore, it is mostly useful for debugging; in production you probably want to use
|
98
|
+
# the other mechanisms this class provides..
|
99
|
+
#
|
100
|
+
# >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).to_a
|
101
|
+
# => [1, 4, 2, 3]
|
102
|
+
def to_a
|
103
|
+
each
|
104
|
+
.force
|
105
|
+
end
|
106
|
+
|
107
|
+
# TODO: fix implementation
|
108
|
+
def self.zip(trees)
|
109
|
+
# p "TREES: "
|
110
|
+
# p trees.to_a
|
111
|
+
# p "END TREES"
|
112
|
+
# raise "Boom!" unless trees.to_a.is_a?(Array) && trees.to_a.first.is_a?(LazyTree)
|
113
|
+
# p self
|
114
|
+
new_root = trees.to_a.map(&:root)
|
115
|
+
# p new_root
|
116
|
+
# new_children = trees.permutations.flat_map(&:children)
|
117
|
+
new_children = permutations(trees).map { |children| LazyTree.zip(children) }
|
118
|
+
# p new_children
|
119
|
+
LazyTree.new(new_root, new_children)
|
120
|
+
end
|
121
|
+
|
122
|
+
private_class_method def self.permutations(trees)
|
123
|
+
# p trees
|
124
|
+
trees.lazy.each_with_index.flat_map do |tree, index|
|
125
|
+
tree.children.map do |child|
|
126
|
+
child_trees = trees.to_a.clone
|
127
|
+
child_trees[index] = child
|
128
|
+
# p "CHILD TREES:"
|
129
|
+
# p child_trees
|
130
|
+
child_trees.lazy
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,285 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require "awesome_print"
|
3
|
+
|
4
|
+
require 'prop_check/property/configuration'
|
5
|
+
module PropCheck
|
6
|
+
##
|
7
|
+
# Run properties
|
8
|
+
class Property
|
9
|
+
|
10
|
+
##
|
11
|
+
# Main entry-point to create (and possibly immediately run) a property-test.
|
12
|
+
#
|
13
|
+
# This method accepts a list of generators and a block.
|
14
|
+
# The block will then be executed many times, passing the values generated by the generators
|
15
|
+
# as respective arguments:
|
16
|
+
#
|
17
|
+
# ```
|
18
|
+
# include PropCheck::Generators
|
19
|
+
# PropCheck.forall(integer(), float()) { |x, y| ... }
|
20
|
+
# ```
|
21
|
+
#
|
22
|
+
# It is also possible (and recommended when having more than a few generators) to use a keyword-list
|
23
|
+
# of generators instead:
|
24
|
+
#
|
25
|
+
# ```
|
26
|
+
# include PropCheck::Generators
|
27
|
+
# PropCheck.forall(x: integer(), y: float()) { |x:, y:| ... }
|
28
|
+
# ```
|
29
|
+
#
|
30
|
+
#
|
31
|
+
# If you do not pass a block right away,
|
32
|
+
# a Property object is returned, which you can call the other instance methods
|
33
|
+
# of this class on before finally passing a block to it using `#check`.
|
34
|
+
# (so `forall(Generators.integer) do |val| ... end` and forall(Generators.integer).check do |val| ... end` are the same)
|
35
|
+
def self.forall(*bindings, &block)
|
36
|
+
|
37
|
+
property = new(*bindings)
|
38
|
+
|
39
|
+
return property.check(&block) if block_given?
|
40
|
+
|
41
|
+
property
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Returns the default configuration of the library as it is configured right now
|
46
|
+
# for introspection.
|
47
|
+
#
|
48
|
+
# For the configuration of a single property, check its `configuration` instance method.
|
49
|
+
# See PropCheck::Property::Configuration for more info on available settings.
|
50
|
+
def self.configuration
|
51
|
+
@configuration ||= Configuration.new
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Yields the library's configuration object for you to alter.
|
56
|
+
# See PropCheck::Property::Configuration for more info on available settings.
|
57
|
+
def self.configure
|
58
|
+
yield(configuration)
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_reader :bindings, :condition
|
62
|
+
|
63
|
+
def initialize(*bindings, **kwbindings)
|
64
|
+
raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
|
65
|
+
|
66
|
+
@bindings = bindings
|
67
|
+
@kwbindings = kwbindings
|
68
|
+
@condition = proc { true }
|
69
|
+
@config = self.class.configuration
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Returns the configuration of this property
|
74
|
+
# for introspection.
|
75
|
+
#
|
76
|
+
# See PropCheck::Property::Configuration for more info on available settings.
|
77
|
+
def configuration
|
78
|
+
@config
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Allows you to override the configuration of this property
|
83
|
+
# by giving a hash with new settings.
|
84
|
+
#
|
85
|
+
# If no other changes need to occur before you want to check the property,
|
86
|
+
# you can immediately pass a block to this method.
|
87
|
+
# (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`)
|
88
|
+
def with_config(**config, &block)
|
89
|
+
@config = @config.merge(config)
|
90
|
+
|
91
|
+
return self.check(&block) if block_given?
|
92
|
+
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# filters the generator using the given `condition`.
|
98
|
+
# The final property checking block will only be run if the condition is truthy.
|
99
|
+
#
|
100
|
+
# If wanted, multiple `where`-conditions can be specified on a property.
|
101
|
+
# Be aware that if you filter away too much generated inputs,
|
102
|
+
# you might encounter a GeneratorExhaustedError.
|
103
|
+
# Only filter if you have few inputs to reject. Otherwise, improve your generators.
|
104
|
+
def where(&condition)
|
105
|
+
original_condition = @condition.dup
|
106
|
+
@condition = proc do |**kwargs|
|
107
|
+
original_condition.call(**kwargs) && condition.call(**kwargs)
|
108
|
+
end
|
109
|
+
|
110
|
+
self
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Checks the property (after settings have been altered using the other instance methods in this class.)
|
115
|
+
def check(&block)
|
116
|
+
gens =
|
117
|
+
if @kwbindings != {}
|
118
|
+
kwbinding_generator = PropCheck::Generators.fixed_hash(**@kwbindings)
|
119
|
+
@bindings + [kwbinding_generator]
|
120
|
+
else
|
121
|
+
@bindings
|
122
|
+
end
|
123
|
+
binding_generator = PropCheck::Generators.tuple(*gens)
|
124
|
+
# binding_generator = PropCheck::Generators.fixed_hash(**@kwbindings)
|
125
|
+
|
126
|
+
n_runs = 0
|
127
|
+
n_successful = 0
|
128
|
+
|
129
|
+
# Loop stops at first exception
|
130
|
+
attempts_enumerator(binding_generator).each do |generator_result|
|
131
|
+
n_runs += 1
|
132
|
+
check_attempt(generator_result, n_successful, &block)
|
133
|
+
n_successful += 1
|
134
|
+
end
|
135
|
+
|
136
|
+
ensure_not_exhausted!(n_runs)
|
137
|
+
end
|
138
|
+
|
139
|
+
private def ensure_not_exhausted!(n_runs)
|
140
|
+
return if n_runs >= @config.n_runs
|
141
|
+
|
142
|
+
raise_generator_exhausted!
|
143
|
+
end
|
144
|
+
|
145
|
+
private def raise_generator_exhausted!()
|
146
|
+
raise Errors::GeneratorExhaustedError, """
|
147
|
+
Could not perform `n_runs = #{@config.n_runs}` runs,
|
148
|
+
(exhausted #{@config.max_generate_attempts} tries)
|
149
|
+
because too few generator results were adhering to
|
150
|
+
the `where` condition.
|
151
|
+
|
152
|
+
Try refining your generators instead.
|
153
|
+
"""
|
154
|
+
end
|
155
|
+
|
156
|
+
private def check_attempt(generator_result, n_successful, &block)
|
157
|
+
block.call(*generator_result.root)
|
158
|
+
|
159
|
+
# immediately stop (without shrinnking) for when the app is asked
|
160
|
+
# to close by outside intervention
|
161
|
+
rescue SignalException, SystemExit
|
162
|
+
raise
|
163
|
+
|
164
|
+
# We want to capture _all_ exceptions (even low-level ones) here,
|
165
|
+
# so we can shrink to find their cause.
|
166
|
+
# don't worry: they all get reraised
|
167
|
+
rescue Exception => e
|
168
|
+
output, shrunken_result, shrunken_exception, n_shrink_steps = show_problem_output(e, generator_result, n_successful, &block)
|
169
|
+
output_string = output.is_a?(StringIO) ? output.string : e.message
|
170
|
+
|
171
|
+
e.define_singleton_method :prop_check_info do
|
172
|
+
{
|
173
|
+
original_input: generator_result.root,
|
174
|
+
original_exception_message: e.message,
|
175
|
+
shrunken_input: shrunken_result,
|
176
|
+
shrunken_exception: shrunken_exception,
|
177
|
+
n_successful: n_successful,
|
178
|
+
n_shrink_steps: n_shrink_steps
|
179
|
+
}
|
180
|
+
end
|
181
|
+
|
182
|
+
raise e, output_string, e.backtrace
|
183
|
+
end
|
184
|
+
|
185
|
+
private def attempts_enumerator(binding_generator)
|
186
|
+
|
187
|
+
rng = Random::DEFAULT
|
188
|
+
n_runs = 0
|
189
|
+
size = 1
|
190
|
+
(0...@config.max_generate_attempts)
|
191
|
+
.lazy
|
192
|
+
.map { binding_generator.generate(size, rng) }
|
193
|
+
.reject { |val| val.root.any? { |elem| elem == :"_PropCheck.filter_me" }}
|
194
|
+
.select { |val| @condition.call(*val.root) }
|
195
|
+
.map do |result|
|
196
|
+
n_runs += 1
|
197
|
+
size += 1
|
198
|
+
|
199
|
+
result
|
200
|
+
end
|
201
|
+
.take_while { n_runs <= @config.n_runs }
|
202
|
+
end
|
203
|
+
|
204
|
+
private def show_problem_output(problem, generator_results, n_successful, &block)
|
205
|
+
output = @config.verbose ? STDOUT : StringIO.new
|
206
|
+
output = pre_output(output, n_successful, generator_results.root, problem)
|
207
|
+
shrunken_result, shrunken_exception, n_shrink_steps = shrink(generator_results, output, &block)
|
208
|
+
output = post_output(output, n_shrink_steps, shrunken_result, shrunken_exception)
|
209
|
+
|
210
|
+
[output, shrunken_result, shrunken_exception, n_shrink_steps]
|
211
|
+
end
|
212
|
+
|
213
|
+
private def pre_output(output, n_successful, generated_root, problem)
|
214
|
+
output.puts ""
|
215
|
+
output.puts "(after #{n_successful} successful property test runs)"
|
216
|
+
output.puts "Failed on: "
|
217
|
+
output.puts "`#{print_roots(generated_root)}`"
|
218
|
+
output.puts ""
|
219
|
+
output.puts "Exception message:\n---\n#{problem}"
|
220
|
+
output.puts "---"
|
221
|
+
output.puts ""
|
222
|
+
|
223
|
+
output
|
224
|
+
end
|
225
|
+
|
226
|
+
private def post_output(output, n_shrink_steps, shrunken_result, shrunken_exception)
|
227
|
+
if n_shrink_steps == 0
|
228
|
+
output.puts '(shrinking impossible)'
|
229
|
+
else
|
230
|
+
output.puts ''
|
231
|
+
output.puts "Shrunken input (after #{n_shrink_steps} shrink steps):"
|
232
|
+
output.puts "`#{print_roots(shrunken_result)}`"
|
233
|
+
output.puts ""
|
234
|
+
output.puts "Shrunken exception:\n---\n#{shrunken_exception}"
|
235
|
+
output.puts "---"
|
236
|
+
output.puts ""
|
237
|
+
end
|
238
|
+
output
|
239
|
+
end
|
240
|
+
|
241
|
+
private def print_roots(lazy_tree_val)
|
242
|
+
if lazy_tree_val.is_a?(Array) && lazy_tree_val.length == 1 && lazy_tree_val[0].is_a?(Hash)
|
243
|
+
lazy_tree_val[0].ai
|
244
|
+
else
|
245
|
+
lazy_tree_val.ai
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
private def shrink(bindings_tree, io, &block)
|
250
|
+
io.puts 'Shrinking...' if @config.verbose
|
251
|
+
problem_child = bindings_tree
|
252
|
+
siblings = problem_child.children.lazy
|
253
|
+
parent_siblings = nil
|
254
|
+
problem_exception = nil
|
255
|
+
shrink_steps = 0
|
256
|
+
(0..@config.max_shrink_steps).each do
|
257
|
+
begin
|
258
|
+
sibling = siblings.next
|
259
|
+
rescue StopIteration
|
260
|
+
break if parent_siblings.nil?
|
261
|
+
|
262
|
+
siblings = parent_siblings.lazy
|
263
|
+
parent_siblings = nil
|
264
|
+
next
|
265
|
+
end
|
266
|
+
|
267
|
+
shrink_steps += 1
|
268
|
+
io.print '.' if @config.verbose
|
269
|
+
|
270
|
+
begin
|
271
|
+
block.call(*sibling.root)
|
272
|
+
rescue Exception => e
|
273
|
+
problem_child = sibling
|
274
|
+
parent_siblings = siblings
|
275
|
+
siblings = problem_child.children.lazy
|
276
|
+
problem_exception = e
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
io.puts "(Note: Exceeded #{@config.max_shrink_steps} shrinking steps, the maximum.)" if shrink_steps >= @config.max_shrink_steps
|
281
|
+
|
282
|
+
[problem_child.root, problem_exception, shrink_steps]
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|