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 +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
|