prop_check 0.6.0 → 0.8.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: add08010531772da827ece44a91bdeb52d259374cf9bb966f05104f29e5a8ae8
4
- data.tar.gz: 246d3a3df6d8c53f198f489dcd6174160379dfe504558c79932bd0d0c78a2733
3
+ metadata.gz: 71879bc6575991fe6582a3f98213ee01fb5c9230cf042f95769eea801c366b1f
4
+ data.tar.gz: 9e351641ffb936461634a871cb984efff16c3ff5e9de0e60d411d33385786ec4
5
5
  SHA512:
6
- metadata.gz: 6b95914aab13287ce63e98a21db6add53a05f5944d432206e706d8ba6a748ea9fd243f2b1d4044456b68111c3adb0dc41e1cd9a829383145d2590b0ea1a82b28
7
- data.tar.gz: 86d94f972980c806721f39a914a8d527364809b532a940c826e050e59ee5b4661b57824e74da950537ae6d6ff2c0046b048f64a9a5a6ad197e4c94f937cc3ba8
6
+ metadata.gz: 704ee74432f653b9993312d965ba1605ab366f67d564061a82522d8ac647b21689f47abdc836483dd82ffa4e3810882e33cccbd47c37e22cf8013d82147c2a20
7
+ data.tar.gz: 12535b39f7ff460bba0fb26bb3156f819e8d96ccf2ed7e2da28b6ab63472af3f42341a2c92e8fe437d5dccb56ce77ee7969e036c7484c833655ad5108e7fd3d9
data/.gitignore CHANGED
@@ -9,3 +9,6 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+
13
+ # .gem version files
14
+ *.gem
@@ -1 +1 @@
1
- ruby 2.5.1
1
+ ruby 2.6.5
@@ -3,5 +3,16 @@ sudo: false
3
3
  language: ruby
4
4
  cache: bundler
5
5
  rvm:
