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.
@@ -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