prop_check 0.7.1 → 0.8.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 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