prop_check 0.16.0 → 0.18.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 +2 -2
- data/CHANGELOG.md +6 -0
- data/README.md +62 -21
- data/lib/prop_check/generator.rb +1 -1
- data/lib/prop_check/generators.rb +74 -4
- data/lib/prop_check/property.rb +5 -8
- data/lib/prop_check/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba078df3937d6f069a4ed24e9aaed9617dc1eb03139be0189e51b060b11a75ba
|
4
|
+
data.tar.gz: 0fe77f31d282754df6712aafae7995e82e99d9ee86f8ad12bd060596147dafe1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e49791f3e5a380e822f4d86f39444a09e26828c4b1ea7c90d298c9eedcc14d8c6fdc05864439e1984b0cead4183beb1ae108bee82c7ce6dccbe11aae5efbd248
|
7
|
+
data.tar.gz: cddd15cf550e166ec93d15fa2ffdf07a68646aa6750d544b6a5cf64c9bfb44177b638e6ff1df2ce25e28105df05aa838534e6eaafadefb7688aed32dae9d3bbc
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
- 0.18.0
|
2
|
+
- Features:
|
3
|
+
- Allows calling `PropCheck::Property#check` without a block, which will just return `self`. This is useful for writing wrapper functions that use `before/after/around/with_config` etc hooks which might themselves optionally want a block so they can be chained. (See the `forall_with_db` snippet in the README for an example)
|
4
|
+
- 0.17.0
|
5
|
+
- Features:
|
6
|
+
- Recursive generation using `PropCheck::Generators.tree`.
|
1
7
|
- 0.16.0
|
2
8
|
- Features:
|
3
9
|
- New option in `PropCheck::Property::Configuration` to resize all generators at once.
|
data/README.md
CHANGED
@@ -40,9 +40,9 @@ It works by generating arbitrary data matching your specification and checking t
|
|
40
40
|
|
41
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
42
|
|
43
|
-
- Your code should
|
43
|
+
- Your code should never crash.
|
44
44
|
- If you remove an object, you can no longer see it
|
45
|
-
- If you serialize and then
|
45
|
+
- If you serialize and then deserialize a value, you get the same value back.
|
46
46
|
|
47
47
|
|
48
48
|
## Implemented and still missing features
|
@@ -64,7 +64,7 @@ Before releasing v1.0, we want to finish the following:
|
|
64
64
|
- [x] Builtin generation of `Set`s
|
65
65
|
- [x] Builtin generation of `Date`s, `Time`s and `DateTime`s.
|
66
66
|
- [x] Configuration option to resize all generators given to a particular Property instance.
|
67
|
-
- [
|
67
|
+
- [x] A simple way to create recursive generators
|
68
68
|
- [ ] A usage guide.
|
69
69
|
|
70
70
|
## Nice-to-haves
|
@@ -107,7 +107,7 @@ PropCheck.forall(G.array(G.integer)) do |numbers|
|
|
107
107
|
|
108
108
|
# Check that no number is smaller than the previous number
|
109
109
|
sorted_numbers.each_cons(2) do |former, latter|
|
110
|
-
raise "Elements are not sorted! #{latter} is < #{former}" if latter
|
110
|
+
raise "Elements are not sorted! #{latter} is < #{former}" if latter > former
|
111
111
|
end
|
112
112
|
end
|
113
113
|
```
|
@@ -122,32 +122,53 @@ def naive_average(array)
|
|
122
122
|
array.sum / array.length
|
123
123
|
end
|
124
124
|
```
|
125
|
-
```ruby
|
126
|
-
# And then in a test case:
|
127
|
-
G = PropCheck::Generators
|
128
|
-
PropCheck.forall(numbers: G.array(G.integer)) do |numbers:|
|
129
|
-
result = naive_average(numbers)
|
130
|
-
unless result.is_a?(Integer) do
|
131
|
-
raise "Expected the average to be an integer!"
|
132
|
-
end
|
133
|
-
end
|
134
125
|
|
135
|
-
|
136
|
-
|
137
|
-
|
126
|
+
The test case, using RSpec:
|
127
|
+
``` ruby
|
128
|
+
require 'rspec'
|
129
|
+
|
130
|
+
RSpec.describe "#naive_average" do
|
138
131
|
G = PropCheck::Generators
|
139
132
|
|
140
133
|
it "returns an integer for any input" do
|
141
|
-
forall(
|
142
|
-
result = naive_average(numbers)
|
134
|
+
PropCheck.forall(G.array(G.integer)) do |numbers|
|
135
|
+
result = naive_average(numbers)
|
136
|
+
|
143
137
|
expect(result).to be_a(Integer)
|
144
138
|
end
|
145
139
|
end
|
146
140
|
end
|
147
141
|
```
|
148
142
|
|
149
|
-
|
143
|
+
The test case, using MiniTest:
|
144
|
+
``` ruby
|
145
|
+
require 'minitest/autorun'
|
146
|
+
class NaiveAverageTest < MiniTest::Unit::TestCase
|
147
|
+
G = PropCheck::Generators
|
148
|
+
|
149
|
+
def test_that_it_returns_an_integer_for_any_input()
|
150
|
+
PropCheck.forall(G.array(G.integer)) do |numbers|
|
151
|
+
result = naive_average(numbers)
|
152
|
+
|
153
|
+
assert_instance_of(Integer, result)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
```
|
150
158
|
|
159
|
+
The test case, using only vanilla Ruby:
|
160
|
+
```ruby
|
161
|
+
# And then in a test case:
|
162
|
+
G = PropCheck::Generators
|
163
|
+
|
164
|
+
PropCheck.forall(G.array(G.integer)) do |numbers|
|
165
|
+
result = naive_average(numbers)
|
166
|
+
|
167
|
+
raise "Expected the average to be an integer!" unless result.is_a?(Integer)
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
When running this particular example PropCheck very quickly finds out that we have made a programming mistake:
|
151
172
|
```ruby
|
152
173
|
ZeroDivisionError:
|
153
174
|
(after 6 successful property test runs)
|
@@ -270,6 +291,26 @@ although above are the most generally useful ones.
|
|
270
291
|
[PropCheck::Generator documentation](https://www.rubydoc.info/github/Qqwy/ruby-prop_check/master/PropCheck/Generator)
|
271
292
|
[PropCheck::Generators documentation](https://www.rubydoc.info/github/Qqwy/ruby-prop_check/master/PropCheck/Generators)
|
272
293
|
|
294
|
+
|
295
|
+
## Usage within Rails / with a database
|
296
|
+
|
297
|
+
Using PropCheck for unit tests in a Rails, Sinatra, Hanami, etc. project is very easy.
|
298
|
+
Here are some simple recommendations for the best results:
|
299
|
+
- Tests that do not need to use the DB at all are usually 10x-100x faster. Faster tests means that you can configure PropCheck to do more test runs.
|
300
|
+
- If you do need to use the database, use the [database_cleaner](https://github.com/DatabaseCleaner/database_cleaner) gem, preferibly with the fast `:transaction` strategy if your RDBMS supports it. To make sure the DB is cleaned around each generated example, you can write the following helper:
|
301
|
+
``` ruby
|
302
|
+
# Version of PropCheck.forall
|
303
|
+
# which ensures records persisted to the DB in one generated example
|
304
|
+
# do not affect any other
|
305
|
+
def forall_with_db(*args, **kwargs, &block)
|
306
|
+
PropCheck.forall(*args, **kwargs)
|
307
|
+
.before { DatabaseCleaner.start }
|
308
|
+
.after { DatabaseCleaner.clean }
|
309
|
+
.check(&block)
|
310
|
+
end
|
311
|
+
```
|
312
|
+
- Other setup/cleanup should also usually happen around each generated example rather than around the whole test: Instead of using the hooks exposed by RSpec/MiniTest/etc., use the before/after/around hooks exposed by PropCheck.
|
313
|
+
|
273
314
|
## Development
|
274
315
|
|
275
316
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -286,7 +327,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
286
327
|
|
287
328
|
## Code of Conduct
|
288
329
|
|
289
|
-
Everyone interacting in the PropCheck project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
330
|
+
Everyone interacting in the PropCheck project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Qqwy/ruby-prop_check/blob/master/CODE_OF_CONDUCT.md).
|
290
331
|
|
291
332
|
## Attribution and Thanks
|
292
333
|
|
@@ -294,7 +335,7 @@ I want to thank the original creators of QuickCheck (Koen Claessen, John Hughes)
|
|
294
335
|
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),
|
295
336
|
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
337
|
|
297
|
-
The implementation and API of PropCheck takes a lot of inspiration from the following
|
338
|
+
The implementation and API of PropCheck takes a lot of inspiration from the following projects:
|
298
339
|
|
299
340
|
- Haskell's [QuickCheck](https://hackage.haskell.org/package/QuickCheck) and [Hedgehog](https://hackage.haskell.org/package/hedgehog);
|
300
341
|
- Erlang's [PropEr](https://hex.pm/packages/proper);
|
data/lib/prop_check/generator.rb
CHANGED
@@ -116,7 +116,7 @@ module PropCheck
|
|
116
116
|
# and act on it.
|
117
117
|
#
|
118
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,
|
119
|
+
# => Date.new(2023, 01, 12)
|
120
120
|
def with_config
|
121
121
|
Generator.new do |**kwargs|
|
122
122
|
result = generate(**kwargs)
|
@@ -769,7 +769,7 @@ module PropCheck
|
|
769
769
|
# DateTimes start around the given `epoch:` and deviate more when `size` increases.
|
770
770
|
# when no epoch is set, `PropCheck::Property::Configuration.default_epoch` is used, which defaults to `DateTime.now`.
|
771
771
|
#
|
772
|
-
# >> PropCheck::Generators.datetime.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
772
|
+
# >> PropCheck::Generators.datetime(epoch: Date.new(2022, 11, 20)).sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
773
773
|
# => [DateTime.parse("2022-11-17 07:11:59.999983907 +0000"), DateTime.parse("2022-11-19 05:27:16.363618076 +0000")]
|
774
774
|
def datetime(epoch: nil)
|
775
775
|
datetime_from_offset(real_float, epoch: epoch)
|
@@ -785,7 +785,7 @@ module PropCheck
|
|
785
785
|
##
|
786
786
|
# Variant of `#datetime` that only generates datetimes in the future (relative to `:epoch`).
|
787
787
|
#
|
788
|
-
# >> PropCheck::Generators.future_datetime.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new).map(&:inspect)
|
788
|
+
# >> PropCheck::Generators.future_datetime(epoch: Date.new(2022, 11, 20)).sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new).map(&:inspect)
|
789
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
790
|
def future_datetime(epoch: nil)
|
791
791
|
datetime_from_offset(real_positive_float, epoch: epoch)
|
@@ -794,7 +794,7 @@ module PropCheck
|
|
794
794
|
##
|
795
795
|
# Variant of `#datetime` that only generates datetimes in the past (relative to `:epoch`).
|
796
796
|
#
|
797
|
-
# >> PropCheck::Generators.past_datetime.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
797
|
+
# >> PropCheck::Generators.past_datetime(epoch: Date.new(2022, 11, 20)).sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
798
798
|
# => [DateTime.parse("2022-11-17 07:11:59.999983907 +0000"), DateTime.parse("2022-11-19 05:27:16.363618076 +0000")]
|
799
799
|
def past_datetime(epoch: nil)
|
800
800
|
datetime_from_offset(real_negative_float, epoch: epoch)
|
@@ -805,7 +805,7 @@ module PropCheck
|
|
805
805
|
# Times start around the given `epoch:` and deviate more when `size` increases.
|
806
806
|
# when no epoch is set, `PropCheck::Property::Configuration.default_epoch` is used, which defaults to `DateTime.now`.
|
807
807
|
#
|
808
|
-
# >> PropCheck::Generators.time.sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
808
|
+
# >> PropCheck::Generators.time(epoch: Date.new(2022, 11, 20)).sample(2, rng: Random.new(42), config: PropCheck::Property::Configuration.new)
|
809
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
810
|
def time(epoch: nil)
|
811
811
|
datetime(epoch: epoch).map(&:to_time)
|
@@ -872,5 +872,75 @@ module PropCheck
|
|
872
872
|
end
|
873
873
|
end
|
874
874
|
end
|
875
|
+
|
876
|
+
##
|
877
|
+
# Helper to build recursive generators
|
878
|
+
#
|
879
|
+
# Given a `leaf_generator`
|
880
|
+
# and a block which:
|
881
|
+
# - is given a generator that generates subtrees.
|
882
|
+
# - it should return the generator for intermediate tree nodes.
|
883
|
+
#
|
884
|
+
# This is best explained with an example.
|
885
|
+
# Say we want to generate a binary tree of integers.
|
886
|
+
#
|
887
|
+
# If we have a struct representing internal nodes:
|
888
|
+
# ```ruby
|
889
|
+
# Branch = Struct.new(:left, :right, keyword_init: true)
|
890
|
+
# ```
|
891
|
+
# we can generate trees like so:
|
892
|
+
# ```ruby
|
893
|
+
# Generators.tree(Generators.integer) do |subtree_gen|
|
894
|
+
# G.instance(Branch, left: subtree_gen, right: subtree_gen)
|
895
|
+
# end
|
896
|
+
# ```
|
897
|
+
#
|
898
|
+
# As another example, consider generating lists of integers:
|
899
|
+
#
|
900
|
+
# >> G = PropCheck::Generators
|
901
|
+
# >> G.tree(G.integer) {|child_gen| G.array(child_gen) }.sample(5, size: 37, rng: Random.new(42))
|
902
|
+
# => [[7, [2, 3], -10], [[-2], [-2, [3]], [[2, 3]]], [], [0, [-2, -3]], [[1], -19, [], [1, -1], [1], [-1, -1], [1]]]
|
903
|
+
#
|
904
|
+
# And finally, here is how one could create a simple generator for parsed JSON data:
|
905
|
+
#
|
906
|
+
# ```ruby`
|
907
|
+
# G = PropCheck::Generators
|
908
|
+
# def json
|
909
|
+
# G.tree(G.one_of(G.boolean, G.real_float, G.ascii_string)) do |json_gen|
|
910
|
+
# G.one_of(G.array(json_gen), G.hash_of(G.ascii_string, json_gen))
|
911
|
+
# end
|
912
|
+
# end
|
913
|
+
# ```
|
914
|
+
#
|
915
|
+
def tree(leaf_generator, &block)
|
916
|
+
# Implementation is based on
|
917
|
+
# https://hexdocs.pm/stream_data/StreamData.html#tree/2
|
918
|
+
Generator.new do |size:, rng:, **other_kwargs|
|
919
|
+
nodes_on_each_level = random_pseudofactors(size.pow(1.1).to_i, rng)
|
920
|
+
result = nodes_on_each_level.reduce(leaf_generator) do |subtree_generator, nodes_on_this_level|
|
921
|
+
frequency(1 => subtree_generator,
|
922
|
+
2 => block.call(subtree_generator).resize { |_size| nodes_on_this_level })
|
923
|
+
end
|
924
|
+
|
925
|
+
result.generate(size: size, rng: rng, **other_kwargs)
|
926
|
+
end
|
927
|
+
end
|
928
|
+
|
929
|
+
private def random_pseudofactors(size, rng)
|
930
|
+
return [size].to_enum if size < 2
|
931
|
+
|
932
|
+
Enumerator.new do |yielder|
|
933
|
+
loop do
|
934
|
+
factor = rng.rand(1..(Math.log2(size).to_i))
|
935
|
+
if factor == 1
|
936
|
+
yielder << size
|
937
|
+
break
|
938
|
+
else
|
939
|
+
yielder << factor
|
940
|
+
size /= factor
|
941
|
+
end
|
942
|
+
end
|
943
|
+
end
|
944
|
+
end
|
875
945
|
end
|
876
946
|
end
|
data/lib/prop_check/property.rb
CHANGED
@@ -42,11 +42,8 @@ module PropCheck
|
|
42
42
|
# of this class on before finally passing a block to it using `#check`.
|
43
43
|
# (so `forall(Generators.integer) do |val| ... end` and forall(Generators.integer).check do |val| ... end` are the same)
|
44
44
|
def self.forall(*bindings, **kwbindings, &block)
|
45
|
-
|
46
|
-
|
47
|
-
return property.check(&block) if block_given?
|
48
|
-
|
49
|
-
property
|
45
|
+
new(*bindings, **kwbindings)
|
46
|
+
.check(&block)
|
50
47
|
end
|
51
48
|
|
52
49
|
##
|
@@ -106,9 +103,7 @@ module PropCheck
|
|
106
103
|
duplicate.instance_variable_set(:@config, @config.merge(config))
|
107
104
|
duplicate.freeze
|
108
105
|
|
109
|
-
|
110
|
-
|
111
|
-
duplicate
|
106
|
+
duplicate.check(&block)
|
112
107
|
end
|
113
108
|
|
114
109
|
##
|
@@ -248,6 +243,8 @@ module PropCheck
|
|
248
243
|
##
|
249
244
|
# Checks the property (after settings have been altered using the other instance methods in this class.)
|
250
245
|
def check(&block)
|
246
|
+
return self unless block_given?
|
247
|
+
|
251
248
|
n_runs = 0
|
252
249
|
n_successful = 0
|
253
250
|
|
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.18.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: 2022-11-
|
11
|
+
date: 2022-11-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: amazing_print
|