prop_check 0.14.1 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/run_tests.yaml +31 -0
- data/CHANGELOG.md +19 -0
- data/README.md +75 -32
- data/lib/prop_check/generator.rb +23 -7
- data/lib/prop_check/generators.rb +274 -28
- data/lib/prop_check/helper.rb +3 -11
- data/lib/prop_check/property/configuration.rb +34 -9
- data/lib/prop_check/property.rb +88 -15
- data/lib/prop_check/version.rb +1 -1
- metadata +4 -5
- data/.travis.yml +0 -18
- data/lib/prop_check/helper/lazy_append.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 912a84633a6ab5b7f209555be0086e4dcafd371f337b289113003edf765fb668
|
4
|
+
data.tar.gz: 92146dcaf693c20f4cb3021178e881495de8b63f3819c254aea186a3780891c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 192ed9c6887dc19f29fc3d8aba9278eeb0fd389aab9fae546c940e2831833ea8064ea53e1e903c599d7d7ec2a8fe5bdc04a2c3122c2374a2d1d933b2184a2ab0
|
7
|
+
data.tar.gz: 63005af74e5f0d1757930f64ce031f5cc922e34b4e876cdaebbbf139280e9e923d134440f20ab63129c4fd1647d1e524b07b6ed3800828b29d22ad7e2cc5e394
|
@@ -0,0 +1,31 @@
|
|
1
|
+
name: Ruby RSpec tests
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
|
9
|
+
permissions:
|
10
|
+
contents: read
|
11
|
+
|
12
|
+
jobs:
|
13
|
+
test:
|
14
|
+
|
15
|
+
runs-on: ubuntu-latest
|
16
|
+
strategy:
|
17
|
+
matrix:
|
18
|
+
ruby-version: ['2.5', '2.6', '2.7', '3.0']
|
19
|
+
|
20
|
+
steps:
|
21
|
+
- uses: actions/checkout@v3
|
22
|
+
- name: Set up Ruby
|
23
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
24
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
25
|
+
# uses: ruby/setup-ruby@v1
|
26
|
+
uses: ruby/setup-ruby@0a29871fe2b0200a17a4497bae54fe5df0d973aa # v1.115.3
|
27
|
+
with:
|
28
|
+
ruby-version: ${{ matrix.ruby-version }}
|
29
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
30
|
+
- name: Run tests
|
31
|
+
run: bundle exec rake
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
- 0.16.0
|
2
|
+
- Features:
|
3
|
+
- New option in `PropCheck::Property::Configuration` to resize all generators at once.
|
4
|
+
- Wrapper functions to modify this easily in `PropCheck::Property` called `#resize`, `#grow_fast`, `#grow_slowly`, `#grow_exponentially`, `#grow_quadratically`, `#grow_logarithmically`.
|
5
|
+
- 0.15.0
|
6
|
+
- Features:
|
7
|
+
- Generators for `Date`, `Time` and `DateTime`.
|
8
|
+
- Basic work done by @Haniyya. Thank you very much!
|
9
|
+
- Extra functions to generate dates/times/datetimes in the future or the past.
|
10
|
+
- Allow overriding the epoch that is used.
|
11
|
+
- A new option in `PropCheck::Property::Configuration` to set the default epoch.
|
12
|
+
- Generator to generate `Set`s.
|
13
|
+
- New builtin float generators (positive, negative, nonzero, nonnegative, nonpositive). Both in 'normal' flavor and in 'real' flavor (that will never generate infinity or other special values).
|
14
|
+
- `PropCheck::Generator#with_config` which enables the possibility to inspect and act on the current `PropCheck::Property::Configuration` while generating values.
|
15
|
+
- Fixes:
|
16
|
+
- Preserve backwards compatibility with Ruby 2.5 by not using infinite ranges internally (c.f. #8, thank you, @hlaf!)
|
17
|
+
- Make a flaky test deterministic by fixing the RNG. (c.f. #9, thank you, @hlaf!)
|
18
|
+
- Fix a crash when using a hash where not all keys are symbols. (c.f. #7, thank you, @Haniyya!)
|
19
|
+
- Fix situations in which `PropCheck::Generators.array` would for certain config values never generate empty arrays.
|
1
20
|
- 0.14.1 - Swap `awesome_print` for `amazing_print` which is a fork of the former that is actively maintained.
|
2
21
|
- 0.14.0 - Adds `uniq: true` option to `Generators.array`. Makes `PropCheck::Property` an immutable object that returns copies that have changes whenever reconfiguring, allowing re-usable configuration.
|
3
22
|
- 0.13.0 - Adds Generator#resize
|
data/README.md
CHANGED
@@ -3,22 +3,53 @@
|
|
3
3
|
PropCheck allows you to do Property Testing in Ruby.
|
4
4
|
|
5
5
|
[![Gem](https://img.shields.io/gem/v/prop_check.svg)](https://rubygems.org/gems/prop_check)
|
6
|
-
[![
|
6
|
+
[![Ruby RSpec tests build status](https://github.com/Qqwy/ruby-prop_check/actions/workflows/run_tests.yaml/badge.svg)](https://github.com/Qqwy/ruby-prop_check/actions/workflows/run_tests.yaml)
|
7
7
|
[![Maintainability](https://api.codeclimate.com/v1/badges/71897f5e6193a5124a53/maintainability)](https://codeclimate.com/github/Qqwy/ruby-prop_check/maintainability)
|
8
|
-
[![RubyDoc](https://img.shields.io/badge/%F0%9F%93%9ARubyDoc-documentation-informational.svg)](https://www.rubydoc.info/github/Qqwy/ruby-prop_check/master/
|
8
|
+
[![RubyDoc](https://img.shields.io/badge/%F0%9F%93%9ARubyDoc-documentation-informational.svg)](https://www.rubydoc.info/github/Qqwy/ruby-prop_check/master/)
|
9
9
|
|
10
10
|
It features:
|
11
11
|
|
12
|
-
- Generators for common datatypes.
|
13
|
-
- An easy DSL to define your own generators (by combining existing ones,
|
12
|
+
- Generators for most common Ruby datatypes.
|
13
|
+
- An easy DSL to define your own generators (by combining existing ones, as well as completely custom ones).
|
14
14
|
- Shrinking to a minimal counter-example on failure.
|
15
|
+
- Hooks to perform extra set-up/cleanup logic before/after every example case.
|
15
16
|
|
17
|
+
## What is PropCheck?
|
16
18
|
|
17
|
-
|
19
|
+
PropCheck is a Ruby library to create unit tests which are simpler to write and more powerful when run, finding edge-cases in your code you wouldn't have thought to look for.
|
18
20
|
|
19
|
-
|
21
|
+
It works by letting you write tests that assert that something should be true for _every_ case, rather than just the ones you happen to think of.
|
20
22
|
|
21
|
-
|
23
|
+
|
24
|
+
A normal unit test looks something like the following:
|
25
|
+
|
26
|
+
1. Set up some data.
|
27
|
+
2. Perform some operations on the data.
|
28
|
+
3. Assert something about the result
|
29
|
+
|
30
|
+
PropCheck lets you write tests which instead look like this:
|
31
|
+
|
32
|
+
1. For all data matching some specification.
|
33
|
+
2. Perform some operations on the data.
|
34
|
+
3. Assert something about the result.
|
35
|
+
|
36
|
+
This is often called property-based testing. It was popularised by the Haskell library [QuickCheck](https://hackage.haskell.org/package/QuickCheck).
|
37
|
+
PropCheck takes further inspiration from Erlang's [PropEr](https://hex.pm/packages/proper), Elixir's [StreamData](https://hex.pm/packages/stream_data) and Python's [Hypothesis](https://hypothesis.works/).
|
38
|
+
|
39
|
+
It works by generating arbitrary data matching your specification and checking that your assertions still hold in that case. If it finds an example where they do not, it takes that example and shrinks it down, simplifying it to find the smallest example that still causes the problem.
|
40
|
+
|
41
|
+
Writing these kinds of tests usually consists of deciding on guarantees that your code should have -- properties that should always hold true, regardless of wat the world throws at you. Some examples are:
|
42
|
+
|
43
|
+
- Your code should not throw an exception, or only a particular type of exception.
|
44
|
+
- If you remove an object, you can no longer see it
|
45
|
+
- If you serialize and then deserializea value, you get the same value back.
|
46
|
+
|
47
|
+
|
48
|
+
## Implemented and still missing features
|
49
|
+
|
50
|
+
Before releasing v1.0, we want to finish the following:
|
51
|
+
|
52
|
+
- [x] Finalize the testing DSL.
|
22
53
|
- [x] Testing the library itself (against known 'true' axiomatically correct Ruby code.)
|
23
54
|
- [x] Customization of common settings
|
24
55
|
- [x] Filtering generators.
|
@@ -28,15 +59,17 @@ Before releasing this gem on Rubygems, the following things need to be finished:
|
|
28
59
|
- [x] Good, unicode-compliant, string generators.
|
29
60
|
- [x] Filtering generator outputs.
|
30
61
|
- [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.
|
62
|
+
- [x] Possibility to resize generators.
|
31
63
|
- [x] `#instance` generator to allow the easy creation of generators for custom datatypes.
|
64
|
+
- [x] Builtin generation of `Set`s
|
65
|
+
- [x] Builtin generation of `Date`s, `Time`s and `DateTime`s.
|
66
|
+
- [x] Configuration option to resize all generators given to a particular Property instance.
|
67
|
+
- [ ] A simple way to create recursive generators
|
32
68
|
- [ ] A usage guide.
|
33
69
|
|
34
|
-
|
70
|
+
## Nice-to-haves
|
35
71
|
|
36
|
-
-
|
37
|
-
- [ ] `aggregate` , `resize` and similar generator-modifying calls (c.f. PropEr's variants of these) which will help with introspection/metrics.
|
38
|
-
- [ ] Integration with other Ruby test frameworks.
|
39
|
-
- Stateful property testing. If implemented at some point, will probably happen in a separate add-on library.
|
72
|
+
- Stateful property testing. If implemented at some point, will probably happen in a separate add-on library.
|
40
73
|
|
41
74
|
|
42
75
|
## Installation
|
@@ -61,17 +94,15 @@ Or install it yourself as:
|
|
61
94
|
### Using PropCheck for basic testing
|
62
95
|
|
63
96
|
Propcheck exposes the `forall` method.
|
64
|
-
It takes generators as keyword arguments
|
65
|
-
|
66
|
-
|
67
|
-
_(to be precise: a method on the execution context is defined which returns the current generated value for that name)_
|
97
|
+
It takes any number of generators as arguments (or keyword arguments), as well as a block to run.
|
98
|
+
The value(s) generated from the generator(s) passed to the `forall` will be given to the block as arguments.
|
68
99
|
|
69
100
|
Raise an exception from the block if there is a problem. If there is no problem, just return normally.
|
70
101
|
|
71
102
|
```ruby
|
72
|
-
|
103
|
+
G = PropCheck::Generators
|
73
104
|
# testing that Enumerable#sort sorts in ascending order
|
74
|
-
PropCheck.forall(array(integer)) do |numbers|
|
105
|
+
PropCheck.forall(G.array(G.integer)) do |numbers|
|
75
106
|
sorted_numbers = numbers.sort
|
76
107
|
|
77
108
|
# Check that no number is smaller than the previous number
|
@@ -93,8 +124,8 @@ end
|
|
93
124
|
```
|
94
125
|
```ruby
|
95
126
|
# And then in a test case:
|
96
|
-
|
97
|
-
PropCheck.forall(numbers: array(integer)) do |numbers:|
|
127
|
+
G = PropCheck::Generators
|
128
|
+
PropCheck.forall(numbers: G.array(G.integer)) do |numbers:|
|
98
129
|
result = naive_average(numbers)
|
99
130
|
unless result.is_a?(Integer) do
|
100
131
|
raise "Expected the average to be an integer!"
|
@@ -104,10 +135,10 @@ end
|
|
104
135
|
# Or if you e.g. are using RSpec:
|
105
136
|
describe "#naive_average" do
|
106
137
|
include PropCheck
|
107
|
-
|
138
|
+
G = PropCheck::Generators
|
108
139
|
|
109
140
|
it "returns an integer for any input" do
|
110
|
-
forall(numbers: array(integer)) do |numbers:|
|
141
|
+
forall(numbers: G.array(G.integer)) do |numbers:|
|
111
142
|
result = naive_average(numbers)
|
112
143
|
expect(result).to be_a(Integer)
|
113
144
|
end
|
@@ -177,11 +208,12 @@ It contains generators for:
|
|
177
208
|
- (any, only real-valued) floats,
|
178
209
|
- (any, printable only, alphanumeric only, etc) strings and symbols
|
179
210
|
- fixed-size arrays and hashes
|
180
|
-
- as well as varying-size arrays and
|
211
|
+
- as well as varying-size arrays, hashes and sets.
|
212
|
+
- dates, times, datetimes.
|
181
213
|
- and many more!
|
182
214
|
|
183
|
-
It is common to
|
184
|
-
|
215
|
+
It is common and recommended to set up a module alias by using `G = PropCheck::Generators` in e.g. your testing-suite files to be able to refer to all of them.
|
216
|
+
_(Earlier versions of the library recommended including the module instead. But this will make it very simple to accidentally shadow a generator with a local variable named `float` or `array` and similar.)_
|
185
217
|
|
186
218
|
### Writing Custom Generators
|
187
219
|
|
@@ -197,33 +229,37 @@ Always returns the given value. No shrinking.
|
|
197
229
|
|
198
230
|
Allows you to take the result of one generator and transform it into something else.
|
199
231
|
|
200
|
-
>>
|
201
|
-
=> "S"
|
232
|
+
>> G.choose(32..128).map(&:chr).sample(1, size: 10, Random.new(42))
|
233
|
+
=> ["S"]
|
202
234
|
|
203
235
|
#### Generator#bind
|
204
236
|
|
205
237
|
Allows you to create one or another generator conditionally on the output of another generator.
|
206
238
|
|
207
|
-
>>
|
208
|
-
=> [2, 79]
|
239
|
+
>> G.integer.bind { |a| G.integer.bind { |b| G.constant([a , b]) } }.sample(1, size: 100, rng: Random.new(42)
|
240
|
+
=> [[2, 79]]
|
241
|
+
|
242
|
+
This is an advanced feature. Often, you can use a combination of `Generators.tuple` and `Generator#map` instead:
|
209
243
|
|
244
|
+
>> G.tuple(integer, integer).sample(1, size: 100, rng: Random.new(42)
|
245
|
+
=> [[2, 79]]
|
210
246
|
|
211
247
|
#### Generators.one_of
|
212
248
|
|
213
249
|
Useful if you want to be able to generate a value to be one of multiple possibilities:
|
214
250
|
|
215
251
|
|
216
|
-
>>
|
252
|
+
>> G.one_of(G.constant(true), G.constant(false)).sample(5, size: 10, rng: Random.new(42))
|
217
253
|
=> [true, false, true, true, true]
|
218
254
|
|
219
|
-
(
|
255
|
+
(Note that for this example, you can also use `G.boolean`. The example happens to show how it is implemented under the hood.)
|
220
256
|
|
221
257
|
#### Generators.frequency
|
222
258
|
|
223
259
|
If `one_of` does not give you enough flexibility because you want some results to be more common than others,
|
224
260
|
you can use `Generators.frequency` which takes a hash of (integer_frequency => generator) keypairs.
|
225
261
|
|
226
|
-
>>
|
262
|
+
>> G.frequency(5 => G.integer, 1 => G.printable_ascii_char).sample(size: 10, rng: Random.new(42))
|
227
263
|
=> [4, -3, 10, 8, 0, -7, 10, 1, "E", 10]
|
228
264
|
|
229
265
|
#### Others
|
@@ -257,3 +293,10 @@ Everyone interacting in the PropCheck project’s codebases, issue trackers, cha
|
|
257
293
|
I want to thank the original creators of QuickCheck (Koen Claessen, John Hughes) as well as the authors of many great property testing libraries that I was/am able to use as inspiration.
|
258
294
|
I also want to greatly thank Thomasz Kowal who made me excited about property based testing [with his great talk about stateful property testing](https://www.youtube.com/watch?v=q0wZzFUYCuM),
|
259
295
|
as well as Fred Herbert for his great book [Property-Based Testing with PropEr, Erlang and Elixir](https://propertesting.com/) which is really worth the read (regardless of what language you are using).
|
296
|
+
|
297
|
+
The implementation and API of PropCheck takes a lot of inspiration from the following pre-existing libraries:
|
298
|
+
|
299
|
+
- Haskell's [QuickCheck](https://hackage.haskell.org/package/QuickCheck) and [Hedgehog](https://hackage.haskell.org/package/hedgehog);
|
300
|
+
- Erlang's [PropEr](https://hex.pm/packages/proper);
|
301
|
+
- Elixir's [StreamData](https://hex.pm/packages/stream_data);
|
302
|
+
- Python's [Hypothesis](https://hypothesis.works/).
|
data/lib/prop_check/generator.rb
CHANGED
@@ -11,7 +11,8 @@ module PropCheck
|
|
11
11
|
@@default_size = 10
|
12
12
|
@@default_rng = Random.new
|
13
13
|
@@max_consecutive_attempts = 100
|
14
|
-
@@default_kwargs = {size: @@default_size, rng: @@default_rng,
|
14
|
+
@@default_kwargs = { size: @@default_size, rng: @@default_rng,
|
15
|
+
max_consecutive_attempts: @@max_consecutive_attempts }
|
15
16
|
|
16
17
|
##
|
17
18
|
# Being a special kind of Proc, a Generator wraps a block.
|
@@ -33,11 +34,11 @@ module PropCheck
|
|
33
34
|
return res
|
34
35
|
end
|
35
36
|
|
36
|
-
raise Errors::GeneratorExhaustedError, "
|
37
|
+
raise Errors::GeneratorExhaustedError, ''"
|
37
38
|
Exhausted #{max_consecutive_attempts} consecutive generation attempts.
|
38
39
|
|
39
40
|
Probably too few generator results were adhering to a `where` condition.
|
40
|
-
"
|
41
|
+
"''
|
41
42
|
end
|
42
43
|
|
43
44
|
##
|
@@ -88,7 +89,7 @@ module PropCheck
|
|
88
89
|
# end.flatten
|
89
90
|
# end
|
90
91
|
Generator.new do |**kwargs|
|
91
|
-
outer_result =
|
92
|
+
outer_result = generate(**kwargs)
|
92
93
|
outer_result.bind do |outer_val|
|
93
94
|
inner_generator = generator_proc.call(outer_val)
|
94
95
|
inner_generator.generate(**kwargs)
|
@@ -103,15 +104,30 @@ module PropCheck
|
|
103
104
|
# => "S"
|
104
105
|
def map(&proc)
|
105
106
|
Generator.new do |**kwargs|
|
106
|
-
result =
|
107
|
+
result = generate(**kwargs)
|
107
108
|
result.map(&proc)
|
108
109
|
end
|
109
110
|
end
|
110
111
|
|
112
|
+
##
|
113
|
+
# Turns a generator returning `x` into a generator returning `[x, config]`
|
114
|
+
# where `config` is the current `PropCheck::Property::Configuration`.
|
115
|
+
# This can be used to inspect the configuration inside a `#map` or `#where`
|
116
|
+
# and act on it.
|
117
|
+
#
|
118
|
+
# >> Generators.choose(0..100).with_config.map { |int, conf| Date.jd(conf[:default_epoch].jd + int) }.call(size: 10, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
119
|
+
# => Date.new(2023, 01, 10)
|
120
|
+
def with_config
|
121
|
+
Generator.new do |**kwargs|
|
122
|
+
result = generate(**kwargs)
|
123
|
+
result.map { |val| [val, kwargs[:config]] }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
111
127
|
##
|
112
128
|
# Creates a new Generator that only produces a value when the block `condition` returns a truthy value.
|
113
129
|
def where(&condition)
|
114
|
-
|
130
|
+
map do |result|
|
115
131
|
# if condition.call(*result)
|
116
132
|
if PropCheck::Helper.call_splatted(result, &condition)
|
117
133
|
result
|
@@ -131,7 +147,7 @@ module PropCheck
|
|
131
147
|
def resize(&proc)
|
132
148
|
Generator.new do |size:, **other_kwargs|
|
133
149
|
new_size = proc.call(size)
|
134
|
-
|
150
|
+
generate(**other_kwargs, size: new_size)
|
135
151
|
end
|
136
152
|
end
|
137
153
|
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
# coding: utf-8
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
3
|
+
require 'date'
|
4
4
|
require 'prop_check/generator'
|
5
5
|
require 'prop_check/lazy_tree'
|
6
6
|
module PropCheck
|
@@ -136,7 +136,8 @@ module PropCheck
|
|
136
136
|
# that is: no infinity, no NaN,
|
137
137
|
# no numbers testing the limits of floating-point arithmetic.
|
138
138
|
#
|
139
|
-
# Shrinks
|
139
|
+
# Shrinks towards zero.
|
140
|
+
# The shrinking strategy also moves towards 'simpler' floats (like `1.0`) from 'complicated' floats (like `3.76543`).
|
140
141
|
#
|
141
142
|
# >> Generators.real_float().sample(10, size: 10, rng: Random.new(42))
|
142
143
|
# => [-2.2, -0.2727272727272727, 4.0, 1.25, -3.7272727272727275, -8.833333333333334, -8.090909090909092, 1.1428571428571428, 0.0, 8.0]
|
@@ -146,20 +147,130 @@ module PropCheck
|
|
146
147
|
end
|
147
148
|
end
|
148
149
|
|
149
|
-
|
150
|
+
##
|
151
|
+
# Generates any real floating-point numbers,
|
152
|
+
# but will never generate zero.
|
153
|
+
# c.f. #real_float
|
154
|
+
#
|
155
|
+
# >> Generators.real_nonzero_float().sample(10, size: 10, rng: Random.new(43))
|
156
|
+
# => [-7.25, 7.125, -7.636363636363637, -3.0, -8.444444444444445, -6.857142857142857, 2.4545454545454546, 3.0, -7.454545454545455, -6.25]
|
157
|
+
def real_nonzero_float
|
158
|
+
real_float.where { |val| val != 0.0 }
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Generates real floating-point numbers which are never negative.
|
163
|
+
# Shrinks towards 0
|
164
|
+
# c.f. #real_float
|
165
|
+
#
|
166
|
+
# >> Generators.real_nonnegative_float().sample(10, size: 10, rng: Random.new(43))
|
167
|
+
# => [7.25, 7.125, 7.636363636363637, 3.0, 8.444444444444445, 0.0, 6.857142857142857, 2.4545454545454546, 3.0, 7.454545454545455]
|
168
|
+
def real_nonnegative_float
|
169
|
+
real_float.map(&:abs)
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Generates real floating-point numbers which are never positive.
|
174
|
+
# Shrinks towards 0
|
175
|
+
# c.f. #real_float
|
176
|
+
#
|
177
|
+
# >> Generators.real_nonpositive_float().sample(10, size: 10, rng: Random.new(44))
|
178
|
+
# => [-9.125, -2.3636363636363638, -8.833333333333334, -1.75, -8.4, -2.4, -3.5714285714285716, -1.0, -6.111111111111111, -4.0]
|
179
|
+
def real_nonpositive_float
|
180
|
+
real_nonnegative_float.map(&:-@)
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Generates real floating-point numbers which are always positive
|
185
|
+
# Shrinks towards Float::MIN
|
186
|
+
#
|
187
|
+
# Does not consider denormals.
|
188
|
+
# c.f. #real_float
|
189
|
+
#
|
190
|
+
# >> Generators.real_positive_float().sample(10, size: 10, rng: Random.new(42))
|
191
|
+
# => [2.2, 0.2727272727272727, 4.0, 1.25, 3.7272727272727275, 8.833333333333334, 8.090909090909092, 1.1428571428571428, 2.2250738585072014e-308, 8.0]
|
192
|
+
def real_positive_float
|
193
|
+
real_nonnegative_float.map { |val| val + Float::MIN }
|
194
|
+
end
|
195
|
+
|
196
|
+
##
|
197
|
+
# Generates real floating-point numbers which are always negative
|
198
|
+
# Shrinks towards -Float::MIN
|
199
|
+
#
|
200
|
+
# Does not consider denormals.
|
201
|
+
# c.f. #real_float
|
202
|
+
#
|
203
|
+
# >> Generators.real_negative_float().sample(10, size: 10, rng: Random.new(42))
|
204
|
+
# => [-2.2, -0.2727272727272727, -4.0, -1.25, -3.7272727272727275, -8.833333333333334, -8.090909090909092, -1.1428571428571428, -2.2250738585072014e-308, -8.0]
|
205
|
+
def real_negative_float
|
206
|
+
real_positive_float.map(&:-@)
|
207
|
+
end
|
208
|
+
|
209
|
+
@@special_floats = [Float::NAN,
|
210
|
+
Float::INFINITY,
|
211
|
+
-Float::INFINITY,
|
212
|
+
Float::MAX,
|
213
|
+
-Float::MAX,
|
214
|
+
Float::MIN,
|
215
|
+
-Float::MIN,
|
216
|
+
Float::EPSILON,
|
217
|
+
-Float::EPSILON,
|
218
|
+
0.0.next_float,
|
219
|
+
0.0.prev_float]
|
150
220
|
##
|
151
221
|
# Generates floating-point numbers
|
152
222
|
# Will generate NaN, Infinity, -Infinity,
|
153
223
|
# as well as Float::EPSILON, Float::MAX, Float::MIN,
|
154
224
|
# 0.0.next_float, 0.0.prev_float,
|
155
225
|
# to test the handling of floating-point edge cases.
|
156
|
-
# Approx. 1/
|
226
|
+
# Approx. 1/50 generated numbers is a special one.
|
157
227
|
#
|
158
228
|
# Shrinks to smaller, real floats.
|
159
229
|
# >> Generators.float().sample(10, size: 10, rng: Random.new(42))
|
160
|
-
#
|
230
|
+
# >> Generators.float().sample(10, size: 10, rng: Random.new(4))
|
231
|
+
# => [-8.0, 2.0, 2.7142857142857144, -4.0, -10.2, -6.666666666666667, -Float::INFINITY, -10.2, 2.1818181818181817, -6.2]
|
161
232
|
def float
|
162
|
-
frequency(
|
233
|
+
frequency(49 => real_float, 1 => one_of(*@@special_floats.map(&method(:constant))))
|
234
|
+
end
|
235
|
+
|
236
|
+
##
|
237
|
+
# Generates any nonzerno floating-point number.
|
238
|
+
# Will generate special floats (except NaN) from time to time.
|
239
|
+
# c.f. #float
|
240
|
+
def nonzero_float
|
241
|
+
float.where { |val| val != 0.0 && val }
|
242
|
+
end
|
243
|
+
|
244
|
+
##
|
245
|
+
# Generates nonnegative floating point numbers
|
246
|
+
# Will generate special floats (except NaN) from time to time.
|
247
|
+
# c.f. #float
|
248
|
+
def nonnegative_float
|
249
|
+
float.map(&:abs).where { |val| val != Float::NAN }
|
250
|
+
end
|
251
|
+
|
252
|
+
##
|
253
|
+
# Generates nonpositive floating point numbers
|
254
|
+
# Will generate special floats (except NaN) from time to time.
|
255
|
+
# c.f. #float
|
256
|
+
def nonpositive_float
|
257
|
+
nonnegative_float.map(&:-@)
|
258
|
+
end
|
259
|
+
|
260
|
+
##
|
261
|
+
# Generates positive floating point numbers
|
262
|
+
# Will generate special floats (except NaN) from time to time.
|
263
|
+
# c.f. #float
|
264
|
+
def positive_float
|
265
|
+
nonnegative_float.where { |val| val != 0.0 && val }
|
266
|
+
end
|
267
|
+
|
268
|
+
##
|
269
|
+
# Generates positive floating point numbers
|
270
|
+
# Will generate special floats (except NaN) from time to time.
|
271
|
+
# c.f. #float
|
272
|
+
def negative_float
|
273
|
+
positive_float.map(&:-@).where { |val| val != 0.0 }
|
163
274
|
end
|
164
275
|
|
165
276
|
##
|
@@ -269,7 +380,7 @@ module PropCheck
|
|
269
380
|
if max.nil?
|
270
381
|
nonnegative_integer.bind { |count| make_array(element_generator, min, count, uniq) }
|
271
382
|
else
|
272
|
-
make_array(element_generator, min,
|
383
|
+
choose(min..max).bind { |count| make_array(element_generator, min, count, uniq) }
|
273
384
|
end
|
274
385
|
end
|
275
386
|
|
@@ -298,31 +409,57 @@ module PropCheck
|
|
298
409
|
arr = []
|
299
410
|
uniques = Set.new
|
300
411
|
count = 0
|
301
|
-
(0..).lazy.map do
|
302
|
-
elem = element_generator.clone.generate(**kwargs)
|
303
|
-
if uniques.add?(uniq_fun.call(elem.root))
|
304
|
-
arr.push(elem)
|
305
|
-
count = 0
|
306
|
-
else
|
307
|
-
count += 1
|
308
|
-
end
|
309
412
|
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
413
|
+
if amount == 0
|
414
|
+
LazyTree.new([])
|
415
|
+
else
|
416
|
+
0.step.lazy.map do
|
417
|
+
elem = element_generator.clone.generate(**kwargs)
|
418
|
+
if uniques.add?(uniq_fun.call(elem.root))
|
419
|
+
arr.push(elem)
|
420
|
+
count = 0
|
314
421
|
else
|
315
|
-
|
422
|
+
count += 1
|
423
|
+
end
|
424
|
+
|
425
|
+
if count > kwargs[:max_consecutive_attempts]
|
426
|
+
if arr.size >= min
|
427
|
+
# Give up and return shorter array in this case
|
428
|
+
amount = min
|
429
|
+
else
|
430
|
+
raise Errors::GeneratorExhaustedError, "Too many consecutive elements filtered by 'uniq:'."
|
431
|
+
end
|
316
432
|
end
|
317
433
|
end
|
318
|
-
|
319
|
-
|
320
|
-
.force
|
434
|
+
.take_while { arr.size < amount }
|
435
|
+
.force
|
321
436
|
|
322
|
-
|
437
|
+
LazyTree.zip(arr).map { |array| array.uniq(&uniq_fun) }
|
438
|
+
end
|
323
439
|
end
|
324
440
|
end
|
325
441
|
|
442
|
+
##
|
443
|
+
# Generates a set of elements, where each of the elements
|
444
|
+
# is generated by `element_generator`.
|
445
|
+
#
|
446
|
+
# Shrinks to smaller sets (with shrunken elements).
|
447
|
+
# Accepted keyword arguments:
|
448
|
+
#
|
449
|
+
# `empty:` When false, behaves the same as `min: 1`
|
450
|
+
# `min:` Ensures at least this many elements are generated. (default: 0)
|
451
|
+
# `max:` Ensures at most this many elements are generated. When nil, an arbitrary count is used instead. (default: nil)
|
452
|
+
#
|
453
|
+
# In the set, elements are always unique.
|
454
|
+
# If it is not possible to generate another unique value after the configured `max_consecutive_attempts`
|
455
|
+
# a `PropCheck::Errors::GeneratorExhaustedError` will be raised.
|
456
|
+
#
|
457
|
+
# >> Generators.set(Generators.positive_integer).sample(5, size: 4, rng: Random.new(42))
|
458
|
+
# => [Set[2, 4], Set[], Set[3, 4], Set[], Set[4]]
|
459
|
+
def set(element_generator, min: 0, max: nil, empty: true)
|
460
|
+
array(element_generator, min: min, max: max, empty: empty, uniq: true).map(&:to_set)
|
461
|
+
end
|
462
|
+
|
326
463
|
##
|
327
464
|
# Generates a hash of key->values,
|
328
465
|
# where each of the keys is made using the `key_generator`
|
@@ -559,8 +696,8 @@ module PropCheck
|
|
559
696
|
#
|
560
697
|
# Shrinks towards simpler terms, like `true`, an empty array, a single character or an integer.
|
561
698
|
#
|
562
|
-
# >> Generators.truthy.sample(5, size:
|
563
|
-
# => [[
|
699
|
+
# >> Generators.truthy.sample(5, size: 2, rng: Random.new(42))
|
700
|
+
# => [[2], {:gz=>0, :""=>0}, [1.0, 0.5], 0.6666666666666667, {"𦐺\u{9FDDB}"=>1, ""=>1}]
|
564
701
|
def truthy
|
565
702
|
one_of(constant(true),
|
566
703
|
constant([]),
|
@@ -574,8 +711,7 @@ module PropCheck
|
|
574
711
|
array(string),
|
575
712
|
hash(simple_symbol, integer),
|
576
713
|
hash(string, integer),
|
577
|
-
hash(string, string)
|
578
|
-
)
|
714
|
+
hash(string, string))
|
579
715
|
end
|
580
716
|
|
581
717
|
##
|
@@ -588,6 +724,116 @@ module PropCheck
|
|
588
724
|
frequency(9 => other_generator, 1 => constant(nil))
|
589
725
|
end
|
590
726
|
|
727
|
+
##
|
728
|
+
# Generates `Date` objects.
|
729
|
+
# DateTimes start around the given `epoch:` and deviate more when `size` increases.
|
730
|
+
# when no epoch is set, `PropCheck::Property::Configuration.default_epoch` is used, which defaults to `DateTime.now.to_date`.
|
731
|
+
#
|
732
|
+
# >> Generators.date(epoch: Date.new(2022, 01, 01)).sample(2, rng: Random.new(42))
|
733
|
+
# => [Date.new(2021, 12, 28), Date.new(2022, 01, 10)]
|
734
|
+
def date(epoch: nil)
|
735
|
+
date_from_offset(integer, epoch: epoch)
|
736
|
+
end
|
737
|
+
|
738
|
+
##
|
739
|
+
# variant of #date that only generates dates in the future (relative to `:epoch`).
|
740
|
+
#
|
741
|
+
# >> Generators.future_date(epoch: Date.new(2022, 01, 01)).sample(2, rng: Random.new(42))
|
742
|
+
# => [Date.new(2022, 01, 06), Date.new(2022, 01, 11)]
|
743
|
+
def future_date(epoch: Date.today)
|
744
|
+
date_from_offset(positive_integer, epoch: epoch)
|
745
|
+
end
|
746
|
+
|
747
|
+
##
|
748
|
+
# variant of #date that only generates dates in the past (relative to `:epoch`).
|
749
|
+
#
|
750
|
+
# >> Generators.past_date(epoch: Date.new(2022, 01, 01)).sample(2, rng: Random.new(42))
|
751
|
+
# => [Date.new(2021, 12, 27), Date.new(2021, 12, 22)]
|
752
|
+
def past_date(epoch: Date.today)
|
753
|
+
date_from_offset(negative_integer, epoch: epoch)
|
754
|
+
end
|
755
|
+
|
756
|
+
private def date_from_offset(offset_gen, epoch:)
|
757
|
+
if epoch
|
758
|
+
offset_gen.map { |offset| Date.jd(epoch.jd + offset) }
|
759
|
+
else
|
760
|
+
offset_gen.with_config.map do |offset, config|
|
761
|
+
epoch = config.default_epoch.to_date
|
762
|
+
Date.jd(epoch.jd + offset)
|
763
|
+
end
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
##
|
768
|
+
# Generates `DateTime` objects.
|
769
|
+
# DateTimes start around the given `epoch:` and deviate more when `size` increases.
|
770
|
+
# when no epoch is set, `PropCheck::Property::Configuration.default_epoch` is used, which defaults to `DateTime.now`.
|
771
|
+
#
|
772
|
+
# >> PropCheck::Generators.datetime.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
773
|
+
# => [DateTime.parse("2022-11-17 07:11:59.999983907 +0000"), DateTime.parse("2022-11-19 05:27:16.363618076 +0000")]
|
774
|
+
def datetime(epoch: nil)
|
775
|
+
datetime_from_offset(real_float, epoch: epoch)
|
776
|
+
end
|
777
|
+
|
778
|
+
##
|
779
|
+
# alias for `#datetime`, for backwards compatibility.
|
780
|
+
# Prefer using `datetime`!
|
781
|
+
def date_time(epoch: nil)
|
782
|
+
datetime(epoch: epoch)
|
783
|
+
end
|
784
|
+
|
785
|
+
##
|
786
|
+
# Variant of `#datetime` that only generates datetimes in the future (relative to `:epoch`).
|
787
|
+
#
|
788
|
+
# >> PropCheck::Generators.future_datetime.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new).map(&:inspect)
|
789
|
+
# => ["#<DateTime: 2022-11-21T16:48:00+00:00 ((2459905j,60480s,16093n),+0s,2299161j)>", "#<DateTime: 2022-11-19T18:32:43+00:00 ((2459903j,66763s,636381924n),+0s,2299161j)>"]
|
790
|
+
def future_datetime(epoch: nil)
|
791
|
+
datetime_from_offset(real_positive_float, epoch: epoch)
|
792
|
+
end
|
793
|
+
|
794
|
+
##
|
795
|
+
# Variant of `#datetime` that only generates datetimes in the past (relative to `:epoch`).
|
796
|
+
#
|
797
|
+
# >> PropCheck::Generators.past_datetime.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
798
|
+
# => [DateTime.parse("2022-11-17 07:11:59.999983907 +0000"), DateTime.parse("2022-11-19 05:27:16.363618076 +0000")]
|
799
|
+
def past_datetime(epoch: nil)
|
800
|
+
datetime_from_offset(real_negative_float, epoch: epoch)
|
801
|
+
end
|
802
|
+
|
803
|
+
##
|
804
|
+
# Generates `Time` objects.
|
805
|
+
# Times start around the given `epoch:` and deviate more when `size` increases.
|
806
|
+
# when no epoch is set, `PropCheck::Property::Configuration.default_epoch` is used, which defaults to `DateTime.now`.
|
807
|
+
#
|
808
|
+
# >> PropCheck::Generators.time.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
809
|
+
# => [DateTime.parse("2022-11-17 07:11:59.999983907 +0000").to_time, DateTime.parse("2022-11-19 05:27:16.363618076 +0000").to_time]
|
810
|
+
def time(epoch: nil)
|
811
|
+
datetime(epoch: epoch).map(&:to_time)
|
812
|
+
end
|
813
|
+
|
814
|
+
##
|
815
|
+
# Variant of `#time` that only generates datetimes in the future (relative to `:epoch`).
|
816
|
+
def future_time(epoch: nil)
|
817
|
+
future_datetime(epoch: epoch).map(&:to_time)
|
818
|
+
end
|
819
|
+
|
820
|
+
##
|
821
|
+
# Variant of `#time` that only generates datetimes in the past (relative to `:epoch`).
|
822
|
+
def past_time(epoch: nil)
|
823
|
+
past_datetime(epoch: epoch).map(&:to_time)
|
824
|
+
end
|
825
|
+
|
826
|
+
private def datetime_from_offset(offset_gen, epoch:)
|
827
|
+
if epoch
|
828
|
+
offset_gen.map { |offset| DateTime.jd(epoch.ajd + offset) }
|
829
|
+
else
|
830
|
+
offset_gen.with_config.map do |offset, config|
|
831
|
+
epoch = config.default_epoch.to_date
|
832
|
+
DateTime.jd(epoch.ajd + offset)
|
833
|
+
end
|
834
|
+
end
|
835
|
+
end
|
836
|
+
|
591
837
|
##
|
592
838
|
# Generates an instance of `klass`
|
593
839
|
# using `args` and/or `kwargs`
|
data/lib/prop_check/helper.rb
CHANGED
@@ -33,17 +33,9 @@ module PropCheck
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def call_splatted(val, &block)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
else
|
40
|
-
block.call(val)
|
41
|
-
end
|
42
|
-
# if kwval != {}
|
43
|
-
# block.call(**kwval)
|
44
|
-
# else
|
45
|
-
# block.call(*val)
|
46
|
-
# end
|
36
|
+
return block.call(**val) if val.is_a?(Hash) && val.keys.all? { |k| k.is_a?(Symbol) }
|
37
|
+
|
38
|
+
block.call(val)
|
47
39
|
end
|
48
40
|
end
|
49
41
|
end
|
@@ -1,25 +1,50 @@
|
|
1
1
|
module PropCheck
|
2
2
|
class Property
|
3
|
+
## Configure PropCheck
|
4
|
+
#
|
5
|
+
# Configurations can be set globally,
|
6
|
+
# but also overridden on a per-generator basis.
|
7
|
+
# c.f. PropCheck.configure, PropCheck.configuration and PropCheck::Property#with_config
|
8
|
+
#
|
9
|
+
# ## Available options
|
10
|
+
# - `verbose:` When true, shows detailed options of the data generation and shrinking process. (Default: false)
|
11
|
+
# - `n_runs:` The amount of iterations each `forall` is being run.
|
12
|
+
# - `max_generate_attempts:` The amount of times the library tries a generator in total
|
13
|
+
# before raising `Errors::GeneratorExhaustedError`. c.f. `PropCheck::Generator#where`. (Default: 10_000)
|
14
|
+
# - `max_shrink_steps:` The amount of times shrinking is attempted. (Default: 10_000)
|
15
|
+
# - `max_consecutive_attempts:`
|
16
|
+
# - `max_consecutive_attempts:` The amount of times the library tries a filtered generator consecutively
|
17
|
+
# again before raising `Errors::GeneratorExhaustedError`. c.f. `PropCheck::Generator#where`. (Default: 10_000)
|
18
|
+
# - `default_epoch:` The 'base' value to use for date/time generators like
|
19
|
+
# `PropCheck::Generators#date` `PropCheck::Generators#future_date` `PropCheck::Generators#time`, etc.
|
20
|
+
# (Default: `DateTime.now`)
|
21
|
+
# - `resize_function` A proc that can be used to resize _all_ generators.
|
22
|
+
# Takes the current size as integer and should return a new integer.
|
23
|
+
# (Default: `proc { |size| size }`)
|
3
24
|
Configuration = Struct.new(
|
4
25
|
:verbose,
|
5
26
|
:n_runs,
|
6
27
|
:max_generate_attempts,
|
7
28
|
:max_shrink_steps,
|
8
29
|
:max_consecutive_attempts,
|
9
|
-
|
10
|
-
|
30
|
+
:default_epoch,
|
31
|
+
:resize_function,
|
32
|
+
keyword_init: true
|
33
|
+
) do
|
11
34
|
def initialize(
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
35
|
+
verbose: false,
|
36
|
+
n_runs: 100,
|
37
|
+
max_generate_attempts: 10_000,
|
38
|
+
max_shrink_steps: 10_000,
|
39
|
+
max_consecutive_attempts: 30,
|
40
|
+
default_epoch: DateTime.now,
|
41
|
+
resize_function: proc { |size| size }
|
42
|
+
)
|
18
43
|
super
|
19
44
|
end
|
20
45
|
|
21
46
|
def merge(other)
|
22
|
-
Configuration.new(**
|
47
|
+
Configuration.new(**to_h.merge(other.to_h))
|
23
48
|
end
|
24
49
|
end
|
25
50
|
end
|
data/lib/prop_check/property.rb
CHANGED
@@ -102,7 +102,7 @@ module PropCheck
|
|
102
102
|
# you can immediately pass a block to this method.
|
103
103
|
# (so `forall(a: Generators.integer).with_config(verbose: true) do ... end` is the same as `forall(a: Generators.integer).with_config(verbose: true).check do ... end`)
|
104
104
|
def with_config(**config, &block)
|
105
|
-
duplicate =
|
105
|
+
duplicate = dup
|
106
106
|
duplicate.instance_variable_set(:@config, @config.merge(config))
|
107
107
|
duplicate.freeze
|
108
108
|
|
@@ -111,10 +111,71 @@ module PropCheck
|
|
111
111
|
duplicate
|
112
112
|
end
|
113
113
|
|
114
|
+
##
|
115
|
+
# Resizes all generators in this property with the given function.
|
116
|
+
#
|
117
|
+
# Shorthand for manually wrapping `PropCheck::Property::Configuration.resize_function` with the new function.
|
118
|
+
def resize(&block)
|
119
|
+
raise '#resize called without a block' unless block_given?
|
120
|
+
|
121
|
+
orig_fun = @config.resize_function
|
122
|
+
with_config(resize_function: block)
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# Resizes all generators in this property. The new size is `2.pow(orig_size)`
|
127
|
+
#
|
128
|
+
# c.f. #resize
|
129
|
+
def growing_exponentially(&block)
|
130
|
+
orig_fun = @config.resize_function
|
131
|
+
fun = proc { |size| 2.pow(orig_fun.call(size)) }
|
132
|
+
with_config(resize_function: fun, &block)
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Resizes all generators in this property. The new size is `orig_size * orig_size`
|
137
|
+
#
|
138
|
+
# c.f. #resize
|
139
|
+
def growing_quadratically(&block)
|
140
|
+
orig_fun = @config.resize_function
|
141
|
+
fun = proc { |size| orig_fun.call(size).pow(2) }
|
142
|
+
with_config(resize_function: fun, &block)
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Resizes all generators in this property. The new size is `2 * orig_size`
|
147
|
+
#
|
148
|
+
# c.f. #resize
|
149
|
+
def growing_fast(&block)
|
150
|
+
orig_fun = @config.resize_function
|
151
|
+
fun = proc { |size| orig_fun.call(size) * 2 }
|
152
|
+
with_config(resize_function: fun, &block)
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# Resizes all generators in this property. The new size is `0.5 * orig_size`
|
157
|
+
#
|
158
|
+
# c.f. #resize
|
159
|
+
def growing_slowly(&block)
|
160
|
+
orig_fun = @config.resize_function
|
161
|
+
fun = proc { |size| orig_fun.call(size) * 0.5 }
|
162
|
+
with_config(resize_function: fun, &block)
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# Resizes all generators in this property. The new size is `Math.log2(orig_size)`
|
167
|
+
#
|
168
|
+
# c.f. #resize
|
169
|
+
def growing_logarithmically(&block)
|
170
|
+
orig_fun = @config.resize_function
|
171
|
+
fun = proc { |size| Math.log2(orig_fun.call(size)) }
|
172
|
+
with_config(resize_function: fun, &block)
|
173
|
+
end
|
174
|
+
|
114
175
|
def with_bindings(*bindings, **kwbindings)
|
115
176
|
raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
|
116
177
|
|
117
|
-
duplicate =
|
178
|
+
duplicate = dup
|
118
179
|
duplicate.instance_variable_set(:@gen, gen_from_bindings(bindings, kwbindings))
|
119
180
|
duplicate.freeze
|
120
181
|
duplicate
|
@@ -129,22 +190,24 @@ module PropCheck
|
|
129
190
|
# you might encounter a GeneratorExhaustedError.
|
130
191
|
# Only filter if you have few inputs to reject. Otherwise, improve your generators.
|
131
192
|
def where(&condition)
|
132
|
-
|
193
|
+
unless @gen
|
194
|
+
raise ArgumentError,
|
195
|
+
'No generator bindings specified! #where should be called after `#forall` or `#with_bindings`.'
|
196
|
+
end
|
133
197
|
|
134
|
-
duplicate =
|
198
|
+
duplicate = dup
|
135
199
|
duplicate.instance_variable_set(:@gen, @gen.where(&condition))
|
136
200
|
duplicate.freeze
|
137
201
|
duplicate
|
138
202
|
end
|
139
203
|
|
140
|
-
|
141
204
|
##
|
142
205
|
# Calls `hook` before each time a check is run with new data.
|
143
206
|
#
|
144
207
|
# This is useful to add setup logic
|
145
208
|
# When called multiple times, earlier-added hooks will be called _before_ `hook` is called.
|
146
209
|
def before(&hook)
|
147
|
-
duplicate =
|
210
|
+
duplicate = dup
|
148
211
|
duplicate.instance_variable_set(:@hooks, @hooks.add_before(&hook))
|
149
212
|
duplicate.freeze
|
150
213
|
duplicate
|
@@ -156,7 +219,7 @@ module PropCheck
|
|
156
219
|
# This is useful to add teardown logic
|
157
220
|
# When called multiple times, earlier-added hooks will be called _after_ `hook` is called.
|
158
221
|
def after(&hook)
|
159
|
-
duplicate =
|
222
|
+
duplicate = dup
|
160
223
|
duplicate.instance_variable_set(:@hooks, @hooks.add_after(&hook))
|
161
224
|
duplicate.freeze
|
162
225
|
duplicate
|
@@ -176,7 +239,7 @@ module PropCheck
|
|
176
239
|
# it is possible for the code after `yield` not to be called.
|
177
240
|
# So make sure that cleanup logic is wrapped with the `ensure` keyword.
|
178
241
|
def around(&hook)
|
179
|
-
duplicate =
|
242
|
+
duplicate = dup
|
180
243
|
duplicate.instance_variable_set(:@hooks, @hooks.add_around(&hook))
|
181
244
|
duplicate.freeze
|
182
245
|
duplicate
|
@@ -223,15 +286,15 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
|
|
223
286
|
raise_generator_exhausted!
|
224
287
|
end
|
225
288
|
|
226
|
-
private def raise_generator_exhausted!
|
227
|
-
raise Errors::GeneratorExhaustedError, "
|
289
|
+
private def raise_generator_exhausted!
|
290
|
+
raise Errors::GeneratorExhaustedError, ''"
|
228
291
|
Could not perform `n_runs = #{@config.n_runs}` runs,
|
229
292
|
(exhausted #{@config.max_generate_attempts} tries)
|
230
293
|
because too few generator results were adhering to
|
231
294
|
the `where` condition.
|
232
295
|
|
233
296
|
Try refining your generators instead.
|
234
|
-
"
|
297
|
+
"''
|
235
298
|
end
|
236
299
|
|
237
300
|
private def check_attempt(generator_result, n_successful, &block)
|
@@ -246,7 +309,8 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
|
|
246
309
|
# so we can shrink to find their cause.
|
247
310
|
# don't worry: they all get reraised
|
248
311
|
rescue Exception => e
|
249
|
-
output, shrunken_result, shrunken_exception, n_shrink_steps = show_problem_output(e, generator_result,
|
312
|
+
output, shrunken_result, shrunken_exception, n_shrink_steps = show_problem_output(e, generator_result,
|
313
|
+
n_successful, &block)
|
250
314
|
output_string = output.is_a?(StringIO) ? output.string : e.message
|
251
315
|
|
252
316
|
e.define_singleton_method :prop_check_info do
|
@@ -264,7 +328,7 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
|
|
264
328
|
end
|
265
329
|
|
266
330
|
private def attempts_enum(binding_generator)
|
267
|
-
|
331
|
+
@hooks
|
268
332
|
.wrap_enum(raw_attempts_enum(binding_generator))
|
269
333
|
.lazy
|
270
334
|
.take(@config.n_runs)
|
@@ -275,7 +339,15 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
|
|
275
339
|
size = 1
|
276
340
|
(0...@config.max_generate_attempts)
|
277
341
|
.lazy
|
278
|
-
.map
|
342
|
+
.map do
|
343
|
+
generator_size = @config.resize_function.call(size).to_i
|
344
|
+
binding_generator.generate(
|
345
|
+
size: generator_size,
|
346
|
+
rng: rng,
|
347
|
+
max_consecutive_attempts: @config.max_consecutive_attempts,
|
348
|
+
config: @config
|
349
|
+
)
|
350
|
+
end
|
279
351
|
.map do |result|
|
280
352
|
size += 1
|
281
353
|
|
@@ -287,7 +359,8 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
|
|
287
359
|
output = @config.verbose ? STDOUT : StringIO.new
|
288
360
|
output = PropCheck::Property::OutputFormatter.pre_output(output, n_successful, generator_results.root, problem)
|
289
361
|
shrunken_result, shrunken_exception, n_shrink_steps = shrink(generator_results, output, &block)
|
290
|
-
output = PropCheck::Property::OutputFormatter.post_output(output, n_shrink_steps, shrunken_result,
|
362
|
+
output = PropCheck::Property::OutputFormatter.post_output(output, n_shrink_steps, shrunken_result,
|
363
|
+
shrunken_exception)
|
291
364
|
|
292
365
|
[output, shrunken_result, shrunken_exception, n_shrink_steps]
|
293
366
|
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.16.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:
|
11
|
+
date: 2022-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: amazing_print
|
@@ -35,11 +35,11 @@ executables: []
|
|
35
35
|
extensions: []
|
36
36
|
extra_rdoc_files: []
|
37
37
|
files:
|
38
|
+
- ".github/workflows/run_tests.yaml"
|
38
39
|
- ".gitignore"
|
39
40
|
- ".rspec"
|
40
41
|
- ".rubocop.yml"
|
41
42
|
- ".tool-versions"
|
42
|
-
- ".travis.yml"
|
43
43
|
- CHANGELOG.md
|
44
44
|
- CODE_OF_CONDUCT.md
|
45
45
|
- Gemfile
|
@@ -53,7 +53,6 @@ files:
|
|
53
53
|
- lib/prop_check/generator.rb
|
54
54
|
- lib/prop_check/generators.rb
|
55
55
|
- lib/prop_check/helper.rb
|
56
|
-
- lib/prop_check/helper/lazy_append.rb
|
57
56
|
- lib/prop_check/hooks.rb
|
58
57
|
- lib/prop_check/lazy_tree.rb
|
59
58
|
- lib/prop_check/property.rb
|
@@ -84,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
83
|
- !ruby/object:Gem::Version
|
85
84
|
version: '0'
|
86
85
|
requirements: []
|
87
|
-
rubygems_version: 3.
|
86
|
+
rubygems_version: 3.2.3
|
88
87
|
signing_key:
|
89
88
|
specification_version: 4
|
90
89
|
summary: PropCheck allows you to do property-based testing, including shrinking.
|
data/.travis.yml
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
---
|
2
|
-
sudo: false
|
3
|
-
language: ruby
|
4
|
-
cache: bundler
|
5
|
-
rvm:
|
6
|
-
- 2.6.5
|
7
|
-
before_install: gem install bundler -v 2.0.2
|
8
|
-
env:
|
9
|
-
global:
|
10
|
-
- CC_TEST_REPORTER_ID=9d18f5b43e49eecd6c3da64d85ea9c765d3606c129289d7c8cadf6d448713311
|
11
|
-
before_script:
|
12
|
-
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
13
|
-
- chmod +x ./cc-test-reporter
|
14
|
-
- ./cc-test-reporter before-build
|
15
|
-
script:
|
16
|
-
- bundle exec rspec
|
17
|
-
after_script:
|
18
|
-
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
@@ -1,18 +0,0 @@
|
|
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
|