prop_check 0.6.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a8b10e33dfef86ed35b2a13ff0261552bbd2c7d7f39a6fa6997bedb4c57651f
4
- data.tar.gz: c2400550a28e15baa8b4d7323adea18ec3aa858384f774b76c59f6ff87f38c82
3
+ metadata.gz: 8e5aeef61ddf82569ca885327d32b9fe8fd939f2fdbef48709cb19504b5cf041
4
+ data.tar.gz: d914dcac32f7a3a976661a0280c71f0a7a665411550aed9902317b04c82ee290
5
5
  SHA512:
6
- metadata.gz: 758c27825f78f4d51ad6ff076f5866d2c0d7ee0a0b6f150b4247d9acb6804676c10d1223278506a2c1f2b1ecd683d8951f4c2331d6e0063cd8039fd6dedbdd94
7
- data.tar.gz: f3db2b04c823a7bc29afa6dd23e58c1c7696873d836b2f42ad0af70f150ba623fbd67b8204e521c544e0a7600bd094c0b4971d6f2ceac37c456e1a050716aac5
6
+ metadata.gz: 85c5b76ac37ba8e03b2c41c04c4cc359844fa2e52d0af8893787531c23f3a54b046eb7eed13b055fed44c8983f87cbfe492d1524746624a3744300a73caf01da
7
+ data.tar.gz: 9beb7ac4e605c72cc8e9b3907e727ae532c2eec4a530b6a9b7c31445135e37c8849c9f186d4571aaf38c24e28989ebe856ed2e5e873baf2ed7b740a73561dae7
@@ -1 +1 @@
1
- ruby 2.5.1
1
+ ruby 2.6.5
@@ -4,4 +4,15 @@ language: ruby
4
4
  cache: bundler
5
5
  rvm:
6
6
  - 2.5.1
7
- before_install: gem install bundler -v 2.0.1
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
@@ -0,0 +1 @@
1
+ - 0.8.0 New syntax that is more explicit, passng generated values to blocks as parameters.
data/Gemfile CHANGED
@@ -5,3 +5,4 @@ gemspec
5
5
 
6
6
  gem 'simplecov', require: false, group: :test
7
7
  gem 'doctest-rspec', require: false, group: :test
