prop_check 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97011ef3b26afe799c5490e6de90bd9be9b0a97b1a028d2226dee5e333db692d
4
- data.tar.gz: 36c5308c6da7a232c407fb8034528cabb21ba3f9c5a19076bc7265eea98d9b35
3
+ metadata.gz: 71879bc6575991fe6582a3f98213ee01fb5c9230cf042f95769eea801c366b1f
4
+ data.tar.gz: 9e351641ffb936461634a871cb984efff16c3ff5e9de0e60d411d33385786ec4
5
5
  SHA512:
6
- metadata.gz: ac484a895221b1d8e7ab5a64c1282641526a310206e0ca5de46610b4d9fc02cf693c8af77a32ad026f610947f20609f3a09ea5833f7d97822dd958da536005c9
7
- data.tar.gz: 367b87d861eb5fcd36dc4ffb0698bad05c6a3805595cf4e1508e276e755241dc415ff5e6f080121608200a4f8766e7ad0992c610a4af963193e4644e62c2b625
6
+ metadata.gz: 704ee74432f653b9993312d965ba1605ab366f67d564061a82522d8ac647b21689f47abdc836483dd82ffa4e3810882e33cccbd47c37e22cf8013d82147c2a20
7
+ data.tar.gz: 12535b39f7ff460bba0fb26bb3156f819e8d96ccf2ed7e2da28b6ab63472af3f42341a2c92e8fe437d5dccb56ce77ee7969e036c7484c833655ad5108e7fd3d9
@@ -0,0 +1 @@
1
+ - 0.8.0 New syntax that is more explicit, passng generated values to blocks as parameters.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- prop_check (0.7.0)
4
+ prop_check (0.7.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -14,7 +14,7 @@ It features:
14
14
  - Shrinking to a minimal counter-example on failure.
15
15
 
16
16
 
17
- ## TODOs before release
17
+ ## TODOs before stable release
18
18
 
19
19
  Before releasing this gem on Rubygems, the following things need to be finished:
20
20
 
@@ -30,7 +30,7 @@ Before releasing this gem on Rubygems, the following things need to be finished:
30
30
 
31
31
  # Nice-to-haves
32
32
 
33
- - [ ] Basic integration with RSpec. See also https://groups.google.com/forum/#!msg/rspec/U-LmL0OnO-Y/iW_Jcd6JBAAJ for progress on this.
33
+ - [x] Basic integration with RSpec. See also https://groups.google.com/forum/#!msg/rspec/U-LmL0OnO-Y/iW_Jcd6JBAAJ for progress on this.
34
34
  - [ ] `aggregate` , `resize` and similar generator-modifying calls (c.f. PropEr's variants of these) which will help with introspection/metrics.
35
35
  - [ ] Integration with other Ruby test frameworks.
36
36
  - Stateful property testing. If implemented at some point, will probably happen in a separate add-on library.
@@ -66,8 +66,9 @@ _(to be precise: a method on the execution context is defined which returns the
66
66
  Raise an exception from the block if there is a problem. If there is no problem, just return normally.
67
67
 
68
68
  ```ruby
69
+ include PropCheck::Generators
69
70
  # testing that Enumerable#sort sorts in ascending order
70
- PropCheck.forall(numbers: array(integer())) do
71
+ PropCheck.forall(array(integer)) do |numbers|
71
72
  sorted_numbers = numbers.sort
72
73
 
73
74
  # Check that no number is smaller than the previous number
@@ -77,6 +78,50 @@ PropCheck.forall(numbers: array(integer())) do
77
78
  end
78
79
  ```
79
80
 
81
+
82
+ Here is another example, using it inside a test case.
83
+ Here we check if `naive_average` indeed always returns an integer for all arrays of numbers we can pass it:
84
+
85
+ ```ruby
86
+ # Somewhere you have this function definition:
87
+ def naive_average(array)
88
+ array.sum / array.length
89
+ end
90
+
91
+ # And then in a test case:
92
+ include PropCheck::Generators
93
+ PropCheck.forall(array(integer)) do |array|
94
+ result = naive_average(array)
95
+ unless result.is_a?(Integer) do
96
+ raise "Expected the average to be an integer!"
97
+ end
98
+ end
99
+ ```
100
+
101
+ When running this particular example PropCheck very quickly finds out that we have made a programming mistake:
102
+
103
+ ```ruby
104
+ ZeroDivisionError:
105
+ (after 6 successful property test runs)
106
+ Failed on:
107
+ `{
108
+ :array => []
109
+ }`
110
+
111
+ Exception message:
112
+ ---
113
+ divided by 0
114
+ ---
115
+
116
+ (shrinking impossible)
117
+ ---
118
+ ```
119
+
120
+ Clearly we forgot to handle the case of an empty array being passed to the function.
121
+ This is a good example of the kind of conceptual bugs that PropCheck (and property-based testing in general)
122
+ are able to check for.
123
+
124
+
80
125
  #### Shrinking
81
126
 
82
127
  When a failure is found, PropCheck will re-run the block given to `forall` to test
@@ -106,21 +151,28 @@ A short summary:
106
151
  - Arrays and hashes shrink to fewer elements, as well as shrinking their elements.
107
152
  - Strings shrink to shorter strings, as well as characters earlier in their alphabet.
108
153
 
154
+ ### Builtin Generators
155
+
156
+ PropCheck comes with [many builtin generators in the PropCheck::Generators](https://www.rubydoc.info/github/Qqwy/ruby-prop_check/master/PropCheck/Generators) module.
157
+
158
+ It contains generators for:
159
+ - (any, positive, negative, etc.) integers,
160
+ - (any, only real-valued) floats,
161
+ - (any, printable only, alphanumeric only, etc) strings and symbols
162
+ - fixed-size arrays and hashes
163
+ - as well as varying-size arrays and hashes.
164
+ - and many more!
165
+
166
+ It is common to call `include PropCheck::Generators` in e.g. your testing-suite files to be able to use these.
167
+ If you want to be more explicit (but somewhat more verbose) when calling these functions. feel free to e.g. create a module-alias (like `PG = PropCheck::Generators`) instead.
109
168
 
110
169
  ### Writing Custom Generators
111
170
 
112
- PropCheck comes bundled with a bunch of common generators, for:
113
- - integers
114
- - floats
115
- - strings
116
- - symbols
117
- - arrays
118
- - hashes
119
- etc.
171
+ As described in the previous section, PropCheck already comes bundled with a bunch of common generators.
120
172
 
121
173
  However, you can easily adapt them to generate your own datatypes:
122
174
 
123
- #### Generator#wrap
175
+ #### Generators#constant / Generator#wrap
124
176
 
125
177
  Always returns the given value. No shrinking.
126
178
 
@@ -72,7 +72,7 @@ module PropCheck
72
72
  # end.flatten
73
73
  # end
74
74
  Generator.new do |size, rng|
75
- outer_result = generate(size, rng)
75
+ outer_result = self.generate(size, rng)
76
76
  outer_result.bind do |outer_val|
77
77
  inner_generator = generator_proc.call(outer_val)
78
78
  inner_generator.generate(size, rng)
@@ -91,5 +91,24 @@ module PropCheck
91
91
  result.map(&proc)
92
92
  end
93
93
  end
94
+
95
+ ##
96
+ # Creates a new Generator that only produces a value when the block `condition` returns a truthy value.
97
+ def where(&condition)
98
+ self.map do |result|
99
+ if condition.call(*result)
100
+ result
101
+ else
102
+ :"_PropCheck.filter_me"
103
+ end
104
+ end
105
+ # self.map do |*result|
106
+ # if condition.call(*result)
107
+ # result
108
+ # else
109
+ # :'_PropCheck.filter_me'
110
+ # end
111
+ # end
112
+ end
94
113
  end
95
114
  end
@@ -73,7 +73,8 @@ module PropCheck
73
73
  [tree.root].lazy_append(new_children)
74
74
  end
75
75
 
76
- squish.call(self, [])
76
+ squish
77
+ .call(self, [])
77
78
 
78
79
  # base = [root]
79
80
  # recursive = children.map(&:each)
@@ -99,7 +100,8 @@ module PropCheck
99
100
  # >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).to_a
100
101
  # => [1, 4, 2, 3]
101
102
  def to_a
102
- each.force
103
+ each
104
+ .force
103
105
  end
104
106
 
105
107
  # TODO: fix implementation
@@ -2,24 +2,39 @@ require 'stringio'
2
2
  require "awesome_print"
3
3
 
4
4
  require 'prop_check/property/configuration'
5
- require 'prop_check/property/check_evaluator'
6
5
  module PropCheck
7
6
  ##
8
7
  # Run properties
9
8
  class Property
10
9
 
11
10
  ##
12
- # Call this with a keyword argument list of (symbol => generators) and a block.
13
- # The block will then be executed many times, with the respective symbol names
14
- # being defined as having a single generated value.
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
+ #
15
30
  #
16
31
  # If you do not pass a block right away,
17
32
  # a Property object is returned, which you can call the other instance methods
18
33
  # of this class on before finally passing a block to it using `#check`.
19
- # (so `forall(a: Generators.integer) do ... end` and forall(a: Generators.integer).check do ... end` are the same)
20
- def self.forall(**bindings, &block)
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)
21
36
 
22
- property = new(bindings)
37
+ property = new(*bindings)
23
38
 
24
39
  return property.check(&block) if block_given?
25
40
 
@@ -45,11 +60,12 @@ module PropCheck
45
60
 
46
61
  attr_reader :bindings, :condition
47
62
 
48
- def initialize(**bindings)
49
- raise ArgumentError, 'No bindings specified!' if bindings.empty?
63
+ def initialize(*bindings, **kwbindings)
64
+ raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
50
65
 
51
66
  @bindings = bindings
52
- @condition = -> { true }
67
+ @kwbindings = kwbindings
68
+ @condition = proc { true }
53
69
  @config = self.class.configuration
54
70
  end
55
71
 
@@ -87,7 +103,9 @@ module PropCheck
87
103
  # Only filter if you have few inputs to reject. Otherwise, improve your generators.
88
104
  def where(&condition)
89
105
  original_condition = @condition.dup
90
- @condition = -> { instance_exec(&original_condition) && instance_exec(&condition) }
106
+ @condition = proc do |**kwargs|
107
+ original_condition.call(**kwargs) && condition.call(**kwargs)
108
+ end
91
109
 
92
110
  self
93
111
  end
@@ -95,7 +113,15 @@ module PropCheck
95
113
  ##
96
114
  # Checks the property (after settings have been altered using the other instance methods in this class.)
97
115
  def check(&block)
98
- binding_generator = PropCheck::Generators.fixed_hash(bindings)
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)
99
125
 
100
126
  n_runs = 0
101
127
  n_successful = 0
@@ -113,6 +139,10 @@ module PropCheck
113
139
  private def ensure_not_exhausted!(n_runs)
114
140
  return if n_runs >= @config.n_runs
115
141
 
142
+ raise_generator_exhausted!
143
+ end
144
+
145
+ private def raise_generator_exhausted!()
116
146
  raise Errors::GeneratorExhaustedError, """
117
147
  Could not perform `n_runs = #{@config.n_runs}` runs,
118
148
  (exhausted #{@config.max_generate_attempts} tries)
@@ -124,7 +154,7 @@ module PropCheck
124
154
  end
125
155
 
126
156
  private def check_attempt(generator_result, n_successful, &block)
127
- CheckEvaluator.new(generator_result.root, &block).call
157
+ block.call(*generator_result.root)
128
158
 
129
159
  # immediately stop (without shrinnking) for when the app is asked
130
160
  # to close by outside intervention
@@ -160,8 +190,8 @@ module PropCheck
160
190
  (0...@config.max_generate_attempts)
161
191
  .lazy
162
192
  .map { binding_generator.generate(size, rng) }
163
- .reject { |val| val.root == :"_PropCheck.filter_me" }
164
- .select { |val| CheckEvaluator.new(val.root, &@condition).call }
193
+ .reject { |val| val.root.any? { |elem| elem == :"_PropCheck.filter_me" }}
194
+ .select { |val| @condition.call(*val.root) }
165
195
  .map do |result|
166
196
  n_runs += 1
167
197
  size += 1
@@ -194,25 +224,29 @@ module PropCheck
194
224
  end
195
225
 
196
226
  private def post_output(output, n_shrink_steps, shrunken_result, shrunken_exception)
197
- output.puts ''
198
- output.puts "Shrunken input (after #{n_shrink_steps} shrink steps):"
199
- output.puts "`#{print_roots(shrunken_result)}`"
200
- output.puts ""
201
- output.puts "Shrunken exception:\n---\n#{shrunken_exception}"
202
- output.puts "---"
203
- output.puts ""
204
-
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
205
238
  output
206
239
  end
207
240
 
208
- private def print_roots(lazy_tree_hash)
209
- # lazy_tree_hash.map do |name, val|
210
- # "#{name} = #{val.inspect}"
211
- # end.join(", ")
212
- lazy_tree_hash.ai
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
213
247
  end
214
248
 
215
- private def shrink(bindings_tree, io, &fun)
249
+ private def shrink(bindings_tree, io, &block)
216
250
  io.puts 'Shrinking...' if @config.verbose
217
251
  problem_child = bindings_tree
218
252
  siblings = problem_child.children.lazy
@@ -234,7 +268,7 @@ module PropCheck
234
268
  io.print '.' if @config.verbose
235
269
 
236
270
  begin
237
- CheckEvaluator.new(sibling.root, &fun).call
271
+ block.call(*sibling.root)
238
272
  rescue Exception => e
239
273
  problem_child = sibling
240
274
  parent_siblings = siblings
@@ -1,17 +1,19 @@
1
1
  module PropCheck
2
2
  ##
3
3
  # Integration with RSpec
4
+ #
5
+ # Currently very basic; it does two things:
6
+ # 1. adds the local `forall` method to examples that calls `PropCheck.forall`
7
+ # 2. adds `include PropCheck::Generators` statement.
4
8
  module RSpec
5
9
  # To make it available within examples
6
10
  def self.extend_object(obj)
11
+ obj.instance_eval do
12
+ include PropCheck::Generators
13
+ end
14
+
7
15
  obj.define_method(:forall) do |*args, **kwargs, &block|
8
- if block_given?
9
- PropCheck::Property.forall(*args, **kwargs) do
10
- instance_exec(self, &block)
11
- end
12
- else
13
- PropCheck::Property.forall(*args, **kwargs)
14
- end
16
+ PropCheck.forall(*args, **kwargs, &block)
15
17
  end
16
18
  end
17
19
  end
@@ -1,3 +1,3 @@
1
1
  module PropCheck
2
- VERSION = "0.7.1"
2
+ VERSION = "0.8.0"
3
3
  end
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.7.1
4
+ version: 0.8.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-06-19 00:00:00.000000000 Z
11
+ date: 2020-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -84,7 +84,6 @@ files:
84
84
  - lib/prop_check/helper/lazy_append.rb
85
85
  - lib/prop_check/lazy_tree.rb
86
86
  - lib/prop_check/property.rb
87
- - lib/prop_check/property/check_evaluator.rb
88
87
  - lib/prop_check/property/configuration.rb
89
88
  - lib/prop_check/rspec.rb
90
89
  - lib/prop_check/version.rb
@@ -1,45 +0,0 @@
1
- module PropCheck
2
- class Property
3
- ##
4
- # A wrapper class that implements the 'Cloaker' concept
5
- # which allows us to refer to variables set in 'bindings',
6
- # while still being able to access things that are only in scope
7
- # in the creator of '&block'.
8
- #
9
- # This allows us to bind the variables specified in `bindings`
10
- # one way during checking and another way during shrinking.
11
- class CheckEvaluator
12
- include RSpec::Matchers if Object.const_defined?('RSpec')
13
-
14
- def initialize(bindings, &block)
15
- @caller = block.binding.receiver
16
- @block = block
17
- define_named_instance_methods(bindings)
18
- end
19
-
20
- def call
21
- self.instance_exec(&@block)
22
- end
23
-
24
- private def define_named_instance_methods(results)
25
- results.each do |name, result|
26
- define_singleton_method(name) { result }
27
- end
28
- end
29
-
30
- ##
31
- # Dispatches to caller whenever something is not part of `bindings`.
32
- # (No need to invoke this method manually)
33
- def method_missing(method, *args, &block)
34
- @caller.__send__(method, *args, &block) || super
35
- end
36
-
37
- ##
38
- # Checks respond_to of caller whenever something is not part of `bindings`.
39
- # (No need to invoke this method manually)
40
- def respond_to_missing?(*args)
41
- @caller.respond_to?(*args) || super
42
- end
43
- end
44
- end
45
- end