6
- - 2.4.2
7
- before_install: gem install bundler -v 2.0.1
6
+ - 2.5.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.7.1)
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(array(integer)) do |array|
94
+ result = naive_average(array)
95
+ unless result.is_a?(Integer) do
96
+ raise "Expected the average to be an integer!"
97
+ end
98
+ end
99
+ ```
100
+
101
+ When running this particular example PropCheck very quickly finds out that we have made a programming mistake:
102
+
103
+ ```ruby
104
+ ZeroDivisionError:
105
+ (after 6 successful property test runs)
106
+ Failed on:
107
+ `{
108
+ :array => []
109
+ }`
110
+
111
+ Exception message:
112
+ ---
113
+ divided by 0
114
+ ---
115
+
116
+ (shrinking impossible)
117
+ ---
118
+ ```
119
+
120
+ Clearly we forgot to handle the case of an empty array being passed to the function.
121
+ This is a good example of the kind of conceptual bugs that PropCheck (and property-based testing in general)
122
+ are able to check for.
123
+
124
+
75
125
  #### Shrinking
76
126
 
77
127
  When a failure is found, PropCheck will re-run the block given to `forall` to test
@@ -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,26 @@ 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.
6
12
  module PropCheck
7
- class Error < StandardError; end
8
- class UserError < Error; end
9
- class GeneratorExhaustedError < UserError; end
10
- class MaxShrinkStepsExceededError < UserError; end
13
+ module Errors
14
+ class Error < StandardError; end
15
+ class UserError < Error; end
16
+ class GeneratorExhaustedError < UserError; end
17
+ class MaxShrinkStepsExceededError < UserError; end
18
+ end
11
19
 
12
20
  extend self
13
21
 
22
+ ##
23
+ # Runs a property.
24
+ #
25
+ # See the README for more details.
14
26
  def forall(*args, **kwargs, &block)
15
27
  PropCheck::Property.forall(*args, **kwargs, &block)
16
28
  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,13 +1,19 @@
1
1
  module PropCheck
2
2
  ##
3
3
  # Integration with RSpec
4
+ #
5
+ # Currently very basic; it does two things:
6
+ # 1. adds the local `forall` method to examples that calls `PropCheck.forall`
7
+ # 2. adds `include PropCheck::Generators` statement.
4
8
  module RSpec
5
9
  # To make it available within examples
6
10
  def self.extend_object(obj)
11
+ obj.instance_eval do
12
+ include PropCheck::Generators
13
+ end
14
+
7
15
  obj.define_method(:forall) do |*args, **kwargs, &block|
8
- PropCheck::Property.forall(*args, **kwargs) do
9
- instance_exec(self, &block)
10
- end
16
+ PropCheck.forall(*args, **kwargs, &block)
11
17
  end
12
18
  end
13
19
  end
@@ -1,3 +1,3 @@
1
1
  module PropCheck
2
- VERSION = "0.6.0"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Qqwy/Wiebe-Marten Wijnja"]
10
10
  spec.email = ["w-m@wmcode.nl"]
11
11
 
12
- spec.summary = %q{PropCheck allows you to do property-based testing , including shrinking. (akin to Haskell's QuickCheck, Erlang's PropEr, Elixir's StreamData)}
13
- spec.description = %q{PropCheck allows you to do property-based testing , including shrinking. (akin to Haskell's QuickCheck, Erlang's PropEr, Elixir's StreamData). This means that your test are run many times with different, autogenerated inputs, and as soon as a failing case is found, this input is simplified, in the end giving you back the simplest input that made the test fail.}
12
+ spec.summary = %q{PropCheck allows you to do property-based testing, including shrinking.}
13
+ spec.description = %q{PropCheck allows you to do property-based testing, including shrinking. (akin to Haskell's QuickCheck, Erlang's PropEr, Elixir's StreamData). This means that your test are run many times with different, autogenerated inputs, and as soon as a failing case is found, this input is simplified, in the end giving you back the simplest input that made the test fail.}
14
14
  spec.homepage = "https://github.com/Qqwy/ruby-prop_check/"
15
15
  spec.license = "MIT"
16
16
 
@@ -34,7 +34,9 @@ Gem::Specification.new do |spec|
34
34
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
35
  spec.require_paths = ["lib"]
36
36
 
37
+ spec.required_ruby_version = '>= 2.5.1'
38
+
37
39
  spec.add_development_dependency "bundler", "~> 2.0"
38
- spec.add_development_dependency "rake", "~> 10.0"
40
+ spec.add_development_dependency "rake", "~> 12.3"
39
41
  spec.add_development_dependency "rspec", "~> 3.0"
40
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.0
4
+ version: 0.8.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-20 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
@@ -52,7 +52,7 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
- description: PropCheck allows you to do property-based testing , including shrinking.
55
+ description: PropCheck allows you to do property-based testing, including shrinking.
56
56
  (akin to Haskell's QuickCheck, Erlang's PropEr, Elixir's StreamData). This means
57
57
  that your test are run many times with different, autogenerated inputs, and as soon
58
58
  as a failing case is found, this input is simplified, in the end giving you back
@@ -81,9 +81,9 @@ 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
88
  - lib/prop_check/rspec.rb
89
89
  - lib/prop_check/version.rb
@@ -95,7 +95,7 @@ metadata:
95
95
  homepage_uri: https://github.com/Qqwy/ruby-prop_check/
96
96
  source_code_uri: https://github.com/Qqwy/ruby-prop_check/
97
97
  changelog_uri: https://github.com/Qqwy/ruby-prop_check/CHANGELOG.md
98
- post_install_message:
98
+ post_install_message:
99
99
  rdoc_options: []
100
100
  require_paths:
101
101
  - lib
@@ -103,17 +103,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
103
103
  requirements:
104
104
  - - ">="
105
105
  - !ruby/object:Gem::Version
106
- version: '0'
106
+ version: 2.5.1
107
107
  required_rubygems_version: !ruby/object:Gem::Requirement
108
108
  requirements:
109
109
  - - ">="
110
110
  - !ruby/object:Gem::Version
111
111
  version: '0'
112
112
  requirements: []
113
- rubyforge_project:
114
- rubygems_version: 2.7.6
115
- signing_key:
113
+ rubygems_version: 3.0.3
114
+ signing_key:
116
115
  specification_version: 4
117
- summary: PropCheck allows you to do property-based testing , including shrinking.
118
- (akin to Haskell's QuickCheck, Erlang's PropEr, Elixir's StreamData)
116
+ summary: PropCheck allows you to do property-based testing, including shrinking.
119
117
  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