prop_check 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +4 -0
- data/.tool-versions +1 -0
- data/.travis.yml +18 -0
- data/CHANGELOG.md +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +50 -0
- data/LICENSE.txt +21 -0
- data/README.md +242 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/prop_check.rb +36 -0
- data/lib/prop_check/generator.rb +114 -0
- data/lib/prop_check/generators.rb +487 -0
- data/lib/prop_check/helper.rb +27 -0
- data/lib/prop_check/helper/lazy_append.rb +18 -0
- data/lib/prop_check/lazy_tree.rb +135 -0
- data/lib/prop_check/property.rb +285 -0
- data/lib/prop_check/property/configuration.rb +14 -0
- data/lib/prop_check/version.rb +3 -0
- data/prop_check.gemspec +42 -0
- metadata +116 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "prop_check"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/prop_check.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require "prop_check/version"
|
2
|
+
require 'prop_check/property'
|
3
|
+
require 'prop_check/generator'
|
4
|
+
require 'prop_check/generators'
|
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
|
+
#
|
19
|
+
module PropCheck
|
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
|
26
|
+
|
27
|
+
extend self
|
28
|
+
|
29
|
+
##
|
30
|
+
# Runs a property.
|
31
|
+
#
|
32
|
+
# See the README for more details.
|
33
|
+
def forall(*args, **kwargs, &block)
|
34
|
+
PropCheck::Property.forall(*args, **kwargs, &block)
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module PropCheck
|
2
|
+
##
|
3
|
+
# A `Generator` is a special kind of 'proc' that,
|
4
|
+
# given a size an random number generator state,
|
5
|
+
# will generate a (finite) LazyTree of output values:
|
6
|
+
#
|
7
|
+
# The root of this tree is the value to be used during testing,
|
8
|
+
# and the children are 'smaller' values related to the root,
|
9
|
+
# to be used during the shrinking phase.
|
10
|
+
class Generator
|
11
|
+
@@default_size = 10
|
12
|
+
@@default_rng = Random.new
|
13
|
+
|
14
|
+
##
|
15
|
+
# Being a special kind of Proc, a Generator wraps a block.
|
16
|
+
def initialize(&block)
|
17
|
+
@block = block
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Given a `size` (integer) and a random number generator state `rng`,
|
22
|
+
# generate a LazyTree.
|
23
|
+
def generate(size = @@default_size, rng = @@default_rng)
|
24
|
+
@block.call(size, rng)
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Generates a value, and only return this value
|
29
|
+
# (drop information for shrinking)
|
30
|
+
#
|
31
|
+
# >> Generators.integer.call(1000, Random.new(42))
|
32
|
+
# => 126
|
33
|
+
def call(size = @@default_size, rng = @@default_rng)
|
34
|
+
generate(size, rng).root
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Returns `num_of_samples` values from calling this Generator.
|
39
|
+
# This is mostly useful for debugging if a generator behaves as you intend it to.
|
40
|
+
def sample(num_of_samples = 10, size: @@default_size, rng: @@default_rng)
|
41
|
+
num_of_samples.times.map do
|
42
|
+
call(size, rng)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Creates a 'constant' generator that always returns the same value,
|
48
|
+
# regardless of `size` or `rng`.
|
49
|
+
#
|
50
|
+
# Keen readers may notice this as the Monadic 'pure'/'return' implementation for Generators.
|
51
|
+
#
|
52
|
+
# >> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(100, Random.new(42))
|
53
|
+
# => [2, 79]
|
54
|
+
def self.wrap(val)
|
55
|
+
Generator.new { |_size, _rng| LazyTree.wrap(val) }
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Create a generator whose implementation depends on the output of another generator.
|
60
|
+
# this allows us to compose multiple generators.
|
61
|
+
#
|
62
|
+
# Keen readers may notice this as the Monadic 'bind' (sometimes known as '>>=') implementation for Generators.
|
63
|
+
#
|
64
|
+
# >> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(100, Random.new(42))
|
65
|
+
# => [2, 79]
|
66
|
+
def bind(&generator_proc)
|
67
|
+
# Generator.new do |size, rng|
|
68
|
+
# outer_result = generate(size, rng)
|
69
|
+
# outer_result.map do |outer_val|
|
70
|
+
# inner_generator = generator_proc.call(outer_val)
|
71
|
+
# inner_generator.generate(size, rng)
|
72
|
+
# end.flatten
|
73
|
+
# end
|
74
|
+
Generator.new do |size, rng|
|
75
|
+
outer_result = self.generate(size, rng)
|
76
|
+
outer_result.bind do |outer_val|
|
77
|
+
inner_generator = generator_proc.call(outer_val)
|
78
|
+
inner_generator.generate(size, rng)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Creates a new Generator that returns a value by running `proc` on the output of the current Generator.
|
85
|
+
#
|
86
|
+
# >> Generators.choose(32..128).map(&:chr).call(10, Random.new(42))
|
87
|
+
# => "S"
|
88
|
+
def map(&proc)
|
89
|
+
Generator.new do |size, rng|
|
90
|
+
result = self.generate(size, rng)
|
91
|
+
result.map(&proc)
|
92
|
+
end
|
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
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,487 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'prop_check/generator'
|
5
|
+
require 'prop_check/lazy_tree'
|
6
|
+
module PropCheck
|
7
|
+
##
|
8
|
+
# Contains common generators.
|
9
|
+
# Use this module by including it in the class (e.g. in your test suite)
|
10
|
+
# where you want to use them.
|
11
|
+
module Generators
|
12
|
+
extend self
|
13
|
+
##
|
14
|
+
# Always returns the same value, regardless of `size` or `rng` (random number generator state)
|
15
|
+
#
|
16
|
+
# No shrinking (only considers the current single value `val`).
|
17
|
+
#
|
18
|
+
# >> Generators.constant("pie").sample(5, size: 10, rng: Random.new(42))
|
19
|
+
# => ["pie", "pie", "pie", "pie", "pie"]
|
20
|
+
def constant(val)
|
21
|
+
Generator.wrap(val)
|
22
|
+
end
|
23
|
+
|
24
|
+
private def integer_shrink(val)
|
25
|
+
# 0 cannot shrink further; base case
|
26
|
+
return [] if val.zero?
|
27
|
+
|
28
|
+
# Numbers are shrunken by
|
29
|
+
# subtracting themselves, their half, quarter, eight, ... (rounded towards zero!)
|
30
|
+
# from themselves, until the number itself is reached.
|
31
|
+
# So: for 20 we have [0, 10, 15, 18, 19, 20]
|
32
|
+
halvings =
|
33
|
+
Helper
|
34
|
+
.scanl(val) { |x| (x / 2.0).truncate }
|
35
|
+
.take_while { |x| !x.zero? }
|
36
|
+
.map { |x| val - x }
|
37
|
+
.map { |x| LazyTree.new(x, integer_shrink(x)) }
|
38
|
+
|
39
|
+
# For negative numbers, we also attempt if the positive number has the same result.
|
40
|
+
if val.abs > val
|
41
|
+
[LazyTree.new(val.abs, halvings)].lazy
|
42
|
+
else
|
43
|
+
halvings
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Returns a random integer in the given range (if a range is given)
|
49
|
+
# or between 0..num (if a single integer is given).
|
50
|
+
#
|
51
|
+
# Does not scale when `size` changes.
|
52
|
+
# This means `choose` is useful for e.g. picking an element out of multiple possibilities,
|
53
|
+
# but for other purposes you probably want to use `integer` et co.
|
54
|
+
#
|
55
|
+
# Shrinks to integers closer to zero.
|
56
|
+
#
|
57
|
+
# >> r = Random.new(42); Generators.choose(0..5).sample(size: 10, rng: r)
|
58
|
+
# => [3, 4, 2, 4, 4, 1, 2, 2, 2, 4]
|
59
|
+
# >> r = Random.new(42); Generators.choose(0..5).sample(size: 20000, rng: r)
|
60
|
+
# => [3, 4, 2, 4, 4, 1, 2, 2, 2, 4]
|
61
|
+
def choose(range)
|
62
|
+
Generator.new do |_size, rng|
|
63
|
+
val = rng.rand(range)
|
64
|
+
LazyTree.new(val, integer_shrink(val))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# A random integer which scales with `size`.
|
70
|
+
# Integers start small (around 0)
|
71
|
+
# and become more extreme (both higher and lower, negative) when `size` increases.
|
72
|
+
#
|
73
|
+
#
|
74
|
+
# Shrinks to integers closer to zero.
|
75
|
+
#
|
76
|
+
# >> Generators.integer.call(2, Random.new(42))
|
77
|
+
# => 1
|
78
|
+
# >> Generators.integer.call(10000, Random.new(42))
|
79
|
+
# => 5795
|
80
|
+
# >> r = Random.new(42); Generators.integer.sample(size: 20000, rng: r)
|
81
|
+
# => [-4205, -19140, 18158, -8716, -13735, -3150, 17194, 1962, -3977, -18315]
|
82
|
+
def integer
|
83
|
+
Generator.new do |size, rng|
|
84
|
+
val = rng.rand(-size..size)
|
85
|
+
LazyTree.new(val, integer_shrink(val))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Only returns integers that are zero or larger.
|
91
|
+
# See `integer` for more information.
|
92
|
+
def nonnegative_integer
|
93
|
+
integer.map(&:abs)
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Only returns integers that are larger than zero.
|
98
|
+
# See `integer` for more information.
|
99
|
+
def positive_integer
|
100
|
+
nonnegative_integer.map { |x| x + 1 }
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Only returns integers that are zero or smaller.
|
105
|
+
# See `integer` for more information.
|
106
|
+
def nonpositive_integer
|
107
|
+
nonnegative_integer.map(&:-@)
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Only returns integers that are smaller than zero.
|
112
|
+
# See `integer` for more information.
|
113
|
+
def negative_integer
|
114
|
+
positive_integer.map(&:-@)
|
115
|
+
end
|
116
|
+
|
117
|
+
private def fraction(num_a, num_b, num_c)
|
118
|
+
num_a.to_f + num_b.to_f / (num_c.to_f.abs + 1.0)
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Generates floating-point numbers
|
123
|
+
# These start small (around 0)
|
124
|
+
# and become more extreme (large positive and large negative numbers)
|
125
|
+
#
|
126
|
+
# Will only generate 'reals',
|
127
|
+
# that is: no infinity, no NaN,
|
128
|
+
# no numbers testing the limits of floating-point arithmetic.
|
129
|
+
#
|
130
|
+
# Shrinks to numbers closer to zero.
|
131
|
+
#
|
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
|
+
tuple(integer, integer, integer).map do |a, b, c|
|
136
|
+
fraction(a, b, c)
|
137
|
+
end
|
138
|
+
end
|
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
|
+
|
156
|
+
##
|
157
|
+
# Picks one of the given generators in `choices` at random uniformly every time.
|
158
|
+
#
|
159
|
+
# Shrinks to values earlier in the list of `choices`.
|
160
|
+
#
|
161
|
+
# >> Generators.one_of(Generators.constant(true), Generators.constant(false)).sample(5, size: 10, rng: Random.new(42))
|
162
|
+
# => [true, false, true, true, true]
|
163
|
+
def one_of(*choices)
|
164
|
+
choose(choices.length).bind do |index|
|
165
|
+
choices[index]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Picks one of the choices given in `frequencies` at random every time.
|
171
|
+
# `frequencies` expects keys to be numbers
|
172
|
+
# (representing the relative frequency of this generator)
|
173
|
+
# and values to be generators.
|
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
|
+
#
|
178
|
+
# Shrinks to arbitrary elements (since hashes are not ordered).
|
179
|
+
#
|
180
|
+
# >> Generators.frequency(5 => Generators.integer, 1 => Generators.printable_ascii_char).sample(size: 10, rng: Random.new(42))
|
181
|
+
# => [4, -3, 10, 8, 0, -7, 10, 1, "E", 10]
|
182
|
+
def frequency(frequencies)
|
183
|
+
choices = frequencies.reduce([]) do |acc, elem|
|
184
|
+
freq, val = elem
|
185
|
+
acc + ([val] * freq)
|
186
|
+
end
|
187
|
+
one_of(*choices)
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
# Generates an array containing always exactly one value from each of the passed generators,
|
192
|
+
# in the same order as specified:
|
193
|
+
#
|
194
|
+
# Shrinks element generators, one at a time (trying last one first).
|
195
|
+
#
|
196
|
+
# >> Generators.tuple(Generators.integer, Generators.real_float).call(10, Random.new(42))
|
197
|
+
# => [-4, 13.0]
|
198
|
+
def tuple(*generators)
|
199
|
+
Generator.new do |size, rng|
|
200
|
+
LazyTree.zip(generators.map do |generator|
|
201
|
+
generator.generate(size, rng)
|
202
|
+
end)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# Given a `hash` where the values are generators,
|
208
|
+
# creates a generator that returns hashes
|
209
|
+
# with the same keys, and their corresponding values from their corresponding generators.
|
210
|
+
#
|
211
|
+
# Shrinks element generators.
|
212
|
+
#
|
213
|
+
# >> Generators.fixed_hash(a: Generators.integer(), b: Generators.real_float(), c: Generators.integer()).call(10, Random.new(42))
|
214
|
+
# => {:a=>-4, :b=>13.0, :c=>-3}
|
215
|
+
def fixed_hash(hash)
|
216
|
+
keypair_generators =
|
217
|
+
hash.map do |key, generator|
|
218
|
+
generator.map { |val| [key, val] }
|
219
|
+
end
|
220
|
+
|
221
|
+
tuple(*keypair_generators)
|
222
|
+
.map(&:to_h)
|
223
|
+
end
|
224
|
+
|
225
|
+
##
|
226
|
+
# Generates an array of elements, where each of the elements
|
227
|
+
# is generated by `element_generator`.
|
228
|
+
#
|
229
|
+
# Shrinks to shorter arrays (with shrunken elements).
|
230
|
+
#
|
231
|
+
# >> Generators.array(Generators.positive_integer).sample(5, size: 10, rng: Random.new(42))
|
232
|
+
# => [[10, 5, 1, 4], [5, 9, 1, 1, 11, 8, 4, 9, 11, 10], [6], [11, 11, 2, 2, 7, 2, 6, 5, 5], [2, 10, 9, 7, 9, 5, 11, 3]]
|
233
|
+
def array(element_generator)
|
234
|
+
nonnegative_integer.bind do |generator|
|
235
|
+
generators = (0...generator).map do
|
236
|
+
element_generator.clone
|
237
|
+
end
|
238
|
+
|
239
|
+
tuple(*generators)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
##
|
244
|
+
# Generates a hash of key->values,
|
245
|
+
# where each of the keys is made using the `key_generator`
|
246
|
+
# and each of the values using the `value_generator`.
|
247
|
+
#
|
248
|
+
# Shrinks to hashes with less key/value pairs.
|
249
|
+
#
|
250
|
+
# >> Generators.hash(Generators.printable_ascii_string, Generators.positive_integer).sample(5, size: 3, rng: Random.new(42))
|
251
|
+
# => [{""=>2, "g\\4"=>4, "rv"=>2}, {"7"=>2}, {"!"=>1, "E!"=>1}, {"kY5"=>2}, {}]
|
252
|
+
def hash(key_generator, value_generator)
|
253
|
+
array(tuple(key_generator, value_generator))
|
254
|
+
.map(&:to_h)
|
255
|
+
end
|
256
|
+
|
257
|
+
|
258
|
+
@alphanumeric_chars = [('a'..'z'), ('A'..'Z'), ('0'..'9')].flat_map(&:to_a).freeze
|
259
|
+
##
|
260
|
+
# Generates a single-character string
|
261
|
+
# containing one of a..z, A..Z, 0..9
|
262
|
+
#
|
263
|
+
# Shrinks towards lowercase 'a'.
|
264
|
+
#
|
265
|
+
# >> Generators.alphanumeric_char.sample(5, size: 10, rng: Random.new(42))
|
266
|
+
# => ["M", "Z", "C", "o", "Q"]
|
267
|
+
def alphanumeric_char
|
268
|
+
one_of(*@alphanumeric_chars.map(&method(:constant)))
|
269
|
+
end
|
270
|
+
|
271
|
+
##
|
272
|
+
# Generates a string
|
273
|
+
# containing only the characters a..z, A..Z, 0..9
|
274
|
+
#
|
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"]
|
279
|
+
def alphanumeric_string
|
280
|
+
array(alphanumeric_char).map(&:join)
|
281
|
+
end
|
282
|
+
|
283
|
+
@printable_ascii_chars = (' '..'~').to_a.freeze
|
284
|
+
|
285
|
+
##
|
286
|
+
# Generates a single-character string
|
287
|
+
# from the printable ASCII character set.
|
288
|
+
#
|
289
|
+
# Shrinks towards ' '.
|
290
|
+
#
|
291
|
+
# >> Generators.printable_ascii_char.sample(size: 10, rng: Random.new(42))
|
292
|
+
# => ["S", "|", ".", "g", "\\", "4", "r", "v", "j", "j"]
|
293
|
+
def printable_ascii_char
|
294
|
+
one_of(*@printable_ascii_chars.map(&method(:constant)))
|
295
|
+
end
|
296
|
+
|
297
|
+
##
|
298
|
+
# Generates strings
|
299
|
+
# from the printable ASCII character set.
|
300
|
+
#
|
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"]
|
305
|
+
def printable_ascii_string
|
306
|
+
array(printable_ascii_char).map(&:join)
|
307
|
+
end
|
308
|
+
|
309
|
+
@ascii_chars = [
|
310
|
+
@printable_ascii_chars,
|
311
|
+
[
|
312
|
+
"\n",
|
313
|
+
"\r",
|
314
|
+
"\t",
|
315
|
+
"\v",
|
316
|
+
"\b",
|
317
|
+
"\f",
|
318
|
+
"\e",
|
319
|
+
"\d",
|
320
|
+
"\a"
|
321
|
+
]
|
322
|
+
].flat_map(&:to_a).freeze
|
323
|
+
|
324
|
+
##
|
325
|
+
# Generates a single-character string
|
326
|
+
# from the printable ASCII character set.
|
327
|
+
#
|
328
|
+
# Shrinks towards '\n'.
|
329
|
+
#
|
330
|
+
# >> Generators.ascii_char.sample(size: 10, rng: Random.new(42))
|
331
|
+
# => ["d", "S", "|", ".", "g", "\\", "4", "d", "r", "v"]
|
332
|
+
def ascii_char
|
333
|
+
one_of(*@ascii_chars.map(&method(:constant)))
|
334
|
+
end
|
335
|
+
|
336
|
+
##
|
337
|
+
# Generates strings
|
338
|
+
# from the printable ASCII character set.
|
339
|
+
#
|
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"]
|
344
|
+
def ascii_string
|
345
|
+
array(ascii_char).map(&:join)
|
346
|
+
end
|
347
|
+
|
348
|
+
@printable_chars = [
|
349
|
+
@ascii_chars,
|
350
|
+
"\u{A0}".."\u{D7FF}",
|
351
|
+
"\u{E000}".."\u{FFFD}",
|
352
|
+
"\u{10000}".."\u{10FFFF}"
|
353
|
+
].flat_map(&:to_a).freeze
|
354
|
+
|
355
|
+
##
|
356
|
+
# Generates a single-character printable string
|
357
|
+
# both ASCII characters and Unicode.
|
358
|
+
#
|
359
|
+
# Shrinks towards characters with lower codepoints, e.g. ASCII
|
360
|
+
#
|
361
|
+
# >> Generators.printable_char.sample(size: 10, rng: Random.new(42))
|
362
|
+
# => ["吏", "", "", "", "", "", "", "", "", "Ȍ"]
|
363
|
+
def printable_char
|
364
|
+
one_of(*@printable_chars.map(&method(:constant)))
|
365
|
+
end
|
366
|
+
|
367
|
+
##
|
368
|
+
# Generates a printable string
|
369
|
+
# both ASCII characters and Unicode.
|
370
|
+
#
|
371
|
+
# Shrinks towards shorter strings, and towards characters with lower codepoints, e.g. ASCII
|
372
|
+
#
|
373
|
+
# >> Generators.printable_string.sample(5, size: 10, rng: Random.new(42))
|
374
|
+
# => ["", "Ȍ", "𐁂", "Ȕ", ""]
|
375
|
+
def printable_string
|
376
|
+
array(printable_char).map(&:join)
|
377
|
+
end
|
378
|
+
|
379
|
+
##
|
380
|
+
# Generates a single unicode character
|
381
|
+
# (both printable and non-printable).
|
382
|
+
#
|
383
|
+
# Shrinks towards characters with lower codepoints, e.g. ASCII
|
384
|
+
#
|
385
|
+
# >> Generators.printable_char.sample(size: 10, rng: Random.new(42))
|
386
|
+
# => ["吏", "", "", "", "", "", "", "", "", "Ȍ"]
|
387
|
+
def char
|
388
|
+
choose(0..0x10FFFF).map do |num|
|
389
|
+
[num].pack('U')
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
##
|
394
|
+
# Generates a string of unicode characters
|
395
|
+
# (which might contain both printable and non-printable characters).
|
396
|
+
#
|
397
|
+
# Shrinks towards characters with lower codepoints, e.g. ASCII
|
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}"]
|
401
|
+
def string
|
402
|
+
array(char).map(&:join)
|
403
|
+
end
|
404
|
+
|
405
|
+
##
|
406
|
+
# Generates either `true` or `false`
|
407
|
+
#
|
408
|
+
# Shrinks towards `false`
|
409
|
+
#
|
410
|
+
# >> Generators.boolean.sample(5, size: 10, rng: Random.new(42))
|
411
|
+
# => [false, true, false, false, false]
|
412
|
+
def boolean
|
413
|
+
one_of(constant(false), constant(true))
|
414
|
+
end
|
415
|
+
|
416
|
+
##
|
417
|
+
# Generates always `nil`.
|
418
|
+
#
|
419
|
+
# Does not shrink.
|
420
|
+
#
|
421
|
+
# >> Generators.nil.sample(5, size: 10, rng: Random.new(42))
|
422
|
+
# => [nil, nil, nil, nil, nil]
|
423
|
+
def nil
|
424
|
+
constant(nil)
|
425
|
+
end
|
426
|
+
|
427
|
+
##
|
428
|
+
# Generates `nil` or `false`.
|
429
|
+
#
|
430
|
+
# Shrinks towards `nil`.
|
431
|
+
#
|
432
|
+
# >> Generators.falsey.sample(5, size: 10, rng: Random.new(42))
|
433
|
+
# => [nil, false, nil, nil, nil]
|
434
|
+
def falsey
|
435
|
+
one_of(constant(nil), constant(false))
|
436
|
+
end
|
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
|
+
|
453
|
+
##
|
454
|
+
# Generates common terms that are not `nil` or `false`.
|
455
|
+
#
|
456
|
+
# Shrinks towards simpler terms, like `true`, an empty array, a single character or an integer.
|
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}"]]
|
460
|
+
def truthy
|
461
|
+
one_of(constant(true),
|
462
|
+
constant([]),
|
463
|
+
char,
|
464
|
+
integer,
|
465
|
+
float,
|
466
|
+
string,
|
467
|
+
array(integer),
|
468
|
+
array(float),
|
469
|
+
array(char),
|
470
|
+
array(string),
|
471
|
+
hash(simple_symbol, integer),
|
472
|
+
hash(string, integer),
|
473
|
+
hash(string, string)
|
474
|
+
)
|
475
|
+
end
|
476
|
+
|
477
|
+
##
|
478
|
+
# Generates whatever `other_generator` generates
|
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]
|
483
|
+
def nillable(other_generator)
|
484
|
+
frequency(9 => other_generator, 1 => constant(nil))
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|