prop_check 0.7.1 → 0.10.2

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: 88b54994bb83cb44c77b887dfc6839e19b467bddd8a32dd7d1fe91e624261799
4
+ data.tar.gz: e515d1522c939cb72eed02ed9fd98b458b1944a16b6c1f50f6c6bbc7076caaa5
5
5
  SHA512:
6
- metadata.gz: ac484a895221b1d8e7ab5a64c1282641526a310206e0ca5de46610b4d9fc02cf693c8af77a32ad026f610947f20609f3a09ea5833f7d97822dd958da536005c9
7
- data.tar.gz: 367b87d861eb5fcd36dc4ffb0698bad05c6a3805595cf4e1508e276e755241dc415ff5e6f080121608200a4f8766e7ad0992c610a4af963193e4644e62c2b625
6
+ metadata.gz: 3ba9d57ffa4e1d1f7d15b9c202cf232b5a931a57ac6d2d2bba4537030400e892d3154d2ab2db9ce97f6896bf97bb3a64a3cbdba02ac9cc15a561c815e7963aa6
7
+ data.tar.gz: 90a2fd3a15cf2ed27fc1eb1aea4273e75c39143df861bcf0a25b456c6373a088452a7d4b5e1b52aa52a0f61b1c94b0ceceef442bf3507793417d38ad91bcb2b3
data/.gitignore CHANGED
@@ -11,4 +11,5 @@
11
11
  .rspec_status
12
12
 
13
13
  # .gem version files
14
- *.gem
14
+ *.gem
15
+ Gemfile.lock
@@ -0,0 +1 @@
1
+ - 0.8.0 New syntax that is more explicit, passng generated values to blocks as parameters.
data/Gemfile CHANGED
@@ -3,6 +3,11 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in prop_check.gemspec
4
4
  gemspec
5
5
 
6
- gem 'simplecov', require: false, group: :test
7
- gem 'doctest-rspec', require: false, group: :test
8
- gem 'awesome_print', require: true
6
+ gem "bundler", "~> 2.0"
7
+
8
+ group :test do
9
+ gem "rake", "~> 12.3", require: false
10
+ gem "rspec", "~> 3.0", require: false
11
+ gem "doctest-rspec", require: false
12
+ gem "simplecov", require: false
13
+ end
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
 
@@ -27,10 +27,11 @@ Before releasing this gem on Rubygems, the following things need to be finished:
27
27
  - [x] Look into customization of settings from e.g. command line arguments.
28
28
  - [x] Good, unicode-compliant, string generators.
29
29
  - [x] Filtering generator outputs.
30
+ - [x] Before/after/around hooks to add setup/teardown logic to be called before/after/around each time a check is run with new data.
30
31
 
31
32
  # Nice-to-haves
32
33
 
