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 +4 -4
- data/CHANGELOG.md +1 -0
- data/Gemfile.lock +1 -1
- data/README.md +64 -12
- data/lib/prop_check/generator.rb +20 -1
- data/lib/prop_check/lazy_tree.rb +4 -2
- data/lib/prop_check/property.rb +64 -30
- data/lib/prop_check/rspec.rb +9 -7
- data/lib/prop_check/version.rb +1 -1
- metadata +2 -3
- data/lib/prop_check/property/check_evaluator.rb +0 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71879bc6575991fe6582a3f98213ee01fb5c9230cf042f95769eea801c366b1f
|
4
|
+
data.tar.gz: 9e351641ffb936461634a871cb984efff16c3ff5e9de0e60d411d33385786ec4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 704ee74432f653b9993312d965ba1605ab366f67d564061a82522d8ac647b21689f47abdc836483dd82ffa4e3810882e33cccbd47c37e22cf8013d82147c2a20
|
7
|
+
data.tar.gz: 12535b39f7ff460bba0fb26bb3156f819e8d96ccf2ed7e2da28b6ab63472af3f42341a2c92e8fe437d5dccb56ce77ee7969e036c7484c833655ad5108e7fd3d9
|
data/CHANGELOG.md
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
- 0.8.0 New syntax that is more explicit, passng generated values to blocks as parameters.
|
data/Gemfile.lock
CHANGED
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
|
-
- [
|
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(
|
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
|
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
|
|
data/lib/prop_check/generator.rb
CHANGED
@@ -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
|
data/lib/prop_check/lazy_tree.rb
CHANGED
@@ -73,7 +73,8 @@ module PropCheck
|
|
73
73
|
[tree.root].lazy_append(new_children)
|
74
74
|
end
|
75
75
|
|
76
|
-
squish
|
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
|
103
|
+
each
|
104
|
+
.force
|
103
105
|
end
|
104
106
|
|
105
107
|
# TODO: fix implementation
|
data/lib/prop_check/property.rb
CHANGED
@@ -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
|
-
#
|
13
|
-
#
|
14
|
-
#
|
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(
|
20
|
-
def self.forall(
|
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(**
|
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
|
-
@
|
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 =
|
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
|
-
|
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
|
-
|
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|
|
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
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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(
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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, &
|
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
|
-
|
271
|
+
block.call(*sibling.root)
|
238
272
|
rescue Exception => e
|
239
273
|
problem_child = sibling
|
240
274
|
parent_siblings = siblings
|
data/lib/prop_check/rspec.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/prop_check/version.rb
CHANGED
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.
|
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-
|
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
|