prop_check 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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