33
- - [ ] Basic integration with RSpec. See also https://groups.google.com/forum/#!msg/rspec/U-LmL0OnO-Y/iW_Jcd6JBAAJ for progress on this.
34
+ - [x] Basic integration with RSpec. See also https://groups.google.com/forum/#!msg/rspec/U-LmL0OnO-Y/iW_Jcd6JBAAJ for progress on this.
34
35
  - [ ] `aggregate` , `resize` and similar generator-modifying calls (c.f. PropEr's variants of these) which will help with introspection/metrics.
35
36
  - [ ] Integration with other Ruby test frameworks.
36
37
  - Stateful property testing. If implemented at some point, will probably happen in a separate add-on library.
@@ -66,8 +67,9 @@ _(to be precise: a method on the execution context is defined which returns the
66
67
  Raise an exception from the block if there is a problem. If there is no problem, just return normally.
67
68
 
68
69
  ```ruby
70
+ include PropCheck::Generators
69
71
  # testing that Enumerable#sort sorts in ascending order
70
- PropCheck.forall(numbers: array(integer())) do
72
+ PropCheck.forall(array(integer)) do |numbers|
71
73
  sorted_numbers = numbers.sort
72
74
 
73
75
  # Check that no number is smaller than the previous number
@@ -77,6 +79,64 @@ PropCheck.forall(numbers: array(integer())) do
77
79
  end
78
80
  ```
79
81
 
82
+
83
+ Here is another example, using it inside a test case.
84
+ Here we check if `naive_average` indeed always returns an integer for all arrays of numbers we can pass it:
85
+
86
+ ```ruby
87
+ # Somewhere you have this function definition:
88
+ def naive_average(array)
89
+ array.sum / array.length
90
+ end
91
+ ```
92
+ ```ruby
93
+ # And then in a test case:
94
+ include PropCheck::Generators
95
+ PropCheck.forall(numbers: array(integer)) do |numbers:|
96
+ result = naive_average(numbers)
97
+ unless result.is_a?(Integer) do
98
+ raise "Expected the average to be an integer!"
99
+ end
100
+ end
101
+
102
+ # Or if you e.g. are using RSpec:
103
+ describe "#naive_average" do
104
+ include PropCheck
105
+ include PropCheck::Generators
106
+
107
+ it "returns an integer for any input" do
108
+ forall(numbers: array(integer)) do |numbers:|
109
+ result = naive_average(numbers)
110
+ expect(result).to be_a(Integer)
111
+ end
112
+ end
113
+ end
114
+ ```
115
+
116
+ When running this particular example PropCheck very quickly finds out that we have made a programming mistake:
117
+
118
+ ```ruby
119
+ ZeroDivisionError:
120
+ (after 6 successful property test runs)
121
+ Failed on:
122
+ `{
123
+ :numbers => []
124
+ }`
125
+
126
+ Exception message:
127
+ ---
128
+ divided by 0
129
+ ---
130
+
131
+ (shrinking impossible)
132
+ ---
133
+ ```
134
+
135
+ Clearly we forgot to handle the case of an empty array being passed to the function.
136
+ This is a good example of the kind of conceptual bugs that PropCheck (and property-based testing in general)
137
+ are able to check for.
138
+
139
+
80
140
  #### Shrinking
81
141
 
82
142
  When a failure is found, PropCheck will re-run the block given to `forall` to test
@@ -88,8 +148,8 @@ PropCheck will see if the failure still happens with `x = 50`.
88
148
  If it does , it will try `x = 25`. If not, it will try `x = 75`, and so on.
89
149
 
90
150
  This means if something only goes wrong for `x = 2`, the program will try:
91
- - `x = 100`(fails),`
92
- - x = 50`(fails),
151
+ - `x = 100`(fails),
152
+ - `x = 50`(fails),
93
153
  - `x = 25`(fails),
94
154
  - `x = 12`(fails),
95
155
  - `x = 6`(fails),
@@ -106,21 +166,28 @@ A short summary:
106
166
  - Arrays and hashes shrink to fewer elements, as well as shrinking their elements.
107
167
  - Strings shrink to shorter strings, as well as characters earlier in their alphabet.
108
168
 
169
+ ### Builtin Generators
170
+
171
+ PropCheck comes with [many builtin generators in the PropCheck::Generators](https://www.rubydoc.info/github/Qqwy/ruby-prop_check/master/PropCheck/Generators) module.
172
+
173
+ It contains generators for:
174
+ - (any, positive, negative, etc.) integers,
175
+ - (any, only real-valued) floats,
176
+ - (any, printable only, alphanumeric only, etc) strings and symbols
177
+ - fixed-size arrays and hashes
178
+ - as well as varying-size arrays and hashes.
179
+ - and many more!
180
+
181
+ It is common to call `include PropCheck::Generators` in e.g. your testing-suite files to be able to use these.
182
+ 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
183
 
110
184
  ### Writing Custom Generators
111
185
 
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.
186
+ As described in the previous section, PropCheck already comes bundled with a bunch of common generators.
120
187
 
121
188
  However, you can easily adapt them to generate your own datatypes:
122
189
 
123
- #### Generator#wrap
190
+ #### Generators#constant / Generator#wrap
124
191
 
125
192
  Always returns the given value. No shrinking.
126
193
 
@@ -9,6 +9,13 @@ require 'prop_check/helper'
9
9
  # You probably want to look at the documentation of
10
10
  # PropCheck::Generator and PropCheck::Generators
11
11
  # to find out more about how to use generators.
12
+ #
13
+ # Common usage is to call `extend PropCheck` in your (testing) modules.
14
+ #
15
+ # This will:
16
+ # 1. Add the local method `forall` which will call `PropCheck.forall`
17
+ # 2. `include PropCheck::Generators`.
18
+ #
12
19
  module PropCheck
13
20
  module Errors
14
21
  class Error < StandardError; end
@@ -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
@@ -23,5 +23,13 @@ module PropCheck
23
23
  end
24
24
  end.lazy
25
25
  end
26
+
27
+ ##
28
+ # allow lazy appending of two (potentially lazy) enumerators:
29
+ # >> PropCheck::Helper::LazyAppend.lazy_append([1,2,3],[4,5.6]).to_a
30
+ # => [1,2,3,4,5,6]
31
+ def lazy_append(this_enumerator, other_enumerator)
32
+ [this_enumerator, other_enumerator].lazy.flat_map(&:lazy)
33
+ end
26
34
  end
27
35
  end
@@ -1,18 +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
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,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # @api private
5
+ # Contains the logic to combine potentially many before/after/around hooks
6
+ # into a single pair of procedures called `before` and `after`.
7
+ #
8
+ # _Note: This module is an implementation detail of PropCheck._
9
+ #
10
+ # These can be invoked by manually calling `#before` and `#after`.
11
+ # Important:
12
+ # - Always call first `#before` and then `#after`.
13
+ # This is required to make sure that `around` callbacks will work properly.
14
+ # - Make sure that if you call `#before`, to also call `#after`.
15
+ # It is thus highly recommended to call `#after` inside an `ensure`.
16
+ # This is to make sure that `around` callbacks indeed perform their proper cleanup.
17
+ #
18
+ # Alternatively, check out `PropCheck::Hooks::Enumerable` which allows
19
+ # wrapping the elements of an enumerable with hooks.
20
+ class PropCheck::Hooks
21
+ # attr_reader :before, :after, :around
22
+ def initialize()
23
+ @before = proc {}
24
+ @after = proc {}
25
+ @around = proc { |*args, &block| block.call(*args) }
26
+ end
27
+
28
+ def wrap_enum(enumerable)
29
+ PropCheck::Hooks::Enumerable.new(enumerable, self)
30
+ end
31
+
32
+
33
+ ##
34
+ # Wraps a block with all hooks that were configured this far.
35
+ #
36
+ # This means that whenever the block is called,
37
+ # the before/around/after hooks are called before/around/after it.
38
+ def wrap_block(&block)
39
+ proc { |*args| call(*args, &block) }
40
+ end
41
+
42
+ ##
43
+ # Wraps a block with all hooks that were configured this far,
44
+ # and immediately calls it using the given `*args`.
45
+ #
46
+ # See also #wrap_block
47
+ def call(*args, &block)
48
+ begin
49
+ @before.call()
50
+ @around.call do
51
+ block.call(*args)
52
+ end
53
+ ensure
54
+ @after.call()
55
+ end
56
+ end
57
+
58
+ ##
59
+ # Adds `hook` to the `before` proc.
60
+ # It is called after earlier-added `before` procs.
61
+ def add_before(&hook)
62
+ old_before = @before
63
+ @before = proc {
64
+ old_before.call
65
+ hook.call
66
+ }
67
+ end
68
+
69
+ ##
70
+ # Adds `hook` to the `after` proc.
71
+ # It is called before earlier-added `after` procs.
72
+ def add_after(&hook)
73
+ old_after = @after
74
+ @after = proc {
75
+ hook.call
76
+ old_after.call
77
+ }
78
+ end
79
+
80
+ ##
81
+ # Adds `hook` to the `around` proc.
82
+ # It is called _inside_ earlier-added `around` procs.
83
+ def add_around(&hook)
84
+ old_around = @around
85
+ @around = proc do |&block|
86
+ old_around.call do |*args|
87
+ hook.call(*args, &block)
88
+ end
89
+ end
90
+ end
91
+
92
+ ##
93
+ # @api private
94
+ # Wraps enumerable `inner` with a `PropCheck::Hooks` object
95
+ # such that the before/after/around hooks are called
96
+ # before/after/around each element that is fetched from `inner`.
97
+ #
98
+ # This is very helpful if you need to perform cleanup logic
99
+ # before/after/around e.g. data is generated or fetched.
100
+ #
101
+ # Note that whatever is after a `yield` in an `around` hook
102
+ # is not guaranteed to be called (for instance when a StopIteration is raised).
103
+ # Thus: make sure you use `ensure` to clean up resources.
104
+ class Enumerable
105
+ include ::Enumerable
106
+
107
+ def initialize(inner, hooks)
108
+ @inner = inner
109
+ @hooks = hooks
110
+ end
111
+
112
+ def each(&task)
113
+ return to_enum(:each) unless block_given?
114
+
115
+ enum = @inner.to_enum
116
+
117
+ wrapped_yielder = @hooks.wrap_block do
118
+ yield enum.next(&task)
119
+ end
120
+
121
+ loop(&wrapped_yielder)
122
+ end
123
+ end
124
+ end
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'prop_check/helper/lazy_append'
4
-
5
3
  module PropCheck
6
4
  ##
7
5
  # A Rose tree with the root being eager,
8
6
  # and the children computed lazily, on demand.
9
7
  class LazyTree
10
- using PropCheck::Helper::LazyAppend
8
+ require 'prop_check/helper'
11
9
 
12
10
  attr_accessor :root, :children
13
11
  def initialize(root, children = [].lazy)
@@ -38,7 +36,7 @@ module PropCheck
38
36
  # root_children = root_tree.children
39
37
  # flattened_children = children.map(&:flatten)
40
38
 
41
- # combined_children = root_children.lazy_append(flattened_children)
39
+ # combined_children = PropCheck::Helper.lazy_append(root_children, flattened_children)
42
40
 
43
41
  # LazyTree.new(root_root, combined_children)
44
42
  # end
@@ -53,7 +51,7 @@ module PropCheck
53
51
  inner_children = inner_tree.children
54
52
  mapped_children = children.map { |child| child.bind(&fun) }
55
53
 
56
- combined_children = inner_children.lazy_append(mapped_children)
54
+ combined_children = PropCheck::Helper.lazy_append(inner_children, mapped_children)
57
55
 
58
56
  LazyTree.new(inner_root, combined_children)
59
57
  end
@@ -70,14 +68,15 @@ module PropCheck
70
68
  def each(&block)
71
69
  squish = lambda do |tree, list|
72
70
  new_children = tree.children.reduce(list) { |acc, elem| squish.call(elem, acc) }
73
- [tree.root].lazy_append(new_children)
71
+ PropCheck::Helper.lazy_append([tree.root], new_children)
74
72
  end
75
73
 
76
- squish.call(self, [])
74
+ squish
75
+ .call(self, [])
77
76
 
78
77
  # base = [root]
79
78
  # recursive = children.map(&:each)
80
- # res = base.lazy_append(recursive)
79
+ # res = PropCheck::Helper.lazy_append(base, recursive)
81
80
 
82
81
  # return res.each(&block) if block_given?
83
82
 
@@ -99,7 +98,8 @@ module PropCheck
99
98
  # >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).to_a
100
99
  # => [1, 4, 2, 3]
101
100
  def to_a
102
- each.force
101
+ each
102
+ .force
103
103
  end
104
104
 
105
105
  # TODO: fix implementation
@@ -2,24 +2,42 @@ require 'stringio'
2
2
  require "awesome_print"
3
3
 
4
4
  require 'prop_check/property/configuration'
5
- require 'prop_check/property/check_evaluator'
5
+ require 'prop_check/property/output_formatter'
6
+ require 'prop_check/property/shrinker'
7
+ require 'prop_check/hooks'
6
8
  module PropCheck
7
9
  ##
8
10
  # Run properties
9
11
  class Property
10
12
 
11
13
  ##
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.
14
+ # Main entry-point to create (and possibly immediately run) a property-test.
15
+ #
16
+ # This method accepts a list of generators and a block.
17
+ # The block will then be executed many times, passing the values generated by the generators
18
+ # as respective arguments:
19
+ #
20
+ # ```
21
+ # include PropCheck::Generators
22
+ # PropCheck.forall(integer(), float()) { |x, y| ... }
23
+ # ```
24
+ #
25
+ # It is also possible (and recommended when having more than a few generators) to use a keyword-list
26
+ # of generators instead:
27
+ #
28
+ # ```
29
+ # include PropCheck::Generators
30
+ # PropCheck.forall(x: integer(), y: float()) { |x:, y:| ... }
31
+ # ```
32
+ #
15
33
  #
16
34
  # If you do not pass a block right away,
17
35
  # a Property object is returned, which you can call the other instance methods
18
36
  # 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)
37
+ # (so `forall(Generators.integer) do |val| ... end` and forall(Generators.integer).check do |val| ... end` are the same)
38
+ def self.forall(*bindings, &block)
21
39
 
22
- property = new(bindings)
40
+ property = new(*bindings)
23
41
 
24
42
  return property.check(&block) if block_given?
25
43
 
@@ -45,12 +63,14 @@ module PropCheck
45
63
 
46
64
  attr_reader :bindings, :condition
47
65
 
48
- def initialize(**bindings)
49
- raise ArgumentError, 'No bindings specified!' if bindings.empty?
66
+ def initialize(*bindings, **kwbindings)
67
+ raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
50
68
 
51
69
  @bindings = bindings
52
- @condition = -> { true }
70
+ @kwbindings = kwbindings
71
+ @condition = proc { true }
53
72
  @config = self.class.configuration
73
+ @hooks = PropCheck::Hooks.new
54
74
  end
55
75
 
56
76
  ##
@@ -87,21 +107,69 @@ module PropCheck
87
107
  # Only filter if you have few inputs to reject. Otherwise, improve your generators.
88
108
  def where(&condition)
89
109
  original_condition = @condition.dup
90
- @condition = -> { instance_exec(&original_condition) && instance_exec(&condition) }
110
+ @condition = proc do |*args|
111
+ original_condition.call(*args) && condition.call(*args)
112
+ end
91
113
 
92
114
  self
93
115
  end
94
116
 
117
+ ##
118
+ # Calls `hook` before each time a check is run with new data.
119
+ #
120
+ # This is useful to add setup logic
121
+ # When called multiple times, earlier-added hooks will be called _before_ `hook` is called.
122
+ def before(&hook)
123
+ @hooks.add_before(&hook)
124
+ self
125
+ end
126
+
127
+ ##
128
+ # Calls `hook` after each time a check is run with new data.
129
+ #
130
+ # This is useful to add teardown logic
131
+ # When called multiple times, earlier-added hooks will be called _after_ `hook` is called.
132
+ def after(&hook)
133
+ @hooks.add_after(&hook)
134
+ self
135
+ end
136
+
137
+ ##
138
+ # Calls `hook` around each time a check is run with new data.
139
+ #
140
+ # `hook` should `yield` to the passed block.
141
+ #
142
+ # When called multiple times, earlier-added hooks will be wrapped _around_ `hook`.
143
+ #
144
+ # Around hooks will be called after all `#before` hooks
145
+ # and before all `#after` hooks.
146
+ #
147
+ # Note that if the block passed to `hook` raises an exception,
148
+ # it is possible for the code after `yield` not to be called.
149
+ # So make sure that cleanup logic is wrapped with the `ensure` keyword.
150
+ def around(&hook)
151
+ @hooks.add_around(&hook)
152
+ self
153
+ end
154
+
95
155
  ##
96
156
  # Checks the property (after settings have been altered using the other instance methods in this class.)
97
157
  def check(&block)
98
- binding_generator = PropCheck::Generators.fixed_hash(bindings)
158
+ gens =
159
+ if @kwbindings != {}
160
+ kwbinding_generator = PropCheck::Generators.fixed_hash(**@kwbindings)
161
+ @bindings + [kwbinding_generator]
162
+ else
163
+ @bindings
164
+ end
165
+ binding_generator = PropCheck::Generators.tuple(*gens)
166
+ # binding_generator = PropCheck::Generators.fixed_hash(**@kwbindings)
99
167
 
100
168
  n_runs = 0
101
169
  n_successful = 0
102
170
 
103
171
  # Loop stops at first exception
104
- attempts_enumerator(binding_generator).each do |generator_result|
172
+ attempts_enum(binding_generator).each do |generator_result|
105
173
  n_runs += 1
106
174
  check_attempt(generator_result, n_successful, &block)
107
175
  n_successful += 1
@@ -113,6 +181,10 @@ module PropCheck
113
181
  private def ensure_not_exhausted!(n_runs)
114
182
  return if n_runs >= @config.n_runs
115
183
 
184
+ raise_generator_exhausted!
185
+ end
186
+
187
+ private def raise_generator_exhausted!()
116
188
  raise Errors::GeneratorExhaustedError, """
117
189
  Could not perform `n_runs = #{@config.n_runs}` runs,
118
190
  (exhausted #{@config.max_generate_attempts} tries)
@@ -124,7 +196,7 @@ module PropCheck
124
196
  end
125
197
 
126
198
  private def check_attempt(generator_result, n_successful, &block)
127
- CheckEvaluator.new(generator_result.root, &block).call
199
+ block.call(*generator_result.root)
128
200
 
129
201
  # immediately stop (without shrinnking) for when the app is asked
130
202
  # to close by outside intervention
@@ -152,100 +224,39 @@ module PropCheck
152
224
  raise e, output_string, e.backtrace
153
225
  end
154
226
 
155
- private def attempts_enumerator(binding_generator)
227
+ private def attempts_enum(binding_generator)
228
+ @hooks
229
+ .wrap_enum(raw_attempts_enum(binding_generator))
230
+ .lazy
231
+ .take(@config.n_runs)
232
+ end
156
233
 
234
+ private def raw_attempts_enum(binding_generator)
157
235
  rng = Random::DEFAULT
158
- n_runs = 0
159
236
  size = 1
160
237
  (0...@config.max_generate_attempts)
161
238
  .lazy
162
239
  .map { binding_generator.generate(size, rng) }
163
- .reject { |val| val.root == :"_PropCheck.filter_me" }
164
- .select { |val| CheckEvaluator.new(val.root, &@condition).call }
240
+ .reject { |val| val.root.any? { |elem| elem == :"_PropCheck.filter_me" }}
241
+ .select { |val| @condition.call(*val.root) }
165
242
  .map do |result|
166
- n_runs += 1
167
243
  size += 1
168
244
 
169
245
  result
170
246
  end
171
- .take_while { n_runs <= @config.n_runs }
172
247
  end
173
248
 
174
249
  private def show_problem_output(problem, generator_results, n_successful, &block)
175
250
  output = @config.verbose ? STDOUT : StringIO.new
176
- output = pre_output(output, n_successful, generator_results.root, problem)
251
+ output = PropCheck::Property::OutputFormatter.pre_output(output, n_successful, generator_results.root, problem)
177
252
  shrunken_result, shrunken_exception, n_shrink_steps = shrink(generator_results, output, &block)
178
- output = post_output(output, n_shrink_steps, shrunken_result, shrunken_exception)
253
+ output = PropCheck::Property::OutputFormatter.post_output(output, n_shrink_steps, shrunken_result, shrunken_exception)
179
254
 
180
255
  [output, shrunken_result, shrunken_exception, n_shrink_steps]
181
256
  end
182
257
 
183
- private def pre_output(output, n_successful, generated_root, problem)
184
- output.puts ""
185
- output.puts "(after #{n_successful} successful property test runs)"
186
- output.puts "Failed on: "
187
- output.puts "`#{print_roots(generated_root)}`"
188
- output.puts ""
189
- output.puts "Exception message:\n---\n#{problem}"
190
- output.puts "---"
191
- output.puts ""
192
-
193
- output
194
- end
195
-
196
- 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
-
205
- output
206
- end
207
-
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
213
- end
214
-
215
- private def shrink(bindings_tree, io, &fun)
216
- io.puts 'Shrinking...' if @config.verbose
217
- problem_child = bindings_tree
218
- siblings = problem_child.children.lazy
219
- parent_siblings = nil
220
- problem_exception = nil
221
- shrink_steps = 0
222
- (0..@config.max_shrink_steps).each do
223
- begin
224
- sibling = siblings.next
225
- rescue StopIteration
226
- break if parent_siblings.nil?
227
-
228
- siblings = parent_siblings.lazy
229
- parent_siblings = nil
230
- next
231
- end
232
-
233
- shrink_steps += 1
234
- io.print '.' if @config.verbose
235
-
236
- begin
237
- CheckEvaluator.new(sibling.root, &fun).call
238
- rescue Exception => e
239
- problem_child = sibling
240
- parent_siblings = siblings
241
- siblings = problem_child.children.lazy
242
- problem_exception = e
243
- end
244
- end
245
-
246
- io.puts "(Note: Exceeded #{@config.max_shrink_steps} shrinking steps, the maximum.)" if shrink_steps >= @config.max_shrink_steps
247
-
248
- [problem_child.root, problem_exception, shrink_steps]
258
+ private def shrink(bindings_tree, io, &block)
259
+ PropCheck::Property::Shrinker.call(bindings_tree, io, @hooks, @config, &block)
249
260
  end
250
261
  end
251
262
  end
@@ -0,0 +1,41 @@
1
+ ##
2
+ # @api private
3
+ module PropCheck::Property::OutputFormatter
4
+ extend self
5
+
6
+ def pre_output(output, n_successful, generated_root, problem)
7
+ output.puts ""
8
+ output.puts "(after #{n_successful} successful property test runs)"
9
+ output.puts "Failed on: "
10
+ output.puts "`#{print_roots(generated_root)}`"
11
+ output.puts ""
12
+ output.puts "Exception message:\n---\n#{problem}"
13
+ output.puts "---"
14
+ output.puts ""
15
+
16
+ output
17
+ end
18
+
19
+ def post_output(output, n_shrink_steps, shrunken_result, shrunken_exception)
20
+ if n_shrink_steps == 0
21
+ output.puts '(shrinking impossible)'
22
+ else
23
+ output.puts ''
24
+ output.puts "Shrunken input (after #{n_shrink_steps} shrink steps):"
25
+ output.puts "`#{print_roots(shrunken_result)}`"
26
+ output.puts ""
27
+ output.puts "Shrunken exception:\n---\n#{shrunken_exception}"
28
+ output.puts "---"
29
+ output.puts ""
30
+ end
31
+ output
32
+ end
33
+
34
+ def print_roots(lazy_tree_val)
35
+ if lazy_tree_val.is_a?(Array) && lazy_tree_val.length == 1 && lazy_tree_val[0].is_a?(Hash)
36
+ lazy_tree_val[0].ai
37
+ else
38
+ lazy_tree_val.ai
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,79 @@
1
+ class PropCheck::Property::Shrinker
2
+ def initialize(bindings_tree, io, hooks, config)
3
+ @problem_child = bindings_tree
4
+ @io = io
5
+ @siblings = @problem_child.children.lazy
6
+ @parent_siblings = nil
7
+ @problem_exception = nil
8
+ @shrink_steps = 0
9
+ @hooks = hooks
10
+ @config = config
11
+ end
12
+
13
+ def self.call(bindings_tree, io, hooks, config, &block)
14
+ self
15
+ .new(bindings_tree, io, hooks, config)
16
+ .call(&block)
17
+ end
18
+
19
+ def call(&block)
20
+ @io.puts 'Shrinking...' if @config.verbose
21
+
22
+ shrink(&block)
23
+
24
+ print_shrinking_exceeded_message if @shrink_steps >= @config.max_shrink_steps
25
+
26
+ [@problem_child.root, @problem_exception, @shrink_steps]
27
+ end
28
+
29
+ private def shrink(&block)
30
+ wrapped_enum.each do
31
+ instruction, sibling = safe_read_sibling
32
+ break if instruction == :break
33
+ next if instruction == :next
34
+
35
+ inc_shrink_step
36
+
37
+ safe_call_block(sibling, &block)
38
+ end
39
+ end
40
+
41
+ private def wrapped_enum
42
+ @hooks.wrap_enum(0..@config.max_shrink_steps).lazy
43
+ end
44
+
45
+ private def inc_shrink_step
46
+ @shrink_steps += 1
47
+ @io.print '.' if @config.verbose
48
+ end
49
+
50
+ private def safe_read_sibling
51
+ begin
52
+ sibling = @siblings.next
53
+ [:continue, sibling]
54
+ rescue StopIteration
55
+ return [:break, nil] if @parent_siblings.nil?
56
+
57
+ @siblings = @parent_siblings.lazy
58
+ @parent_siblings = nil
59
+ [:next, nil]
60
+ end
61
+ end
62
+
63
+ private def safe_call_block(sibling, &block)
64
+ begin
65
+ block.call(*sibling.root)
66
+ # It is correct that we want to rescue _all_ Exceptions
67
+ # not only 'StandardError's
68
+ rescue Exception => e
69
+ @problem_child = sibling
70
+ @parent_siblings = @siblings
71
+ @siblings = @problem_child.children.lazy
72
+ @problem_exception = e
73
+ end
74
+ end
75
+
76
+ private def print_shrinking_exceeded_message
77
+ @io.puts "(Note: Exceeded #{@config.max_shrink_steps} shrinking steps, the maximum.)"
78
+ end
79
+ end
@@ -1,3 +1,3 @@
1
1
  module PropCheck
2
- VERSION = "0.7.1"
2
+ VERSION = '0.10.2'
3
3
  end
@@ -36,7 +36,5 @@ Gem::Specification.new do |spec|
36
36
 
37
37
  spec.required_ruby_version = '>= 2.5.1'
38
38
 
39
- spec.add_development_dependency "bundler", "~> 2.0"
40
- spec.add_development_dependency "rake", "~> 12.3"
41
- spec.add_development_dependency "rspec", "~> 3.0"
39
+ spec.add_dependency 'awesome_print', '~> 1.8'
42
40
  end
metadata CHANGED
@@ -1,57 +1,29 @@
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.10.2
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-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: awesome_print
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.0'
20
- type: :development
19
+ version: '1.8'
20
+ type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '2.0'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '12.3'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '12.3'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '3.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '3.0'
26
+ version: '1.8'
55
27
  description: PropCheck allows you to do property-based testing, including shrinking.
56
28
  (akin to Haskell's QuickCheck, Erlang's PropEr, Elixir's StreamData). This means
57
29
  that your test are run many times with different, autogenerated inputs, and as soon
@@ -71,7 +43,6 @@ files:
71
43
  - CHANGELOG.md
72
44
  - CODE_OF_CONDUCT.md
73
45
  - Gemfile
74
- - Gemfile.lock
75
46
  - LICENSE.txt
76
47
  - README.md
77
48
  - Rakefile
@@ -82,11 +53,12 @@ files:
82
53
  - lib/prop_check/generators.rb
83
54
  - lib/prop_check/helper.rb
84
55
  - lib/prop_check/helper/lazy_append.rb
56
+ - lib/prop_check/hooks.rb
85
57
  - lib/prop_check/lazy_tree.rb
86
58
  - lib/prop_check/property.rb
87
- - lib/prop_check/property/check_evaluator.rb
88
59
  - lib/prop_check/property/configuration.rb
89
- - lib/prop_check/rspec.rb
60
+ - lib/prop_check/property/output_formatter.rb
61
+ - lib/prop_check/property/shrinker.rb
90
62
  - lib/prop_check/version.rb
91
63
  - prop_check.gemspec
92
64
  homepage: https://github.com/Qqwy/ruby-prop_check/
@@ -1,50 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- prop_check (0.7.0)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- awesome_print (1.8.0)
10
- diff-lcs (1.3)
11
- docile (1.3.2)
12
- doctest-core (0.0.2)
13
- doctest-rspec (0.0.3)
14
- doctest-core (~> 0.0.2)
15
- rspec
16
- json (2.2.0)
17
- rake (12.3.3)
18
- rspec (3.8.0)
19
- rspec-core (~> 3.8.0)
20
- rspec-expectations (~> 3.8.0)
21
- rspec-mocks (~> 3.8.0)
22
- rspec-core (3.8.1)
23
- rspec-support (~> 3.8.0)
24
- rspec-expectations (3.8.4)
25
- diff-lcs (>= 1.2.0, < 2.0)
26
- rspec-support (~> 3.8.0)
27
- rspec-mocks (3.8.1)
28
- diff-lcs (>= 1.2.0, < 2.0)
29
- rspec-support (~> 3.8.0)
30
- rspec-support (3.8.2)
31
- simplecov (0.16.1)
32
- docile (~> 1.1)
33
- json (>= 1.8, < 3)
34
- simplecov-html (~> 0.10.0)
35
- simplecov-html (0.10.2)
36
-
37
- PLATFORMS
38
- ruby
39
-
40
- DEPENDENCIES
41
- awesome_print
42
- bundler (~> 2.0)
43
- doctest-rspec
44
- prop_check!
45
- rake (~> 12.3)
46
- rspec (~> 3.0)
47
- simplecov
48
-
49
- BUNDLED WITH
50
- 2.1.4
@@ -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
@@ -1,18 +0,0 @@
1
- module PropCheck
2
- ##
3
- # Integration with RSpec
4
- module RSpec
5
- # To make it available within examples
6
- def self.extend_object(obj)
7
- 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
15
- end
16
- end
17
- end
18
- end