8
+ gem 'awesome_print', require: true
@@ -1,11 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- prop_check (0.6.0)
4
+ prop_check (0.9.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
+ awesome_print (1.8.0)
9
10
  diff-lcs (1.3)
10
11
  docile (1.3.2)
11
12
  doctest-core (0.0.2)
@@ -13,7 +14,7 @@ GEM
13
14
  doctest-core (~> 0.0.2)
14
15
  rspec
15
16
  json (2.2.0)
16
- rake (10.5.0)
17
+ rake (12.3.3)
17
18
  rspec (3.8.0)
18
19
  rspec-core (~> 3.8.0)
19
20
  rspec-expectations (~> 3.8.0)
@@ -37,12 +38,13 @@ PLATFORMS
37
38
  ruby
38
39
 
39
40
  DEPENDENCIES
41
+ awesome_print
40
42
  bundler (~> 2.0)
41
43
  doctest-rspec
42
44
  prop_check!
43
- rake (~> 10.0)
45
+ rake (~> 12.3)
44
46
  rspec (~> 3.0)
45
47
  simplecov
46
48
 
47
49
  BUNDLED WITH
48
- 2.0.2
50
+ 2.1.4
data/README.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  PropCheck allows you to do Property Testing in Ruby.
4
4
 
5
+ [![Gem](https://img.shields.io/gem/v/prop_check.svg)](https://rubygems.org/gems/prop_check)
6
+ [![Build Status](https://travis-ci.org/Qqwy/ruby-prop_check.svg?branch=master)](https://travis-ci.org/Qqwy/ruby-prop_check)
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/PropCheck)
9
+
5
10
  It features:
6
11
 
7
12
  - Generators for common datatypes.
@@ -9,7 +14,7 @@ It features:
9
14
  - Shrinking to a minimal counter-example on failure.
10
15
 
11
16
 
12
- ## TODOs before release
17
+ ## TODOs before stable release
13
18
 
14
19
  Before releasing this gem on Rubygems, the following things need to be finished:
15
20
 
@@ -21,11 +26,11 @@ Before releasing this gem on Rubygems, the following things need to be finished:
21
26
  - [x] Stop after a ludicrous amount of generator runs, to prevent malfunctioning (infinitely looping) generators from blowing up someone's computer.
22
27
  - [x] Look into customization of settings from e.g. command line arguments.
23
28
  - [x] Good, unicode-compliant, string generators.
24
- - [ ] Filtering generator outputs.
29
+ - [x] Filtering generator outputs.
25
30
 
26
31
  # Nice-to-haves
27
32
 
28
- - [ ] Basic integration with RSpec. See also https://groups.google.com/forum/#!msg/rspec/U-LmL0OnO-Y/iW_Jcd6JBAAJ for progress on this.
33
+ - [x] Basic integration with RSpec. See also https://groups.google.com/forum/#!msg/rspec/U-LmL0OnO-Y/iW_Jcd6JBAAJ for progress on this.
29
34
  - [ ] `aggregate` , `resize` and similar generator-modifying calls (c.f. PropEr's variants of these) which will help with introspection/metrics.
30
35
  - [ ] Integration with other Ruby test frameworks.
31
36
  - Stateful property testing. If implemented at some point, will probably happen in a separate add-on library.
@@ -61,8 +66,9 @@ _(to be precise: a method on the execution context is defined which returns the
61
66
  Raise an exception from the block if there is a problem. If there is no problem, just return normally.
62
67
 
63
68
  ```ruby
69
+ include PropCheck::Generators
64
70
  # testing that Enumerable#sort sorts in ascending order
65
- PropCheck.forall(numbers: array(integer())) do
71
+ PropCheck.forall(array(integer)) do |numbers|
66
72
  sorted_numbers = numbers.sort
67
73
 
68
74
  # Check that no number is smaller than the previous number
@@ -72,6 +78,50 @@ PropCheck.forall(numbers: array(integer())) do
72
78
  end
73
79
  ```
74
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(numbers: array(integer)) do |numbers:|
94
+ result = naive_average(numbers)
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
+ :numbers => []
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
+
75
125
  #### Shrinking
76
126
 
77
127
  When a failure is found, PropCheck will re-run the block given to `forall` to test
@@ -83,8 +133,8 @@ PropCheck will see if the failure still happens with `x = 50`.
83
133
  If it does , it will try `x = 25`. If not, it will try `x = 75`, and so on.
84
134
 
85
135
  This means if something only goes wrong for `x = 2`, the program will try:
86
- - `x = 100`(fails),`
87
- - x = 50`(fails),
136
+ - `x = 100`(fails),
137
+ - `x = 50`(fails),
88
138
  - `x = 25`(fails),
89
139
  - `x = 12`(fails),
90
140
  - `x = 6`(fails),
@@ -101,21 +151,28 @@ A short summary:
101
151
  - Arrays and hashes shrink to fewer elements, as well as shrinking their elements.
102
152
  - Strings shrink to shorter strings, as well as characters earlier in their alphabet.
103
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.
104
168
 
105
169
  ### Writing Custom Generators
106
170
 
107
- PropCheck comes bundled with a bunch of common generators, for:
108
- - integers
109
- - floats
110
- - strings
111
- - symbols
112
- - arrays
113
- - hashes
114
- etc.
171
+ As described in the previous section, PropCheck already comes bundled with a bunch of common generators.
115
172
 
116
173
  However, you can easily adapt them to generate your own datatypes:
117
174
 
118
- #### Generator#wrap
175
+ #### Generators#constant / Generator#wrap
119
176
 
120
177
  Always returns the given value. No shrinking.
121
178
 
@@ -157,6 +214,9 @@ you can use `Generators.frequency` which takes a hash of (integer_frequency => g
157
214
  There are even more functions in the `Generator` class and the `Generators` module that you might want to use,
158
215
  although above are the most generally useful ones.
159
216
 
217
+ [PropCheck::Generator documentation](https://www.rubydoc.info/github/Qqwy/ruby-prop_check/master/PropCheck/Generator)
218
+ [PropCheck::Generators documentation](https://www.rubydoc.info/github/Qqwy/ruby-prop_check/master/PropCheck/Generators)
219
+
160
220
  ## Development
161
221
 
162
222
  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.
@@ -3,14 +3,33 @@ require 'prop_check/property'
3
3
  require 'prop_check/generator'
4
4
  require 'prop_check/generators'
5
5
  require 'prop_check/helper'
6
+ ##
7
+ # Main module of the PropCheck library.
8
+ #
9
+ # You probably want to look at the documentation of
10
+ # PropCheck::Generator and PropCheck::Generators
11
+ # to find out more about how to use generators.
12
+ #
13
+ # Common usage is to call `extend PropCheck` in your (testing) modules.
14
+ #
15
+ # This will:
16
+ # 1. Add the local method `forall` which will call `PropCheck.forall`
17
+ # 2. `include PropCheck::Generators`.
18
+ #
6
19
  module PropCheck
7
- class Error < StandardError; end
8
- class UserError < Error; end
9
- class GeneratorExhaustedError < UserError; end
10
- class MaxShrinkStepsExceededError < UserError; end
20
+ module Errors
21
+ class Error < StandardError; end
22
+ class UserError < Error; end
23
+ class GeneratorExhaustedError < UserError; end
24
+ class MaxShrinkStepsExceededError < UserError; end
25
+ end
11
26
 
12
27
  extend self
13
28
 
29
+ ##
30
+ # Runs a property.
31
+ #
32
+ # See the README for more details.
14
33
  def forall(*args, **kwargs, &block)
15
34
  PropCheck::Property.forall(*args, **kwargs, &block)
16
35
  end
@@ -72,7 +72,7 @@ module PropCheck
72
72
  # end.flatten
73
73
  # end
74
74
  Generator.new do |size, rng|
75
- outer_result = generate(size, rng)
75
+ outer_result = self.generate(size, rng)
76
76
  outer_result.bind do |outer_val|
77
77
  inner_generator = generator_proc.call(outer_val)
78
78
  inner_generator.generate(size, rng)
@@ -91,5 +91,24 @@ module PropCheck
91
91
  result.map(&proc)
92
92
  end
93
93
  end
94
+
95
+ ##
96
+ # Creates a new Generator that only produces a value when the block `condition` returns a truthy value.
97
+ def where(&condition)
98
+ self.map do |result|
99
+ if condition.call(*result)
100
+ result
101
+ else
102
+ :"_PropCheck.filter_me"
103
+ end
104
+ end
105
+ # self.map do |*result|
106
+ # if condition.call(*result)
107
+ # result
108
+ # else
109
+ # :'_PropCheck.filter_me'
110
+ # end
111
+ # end
112
+ end
94
113
  end
95
114
  end
@@ -1,3 +1,6 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  require 'prop_check/generator'
2
5
  require 'prop_check/lazy_tree'
3
6
  module PropCheck
@@ -116,27 +119,40 @@ module PropCheck
116
119
  end
117
120
 
118
121
  ##
119
- # Generates floating point numbers
122
+ # Generates floating-point numbers
120
123
  # These start small (around 0)
121
124
  # and become more extreme (large positive and large negative numbers)
122
125
  #
126
+ # Will only generate 'reals',
127
+ # that is: no infinity, no NaN,
128
+ # no numbers testing the limits of floating-point arithmetic.
123
129
  #
124
130
  # Shrinks to numbers closer to zero.
125
131
  #
126
- # TODO testing for NaN, Infinity?
127
- def float
128
- # integer.bind do |a|
129
- # integer.bind do |b|
130
- # integer.bind do |c|
131
- # Generator.wrap(fraction(a, b, c))
132
- # end
133
- # end
134
- # end
132
+ # >> Generators.real_float().sample(10, size: 10, rng: Random.new(42))
133
+ # => [-2.2, -0.2727272727272727, 4.0, 1.25, -3.7272727272727275, -8.833333333333334, -8.090909090909092, 1.1428571428571428, 0.0, 8.0]
134
+ def real_float
135
135
  tuple(integer, integer, integer).map do |a, b, c|
136
136
  fraction(a, b, c)
137
137
  end
138
138
  end
139
139
 
140
+ @special_floats = [Float::NAN, Float::INFINITY, -Float::INFINITY, Float::MAX, Float::MIN, 0.0.next_float, 0.0.prev_float]
141
+ ##
142
+ # Generates floating-point numbers
143
+ # Will generate NaN, Infinity, -Infinity,
144
+ # as well as Float::EPSILON, Float::MAX, Float::MIN,
145
+ # 0.0.next_float, 0.0.prev_float,
146
+ # to test the handling of floating-point edge cases.
147
+ # Approx. 1/100 generated numbers is a special one.
148
+ #
149
+ # Shrinks to smaller, real floats.
150
+ # >> Generators.float().sample(10, size: 10, rng: Random.new(42))
151
+ # => [4.0, 9.555555555555555, 0.0, -Float::INFINITY, 5.5, -5.818181818181818, 1.1428571428571428, 0.0, 8.0, 7.857142857142858]
152
+ def float
153
+ frequency(99 => real_float, 1 => one_of(*@special_floats.map(&method(:constant))))
154
+ end
155
+
140
156
  ##
141
157
  # Picks one of the given generators in `choices` at random uniformly every time.
142
158
  #
@@ -156,6 +172,9 @@ module PropCheck
156
172
  # (representing the relative frequency of this generator)
157
173
  # and values to be generators.
158
174
  #
175
+ # Side note: If you want to use the same frequency number for multiple generators,
176
+ # Ruby syntax requires you to send an array of two-element arrays instead of a hash.
177
+ #
159
178
  # Shrinks to arbitrary elements (since hashes are not ordered).
160
179
  #
161
180
  # >> Generators.frequency(5 => Generators.integer, 1 => Generators.printable_ascii_char).sample(size: 10, rng: Random.new(42))
@@ -174,7 +193,7 @@ module PropCheck
174
193
  #
175
194
  # Shrinks element generators, one at a time (trying last one first).
176
195
  #
177
- # >> Generators.tuple(Generators.integer, Generators.float).call(10, Random.new(42))
196
+ # >> Generators.tuple(Generators.integer, Generators.real_float).call(10, Random.new(42))
178
197
  # => [-4, 13.0]
179
198
  def tuple(*generators)
180
199
  Generator.new do |size, rng|
@@ -191,7 +210,7 @@ module PropCheck
191
210
  #
192
211
  # Shrinks element generators.
193
212
  #
194
- # >> Generators.fixed_hash(a: Generators.integer(), b: Generators.float(), c: Generators.integer()).call(10, Random.new(42))
213
+ # >> Generators.fixed_hash(a: Generators.integer(), b: Generators.real_float(), c: Generators.integer()).call(10, Random.new(42))
195
214
  # => {:a=>-4, :b=>13.0, :c=>-3}
196
215
  def fixed_hash(hash)
197
216
  keypair_generators =
@@ -242,7 +261,9 @@ module PropCheck
242
261
  # containing one of a..z, A..Z, 0..9
243
262
  #
244
263
  # Shrinks towards lowercase 'a'.
245
- #
264
+ #
265
+ # >> Generators.alphanumeric_char.sample(5, size: 10, rng: Random.new(42))
266
+ # => ["M", "Z", "C", "o", "Q"]
246
267
  def alphanumeric_char
247
268
  one_of(*@alphanumeric_chars.map(&method(:constant)))
248
269
  end
@@ -250,7 +271,11 @@ module PropCheck
250
271
  ##
251
272
  # Generates a string
252
273
  # containing only the characters a..z, A..Z, 0..9
274
+ #
253
275
  # Shrinks towards fewer characters, and towards lowercase 'a'.
276
+ #
277
+ # >> Generators.alphanumeric_string.sample(5, size: 10, rng: Random.new(42))
278
+ # => ["ZCoQ", "8uM", "wkkx0JNx", "v0bxRDLb", "Gl5v8RyWA6"]
254
279
  def alphanumeric_string
255
280
  array(alphanumeric_char).map(&:join)
256
281
  end
@@ -272,7 +297,11 @@ module PropCheck
272
297
  ##
273
298
  # Generates strings
274
299
  # from the printable ASCII character set.
300
+ #
275
301
  # Shrinks towards fewer characters, and towards ' '.
302
+ #
303
+ # >> Generators.printable_ascii_string.sample(5, size: 10, rng: Random.new(42))
304
+ # => ["S|.g", "rvjjw7\"5T!", "=", "!_[4@", "Y"]
276
305
  def printable_ascii_string
277
306
  array(printable_ascii_char).map(&:join)
278
307
  end
@@ -295,7 +324,11 @@ module PropCheck
295
324
  ##
296
325
  # Generates a single-character string
297
326
  # from the printable ASCII character set.
327
+ #
298
328
  # Shrinks towards '\n'.
329
+ #
330
+ # >> Generators.ascii_char.sample(size: 10, rng: Random.new(42))
331
+ # => ["d", "S", "|", ".", "g", "\\", "4", "d", "r", "v"]
299
332
  def ascii_char
300
333
  one_of(*@ascii_chars.map(&method(:constant)))
301
334
  end
@@ -303,7 +336,11 @@ module PropCheck
303
336
  ##
304
337
  # Generates strings
305
338
  # from the printable ASCII character set.
339
+ #
306
340
  # Shrinks towards fewer characters, and towards '\n'.
341
+ #
342
+ # >> Generators.ascii_string.sample(5, size: 10, rng: Random.new(42))
343
+ # => ["S|.g", "drvjjw\b\a7\"", "!w=E!_[4@k", "x", "zZI{[o"]
307
344
  def ascii_string
308
345
  array(ascii_char).map(&:join)
309
346
  end
@@ -321,6 +358,8 @@ module PropCheck
321
358
  #
322
359
  # Shrinks towards characters with lower codepoints, e.g. ASCII
323
360
  #
361
+ # >> Generators.printable_char.sample(size: 10, rng: Random.new(42))
362
+ # => ["吏", "", "", "", "", "", "", "", "", "Ȍ"]
324
363
  def printable_char
325
364
  one_of(*@printable_chars.map(&method(:constant)))
326
365
  end
@@ -331,6 +370,8 @@ module PropCheck
331
370
  #
332
371
  # Shrinks towards shorter strings, and towards characters with lower codepoints, e.g. ASCII
333
372
  #
373
+ # >> Generators.printable_string.sample(5, size: 10, rng: Random.new(42))
374
+ # => ["", "Ȍ", "𐁂", "Ȕ", ""]
334
375
  def printable_string
335
376
  array(printable_char).map(&:join)
336
377
  end
@@ -341,6 +382,8 @@ module PropCheck
341
382
  #
342
383
  # Shrinks towards characters with lower codepoints, e.g. ASCII
343
384
  #
385
+ # >> Generators.printable_char.sample(size: 10, rng: Random.new(42))
386
+ # => ["吏", "", "", "", "", "", "", "", "", "Ȍ"]
344
387
  def char
345
388
  choose(0..0x10FFFF).map do |num|
346
389
  [num].pack('U')
@@ -353,6 +396,8 @@ module PropCheck
353
396
  #
354
397
  # Shrinks towards characters with lower codepoints, e.g. ASCII
355
398
  #
399
+ # >> Generators.string.sample(5, size: 10, rng: Random.new(42))
400
+ # => ["\u{A3DB3}𠍜\u{3F46A}\u{1AEBC}", "􍙦𡡹󴇒\u{DED74}𪱣\u{43E97}ꂂ\u{50695}􏴴\u{C0301}", "\u{4FD9D}", "\u{C14BF}\u{193BB}𭇋󱣼\u{76B58}", "𦐺\u{9FDDB}\u{80ABB}\u{9E3CF}𐂽\u{14AAE}"]
356
401
  def string
357
402
  array(char).map(&:join)
358
403
  end
@@ -361,7 +406,9 @@ module PropCheck
361
406
  # Generates either `true` or `false`
362
407
  #
363
408
  # Shrinks towards `false`
364
- #
409
+ #
410
+ # >> Generators.boolean.sample(5, size: 10, rng: Random.new(42))
411
+ # => [false, true, false, false, false]
365
412
  def boolean
366
413
  one_of(constant(false), constant(true))
367
414
  end
@@ -370,6 +417,9 @@ module PropCheck
370
417
  # Generates always `nil`.
371
418
  #
372
419
  # Does not shrink.
420
+ #
421
+ # >> Generators.nil.sample(5, size: 10, rng: Random.new(42))
422
+ # => [nil, nil, nil, nil, nil]
373
423
  def nil
374
424
  constant(nil)
375
425
  end
@@ -379,15 +429,34 @@ module PropCheck
379
429
  #
380
430
  # Shrinks towards `nil`.
381
431
  #
432
+ # >> Generators.falsey.sample(5, size: 10, rng: Random.new(42))
433
+ # => [nil, false, nil, nil, nil]
382
434
  def falsey
383
435
  one_of(constant(nil), constant(false))
384
436
  end
385
437
 
438
+ ##
439
+ # Generates symbols consisting of lowercase letters and potentially underscores.
440
+ #
441
+ # Shrinks towards shorter symbols and the letter 'a'.
442
+ #
443
+ # >> Generators.simple_symbol.sample(5, size: 10, rng: Random.new(42))
444
+ # => [:tokh, :gzswkkxudh, :vubxlfbu, :lzvlyq__jp, :oslw]
445
+ def simple_symbol
446
+ alphabet = ('a'..'z').to_a
447
+ alphabet << '_'
448
+ array(one_of(*alphabet.map(&method(:constant))))
449
+ .map(&:join)
450
+ .map(&:to_sym)
451
+ end
452
+
386
453
  ##
387
454
  # Generates common terms that are not `nil` or `false`.
388
455
  #
389
456
  # Shrinks towards simpler terms, like `true`, an empty array, a single character or an integer.
390
457
  #
458
+ # >> Generators.truthy.sample(5, size: 10, rng: Random.new(42))
459
+ # => [[4, 0, -3, 10, -4, 8, 0, 0, 10], -3, [5.5, -5.818181818181818, 1.1428571428571428, 0.0, 8.0, 7.857142857142858, -0.6666666666666665, 5.25], [], ["\u{9E553}\u{DD56E}\u{A5BBB}\u{8BDAB}\u{3E9FC}\u{C4307}\u{DAFAE}\u{1A022}\u{938CD}\u{70631}", "\u{C4C01}\u{32D85}\u{425DC}"]]
391
460
  def truthy
392
461
  one_of(constant(true),
393
462
  constant([]),
@@ -399,7 +468,7 @@ module PropCheck
399
468
  array(float),
400
469
  array(char),
401
470
  array(string),
402
- hash(symbol, integer),
471
+ hash(simple_symbol, integer),
403
472
  hash(string, integer),
404
473
  hash(string, string)
405
474
  )
@@ -408,6 +477,9 @@ module PropCheck
408
477
  ##
409
478
  # Generates whatever `other_generator` generates
410
479
  # but sometimes instead `nil`.`
480
+ #
481
+ # >> Generators.nillable(Generators.integer).sample(20, size: 10, rng: Random.new(42))
482
+ # => [9, 10, 8, 0, 10, -3, -8, 10, 1, -9, -10, nil, 1, 6, nil, 1, 9, -8, 8, 10]
411
483
  def nillable(other_generator)
412
484
  frequency(9 => other_generator, 1 => constant(nil))
413
485
  end
@@ -0,0 +1,18 @@
1
+ module PropCheck
2
+ module Helper
3
+ ##
4
+ # A refinement for enumerators
5
+ # to allow lazy appending of two (potentially lazy) enumerators:
6
+ # >> [1,2,3].lazy_append([4,5.6]).to_a
7
+ # => [1,2,3,4,5,6]
8
+ module LazyAppend
9
+ refine Enumerable do
10
+ ## >> [1,2,3].lazy_append([4,5.6]).to_a
11
+ ## => [1,2,3,4,5,6]
12
+ def lazy_append(other_enumerator)
13
+ [self, other_enumerator].lazy.flat_map(&:lazy)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,22 +1,13 @@
1
- ##
2
- # A refinement for enumerators
3
- # to allow lazy appending of two (potentially lazy) enumerators:
4
- module LazyAppend
5
- refine Enumerable do
6
- ## >> [1,2,3].lazy_append([4,5.6]).to_a
7
- ## => [1,2,3,4,5,6]
8
- def lazy_append(other_enumerator)
9
- [self, other_enumerator].lazy.flat_map(&:lazy)
10
- end
11
- end
12
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'prop_check/helper/lazy_append'
13
4
 
14
5
  module PropCheck
15
6
  ##
16
7
  # A Rose tree with the root being eager,
17
8
  # and the children computed lazily, on demand.
18
9
  class LazyTree
19
- using LazyAppend
10
+ using PropCheck::Helper::LazyAppend
20
11
 
21
12
  attr_accessor :root, :children
22
13
  def initialize(root, children = [].lazy)
@@ -82,7 +73,8 @@ module PropCheck
82
73
  [tree.root].lazy_append(new_children)
83
74
  end
84
75
 
85
- squish.call(self, [])
76
+ squish
77
+ .call(self, [])
86
78
 
87
79
  # base = [root]
88
80
  # recursive = children.map(&:each)
@@ -108,7 +100,8 @@ module PropCheck
108
100
  # >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).to_a
109
101
  # => [1, 4, 2, 3]
110
102
  def to_a
111
- each.force
103
+ each
104
+ .force
112
105
  end
113
106
 
114
107
  # TODO: fix implementation
@@ -1,41 +1,90 @@
1
1
  require 'stringio'
2
+ require "awesome_print"
2
3
 
3
4
  require 'prop_check/property/configuration'
4
- require 'prop_check/property/check_evaluator'
5
5
  module PropCheck
6
+ ##
7
+ # Run properties
6
8
  class Property
7
9
 
8
- def self.forall(**bindings, &block)
9
-
10
- property = new(bindings)
10
+ ##
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
+ #
30
+ #
31
+ # If you do not pass a block right away,
32
+ # a Property object is returned, which you can call the other instance methods
33
+ # of this class on before finally passing a block to it using `#check`.
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)
36
+
37
+ property = new(*bindings)
11
38
 
12
39
  return property.check(&block) if block_given?
13
40
 
14
41
  property
15
42
  end
16
43
 
44
+ ##
45
+ # Returns the default configuration of the library as it is configured right now
46
+ # for introspection.
47
+ #
48
+ # For the configuration of a single property, check its `configuration` instance method.
49
+ # See PropCheck::Property::Configuration for more info on available settings.
17
50
  def self.configuration
18
51
  @configuration ||= Configuration.new
19
52
  end
20
53
 
54
+ ##
55
+ # Yields the library's configuration object for you to alter.
56
+ # See PropCheck::Property::Configuration for more info on available settings.
21
57
  def self.configure
22
58
  yield(configuration)
23
59
  end
24
60
 
25
61
  attr_reader :bindings, :condition
26
62
 
27
- def initialize(**bindings)
28
- raise ArgumentError, 'No bindings specified!' if bindings.empty?
63
+ def initialize(*bindings, **kwbindings)
64
+ raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
29
65
 
30
66
  @bindings = bindings
31
- @condition = -> { true }
67
+ @kwbindings = kwbindings
68
+ @condition = proc { true }
32
69
  @config = self.class.configuration
33
70
  end
34
71
 
72
+ ##
73
+ # Returns the configuration of this property
74
+ # for introspection.
75
+ #
76
+ # See PropCheck::Property::Configuration for more info on available settings.
35
77
  def configuration
36
78
  @config
37
79
  end
38
80
 
81
+ ##
82
+ # Allows you to override the configuration of this property
83
+ # by giving a hash with new settings.
84
+ #
85
+ # If no other changes need to occur before you want to check the property,
86
+ # you can immediately pass a block to this method.
87
+ # (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`)
39
88
  def with_config(**config, &block)
40
89
  @config = @config.merge(config)
41
90
 
@@ -44,15 +93,35 @@ module PropCheck
44
93
  self
45
94
  end
46
95
 
47
- def where(&new_condition)
96
+ ##
97
+ # filters the generator using the given `condition`.
98
+ # The final property checking block will only be run if the condition is truthy.
99
+ #
100
+ # If wanted, multiple `where`-conditions can be specified on a property.
101
+ # Be aware that if you filter away too much generated inputs,
102
+ # you might encounter a GeneratorExhaustedError.
103
+ # Only filter if you have few inputs to reject. Otherwise, improve your generators.
104
+ def where(&condition)
48
105
  original_condition = @condition.dup
49
- @condition = -> { instance_exec(&original_condition) && instance_exec(&new_condition) }
106
+ @condition = proc do |**kwargs|
107
+ original_condition.call(**kwargs) && condition.call(**kwargs)
108
+ end
50
109
 
51
110
  self
52
111
  end
53
112
 
113
+ ##
114
+ # Checks the property (after settings have been altered using the other instance methods in this class.)
54
115
  def check(&block)
55
- binding_generator = PropCheck::Generators.fixed_hash(bindings)
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)
56
125
 
57
126
  n_runs = 0
58
127
  n_successful = 0
@@ -70,7 +139,11 @@ module PropCheck
70
139
  private def ensure_not_exhausted!(n_runs)
71
140
  return if n_runs >= @config.n_runs
72
141
 
73
- raise GeneratorExhaustedError, """
142
+ raise_generator_exhausted!
143
+ end
144
+
145
+ private def raise_generator_exhausted!()
146
+ raise Errors::GeneratorExhaustedError, """
74
147
  Could not perform `n_runs = #{@config.n_runs}` runs,
75
148
  (exhausted #{@config.max_generate_attempts} tries)
76
149
  because too few generator results were adhering to
@@ -81,7 +154,7 @@ module PropCheck
81
154
  end
82
155
 
83
156
  private def check_attempt(generator_result, n_successful, &block)
84
- CheckEvaluator.new(generator_result.root, &block).call
157
+ block.call(*generator_result.root)
85
158
 
86
159
  # immediately stop (without shrinnking) for when the app is asked
87
160
  # to close by outside intervention
@@ -117,8 +190,8 @@ module PropCheck
117
190
  (0...@config.max_generate_attempts)
118
191
  .lazy
119
192
  .map { binding_generator.generate(size, rng) }
120
- .reject { |val| val.root == :"_PropCheck.filter_me" }
121
- .select { |val| CheckEvaluator.new(val.root, &@condition).call }
193
+ .reject { |val| val.root.any? { |elem| elem == :"_PropCheck.filter_me" }}
194
+ .select { |val| @condition.call(*val.root) }
122
195
  .map do |result|
123
196
  n_runs += 1
124
197
  size += 1
@@ -131,7 +204,7 @@ module PropCheck
131
204
  private def show_problem_output(problem, generator_results, n_successful, &block)
132
205
  output = @config.verbose ? STDOUT : StringIO.new
133
206
  output = pre_output(output, n_successful, generator_results.root, problem)
134
- shrunken_result, shrunken_exception, n_shrink_steps = shrink2(generator_results, output, &block)
207
+ shrunken_result, shrunken_exception, n_shrink_steps = shrink(generator_results, output, &block)
135
208
  output = post_output(output, n_shrink_steps, shrunken_result, shrunken_exception)
136
209
 
137
210
  [output, shrunken_result, shrunken_exception, n_shrink_steps]
@@ -151,24 +224,29 @@ module PropCheck
151
224
  end
152
225
 
153
226
  private def post_output(output, n_shrink_steps, shrunken_result, shrunken_exception)
154
- output.puts ''
155
- output.puts "Shrunken input (after #{n_shrink_steps} shrink steps):"
156
- output.puts "`#{print_roots(shrunken_result)}`"
157
- output.puts ""
158
- output.puts "Shrunken exception:\n---\n#{shrunken_exception}"
159
- output.puts "---"
160
- output.puts ""
161
-
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
162
238
  output
163
239
  end
164
240
 
165
- private def print_roots(lazy_tree_hash)
166
- lazy_tree_hash.map do |name, val|
167
- "#{name} = #{val.inspect}"
168
- end.join(", ")
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
169
247
  end
170
248
 
171
- private def shrink2(bindings_tree, io, &fun)
249
+ private def shrink(bindings_tree, io, &block)
172
250
  io.puts 'Shrinking...' if @config.verbose
173
251
  problem_child = bindings_tree
174
252
  siblings = problem_child.children.lazy
@@ -190,12 +268,12 @@ module PropCheck
190
268
  io.print '.' if @config.verbose
191
269
 
192
270
  begin
193
- CheckEvaluator.new(sibling.root, &fun).call
194
- rescue Exception => problem
271
+ block.call(*sibling.root)
272
+ rescue Exception => e
195
273
  problem_child = sibling
196
274
  parent_siblings = siblings
197
275
  siblings = problem_child.children.lazy
198
- problem_exception = problem
276
+ problem_exception = e
199
277
  end
200
278
  end
201
279
 
@@ -1,3 +1,3 @@
1
1
  module PropCheck
2
- VERSION = "0.6.1"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -37,6 +37,6 @@ Gem::Specification.new do |spec|
37
37
  spec.required_ruby_version = '>= 2.5.1'
38
38
 
39
39
  spec.add_development_dependency "bundler", "~> 2.0"
40
- spec.add_development_dependency "rake", "~> 10.0"
40
+ spec.add_development_dependency "rake", "~> 12.3"
41
41
  spec.add_development_dependency "rspec", "~> 3.0"
42
42
  end
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.6.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Qqwy/Wiebe-Marten Wijnja
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-07-03 00:00:00.000000000 Z
11
+ date: 2020-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '12.3'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '12.3'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -81,11 +81,10 @@ files:
81
81
  - lib/prop_check/generator.rb
82
82
  - lib/prop_check/generators.rb
83
83
  - lib/prop_check/helper.rb
84
+ - lib/prop_check/helper/lazy_append.rb
84
85
  - lib/prop_check/lazy_tree.rb
85
86
  - lib/prop_check/property.rb
86
- - lib/prop_check/property/check_evaluator.rb
87
87
  - lib/prop_check/property/configuration.rb
88
- - lib/prop_check/rspec.rb
89
88
  - lib/prop_check/version.rb
90
89
  - prop_check.gemspec
91
90
  homepage: https://github.com/Qqwy/ruby-prop_check/
@@ -95,7 +94,7 @@ metadata:
95
94
  homepage_uri: https://github.com/Qqwy/ruby-prop_check/
96
95
  source_code_uri: https://github.com/Qqwy/ruby-prop_check/
97
96
  changelog_uri: https://github.com/Qqwy/ruby-prop_check/CHANGELOG.md
98
- post_install_message:
97
+ post_install_message:
99
98
  rdoc_options: []
100
99
  require_paths:
101
100
  - lib
@@ -110,9 +109,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
109
  - !ruby/object:Gem::Version
111
110
  version: '0'
112
111
  requirements: []
113
- rubyforge_project:
114
- rubygems_version: 2.7.6
115
- signing_key:
112
+ rubygems_version: 3.0.3
113
+ signing_key:
116
114
  specification_version: 4
117
115
  summary: PropCheck allows you to do property-based testing, including shrinking.
118
116
  test_files: []
@@ -1,45 +0,0 @@
1
- module PropCheck
2
- class Property
3
- ##
4
- # A wrapper class that implements the 'Cloaker' concept
5
- # which allows us to refer to variables set in 'bindings',
6
- # while still being able to access things that are only in scope
7
- # in the creator of '&block'.
8
- #
9
- # This allows us to bind the variables specified in `bindings`
10
- # one way during checking and another way during shrinking.
11
- class CheckEvaluator
12
- include RSpec::Matchers if Object.const_defined?('RSpec')
13
-
14
- def initialize(bindings, &block)
15
- @caller = block.binding.receiver
16
- @block = block
17
- define_named_instance_methods(bindings)
18
- end
19
-
20
- def call
21
- self.instance_exec(&@block)
22
- end
23
-
24
- private def define_named_instance_methods(results)
25
- results.each do |name, result|
26
- define_singleton_method(name) { result }
27
- end
28
- end
29
-
30
- ##
31
- # Dispatches to caller whenever something is not part of `bindings`.
32
- # (No need to invoke this method manually)
33
- def method_missing(method, *args, &block)
34
- @caller.__send__(method, *args, &block) || super
35
- end
36
-
37
- ##
38
- # Checks respond_to of caller whenever something is not part of `bindings`.
39
- # (No need to invoke this method manually)
40
- def respond_to_missing?(*args)
41
- @caller.respond_to?(*args) || super
42
- end
43
- end
44
- end
45
- end
@@ -1,14 +0,0 @@
1
- module PropCheck
2
- ##
3
- # Integration with RSpec
4
- module RSpec
5
- # To make it available within examples
6
- def self.extend_object(obj)
7
- obj.define_method(:forall) do |*args, **kwargs, &block|
8
- PropCheck::Property.forall(*args, **kwargs) do
9
- instance_exec(self, &block)
10
- end
11
- end
12
- end
13
- end
14
- end