forall 1.0.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/lib/forall.rb +200 -0
- data/lib/forall/counter.rb +39 -0
- data/lib/forall/input.rb +107 -0
- data/lib/forall/matchers.rb +107 -0
- data/lib/forall/random.rb +284 -0
- data/lib/forall/shrink.rb +49 -0
- metadata +48 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: eaeea253a28a5ebeda2e96db04da2c8a8d611062dd553ec2ed1a3dba85bdfae1
|
|
4
|
+
data.tar.gz: 25bb8fbec548c8a6e0c5d907a48170554129e4d46554ed5115a9c9cedd3ce377
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9031e48f3c8c7f03e36dd331b73223574b3496ac91380d4ef368f311c625f3603c543965cfc6d67f0164d745bdf6b8c659fe8bb30e006443656b8a382cf7ce66
|
|
7
|
+
data.tar.gz: 94c69967b093cb6b0bcc56d998cca7e926c283a1d7c0fa56fd4f6270db874dfbd2ea463b670fa53aef583513b65c8db42cbf27a4a89715f51bca467c2609f6f6
|
data/lib/forall.rb
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Forall
|
|
4
|
+
autoload :Input, "forall/input"
|
|
5
|
+
autoload :Random, "forall/random"
|
|
6
|
+
autoload :Counter, "forall/counter"
|
|
7
|
+
autoload :Matchers, "forall/matchers"
|
|
8
|
+
|
|
9
|
+
# The property was true of all tested inputs
|
|
10
|
+
Ok = Struct.new(:seed, :counter)
|
|
11
|
+
|
|
12
|
+
# The property was not true of at least one tested input
|
|
13
|
+
No = Struct.new(:seed, :counter, :counterexample)
|
|
14
|
+
|
|
15
|
+
# Couldn't find enough suitable inputs to test
|
|
16
|
+
Vacuous = Struct.new(:seed, :counter)
|
|
17
|
+
|
|
18
|
+
# An error occurred while checking the property
|
|
19
|
+
Fail = Struct.new(:seed, :counter, :counterexample, :error)
|
|
20
|
+
|
|
21
|
+
# TODO: Is there a way to provide a default implementation of `shrink`? Will
|
|
22
|
+
# it interefere with a user-given implementation?
|
|
23
|
+
|
|
24
|
+
Options = Struct.new(
|
|
25
|
+
:max_ok, # Stop looking for counterexamples after this many inputs pass
|
|
26
|
+
:max_skip, # Give up if more than this many inputs are skipped
|
|
27
|
+
:max_shrink) # Number of similar inputs to evaluate when searching for simpler counterexamples
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def check(input, random, options = nil, &prop)
|
|
31
|
+
options ||= Options.new
|
|
32
|
+
options.max_shrink ||= 100
|
|
33
|
+
|
|
34
|
+
if input.exhaustive?
|
|
35
|
+
options.max_ok ||= input.size * 0.90
|
|
36
|
+
options.max_skip ||= input.size * 0.10
|
|
37
|
+
else
|
|
38
|
+
options.max_ok ||= 100
|
|
39
|
+
options.max_skip ||= options.max_ok * 0.10
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if prop.arity == 1
|
|
43
|
+
_prop = prop
|
|
44
|
+
prop = lambda{|x,_| _prop.call(x) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
raise ArgumentError, "property must take one or two arguments" \
|
|
48
|
+
unless prop.arity == 2
|
|
49
|
+
|
|
50
|
+
counter = Counter.new
|
|
51
|
+
|
|
52
|
+
input.each(random) do |example|
|
|
53
|
+
return Ok.new(random.seed, counter) if counter.ok >= options.max_ok
|
|
54
|
+
return Vacuous.new(random.seed, counter) if counter.skip >= options.max_skip
|
|
55
|
+
|
|
56
|
+
catch(:skip) do
|
|
57
|
+
if prop.call(example, counter)
|
|
58
|
+
counter.ok += 1
|
|
59
|
+
else
|
|
60
|
+
return no(random, counter, input.shrink, example, options, prop)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
rescue Exception => error
|
|
64
|
+
counter.fail += 1
|
|
65
|
+
return fail(random, counter, input.shrink, example, options, prop, error)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Didn't meet ok_max because input was exhausted
|
|
69
|
+
Ok.new(random.seed, counter)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def weight(xs, min, max)
|
|
75
|
+
n = xs.length.to_f
|
|
76
|
+
r = max - min
|
|
77
|
+
xs.map.with_index{|x, k| [x, max-r*k/n] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def _weight(xs, min, max)
|
|
81
|
+
n = xs.length.to_f
|
|
82
|
+
r = max - min
|
|
83
|
+
xs.map.with_index{|x, k| C.new(x, max-r*k/n, 0) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
C = Struct.new(:value, :fitness, :heuristic, :note)
|
|
87
|
+
|
|
88
|
+
# Search for the simplest counterexample
|
|
89
|
+
def no(random, counter, shrink, shrunk, options, prop)
|
|
90
|
+
return No.new(random.seed, counter, shrunk) if shrink.nil?
|
|
91
|
+
|
|
92
|
+
# The problem of finding the smallest counterexample can be described in
|
|
93
|
+
# terms of local search. The search space has a graph structure (think of
|
|
94
|
+
# a tree with duplicate nodes) and candidate solutions are examples drawn
|
|
95
|
+
# from the domain over which the property holds. The criterion to be
|
|
96
|
+
# maximized is the simplicity of a counterexample -- simple is not meant
|
|
97
|
+
# in any particular formal sense. The neighborhood relation is described
|
|
98
|
+
# by the user-provided function `shrink`, which should return candidate
|
|
99
|
+
# solutions that are only incrementally simpler than its argument.
|
|
100
|
+
#
|
|
101
|
+
# For ease of use, the user does not need to provide a function to
|
|
102
|
+
# calculate the criterion (simplicity) of a candidate solution. Instead,
|
|
103
|
+
# it is inferred by the number of edges in its path from the root and by
|
|
104
|
+
# the relative order in which it was returned among other candidate
|
|
105
|
+
# solutions (the first being simplest). There is also no requirement for
|
|
106
|
+
# a user-supplied heuristic to rank partial solutions, as it would often
|
|
107
|
+
# be difficult to estimate an optimal solution let alone calculate some
|
|
108
|
+
# quantifiable difference between it and any other partial solution.
|
|
109
|
+
#
|
|
110
|
+
# The solution space is finite (eventually `shrink` must return an empty
|
|
111
|
+
# list), but because we have a self-imposed computational budget, its not
|
|
112
|
+
# feasible to exhaustively search for the deepest leaf in the tree. We can
|
|
113
|
+
# conjure a heuristic based on a candidate solution's ratio of ancestors
|
|
114
|
+
# that are examples or counterexamples. If one candidate solution was
|
|
115
|
+
# generated among many which did not disprove the property, and another
|
|
116
|
+
# candidate solution was generated among many counterexamples, we will
|
|
117
|
+
# assume the first candidate is less likely to produce more
|
|
118
|
+
# counterexamples than the second.
|
|
119
|
+
#
|
|
120
|
+
# shrunk
|
|
121
|
+
# |
|
|
122
|
+
# [✓] [✕] [✓]
|
|
123
|
+
# 1.0 0.8 0.6
|
|
124
|
+
# / \
|
|
125
|
+
# [✕] [✓] [✕] [✓] [✓]
|
|
126
|
+
# 2.0 1.8 1.6 1.6 1.4
|
|
127
|
+
# / / \ / \
|
|
128
|
+
#
|
|
129
|
+
# The computational budget is in terms of how many candidate solutions we
|
|
130
|
+
# will test to determine if they are counterexamples. As a result, the
|
|
131
|
+
# heuristic value of a candidate solution would seem to require testing
|
|
132
|
+
# each of its ancestors and their siblings. For now that is what we'll do,
|
|
133
|
+
# but in the future there may be a way to make do with partial information
|
|
134
|
+
# and reserve the budget for exploring the tree more deeply.
|
|
135
|
+
|
|
136
|
+
fitness = 0
|
|
137
|
+
queue = _weight(shrink.call(shrunk), 0, 1)
|
|
138
|
+
_counter = counter.shrunk
|
|
139
|
+
|
|
140
|
+
until queue.empty? or _counter.total >= options.max_shrink
|
|
141
|
+
c = queue.shift
|
|
142
|
+
|
|
143
|
+
catch(:skip) do
|
|
144
|
+
if prop.call(c.value, _counter)
|
|
145
|
+
_counter.ok += 1
|
|
146
|
+
else
|
|
147
|
+
if c.fitness > fitness
|
|
148
|
+
fitness = c.fitness
|
|
149
|
+
shrunk = c.value
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
_counter.no += 1
|
|
153
|
+
queue.concat(_weight(shrink.call(c.value), c.fitness+0.5, c.fitness+1.0))
|
|
154
|
+
queue.sort_by!{|x| -x.fitness }
|
|
155
|
+
end
|
|
156
|
+
rescue => e
|
|
157
|
+
_counter.fail += 1
|
|
158
|
+
raise e
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
No.new(random.seed, counter, shrunk)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Search for a simpler example that causes the same exception
|
|
166
|
+
def fail(random, counter, shrink, shrunk, options, prop, error)
|
|
167
|
+
return Fail.new(random.seed, counter, shrunk, error) if shrink.nil?
|
|
168
|
+
|
|
169
|
+
fitness = 0
|
|
170
|
+
queue = _weight(shrink.call(shrunk), 0, 1)
|
|
171
|
+
_counter = counter.shrunk
|
|
172
|
+
|
|
173
|
+
until queue.empty? or _counter.total >= options.max_shrink
|
|
174
|
+
c = queue.shift
|
|
175
|
+
|
|
176
|
+
catch(:skip) do
|
|
177
|
+
if prop.call(c.value, _counter)
|
|
178
|
+
_counter.ok += 1
|
|
179
|
+
else
|
|
180
|
+
_counter.no += 1
|
|
181
|
+
end
|
|
182
|
+
rescue => e
|
|
183
|
+
# TODO: Do we care if this exception is different from `error`?
|
|
184
|
+
|
|
185
|
+
if c.fitness > fitness
|
|
186
|
+
fitness = c.fitness
|
|
187
|
+
shrunk = c.value
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
_counter.fail += 1
|
|
191
|
+
queue.concat(_weight(shrink.call(c.value), c.fitness+0.5, c.fitness+1.0))
|
|
192
|
+
queue.sort_by!{|x| -x.fitness }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
Fail.new(random.seed, counter, shrunk, error)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Forall
|
|
4
|
+
class Counter
|
|
5
|
+
attr_accessor :ok, :no, :skip, :fail, :steps
|
|
6
|
+
|
|
7
|
+
attr_reader :shrunk, :labels
|
|
8
|
+
|
|
9
|
+
def initialize(top = true)
|
|
10
|
+
@ok = 0
|
|
11
|
+
@no = 0
|
|
12
|
+
@skip = 0
|
|
13
|
+
@fail = 0
|
|
14
|
+
@steps = 0
|
|
15
|
+
@shrunk = Counter.new(false) if top
|
|
16
|
+
@labels = Hash.new{|h,k| h[k] = 0 }
|
|
17
|
+
@private = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def total
|
|
21
|
+
@ok + @no + @skip + @fail
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test
|
|
25
|
+
@ok + @no + @fail
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def skip!
|
|
29
|
+
@skip += 1
|
|
30
|
+
throw :skip, true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def label!(*names)
|
|
34
|
+
names.each do |x|
|
|
35
|
+
@labels[x] += 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/forall/input.rb
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Forall
|
|
4
|
+
class Input
|
|
5
|
+
class << self
|
|
6
|
+
# @param value [Proc | Enumerable]
|
|
7
|
+
def build(value = nil, &block)
|
|
8
|
+
if Input === value
|
|
9
|
+
value
|
|
10
|
+
elsif Enumerable === value
|
|
11
|
+
# This includes Range
|
|
12
|
+
All.new(value)
|
|
13
|
+
elsif Proc === value
|
|
14
|
+
Some.new(value)
|
|
15
|
+
elsif block_given?
|
|
16
|
+
Some.new(block)
|
|
17
|
+
else
|
|
18
|
+
raise TypeError, "argument must be a Proc or Enumerable"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param value [Enumerable]
|
|
23
|
+
def exhaustive(value = nil, &block)
|
|
24
|
+
if Enumerable === value
|
|
25
|
+
All.new(value)
|
|
26
|
+
else
|
|
27
|
+
raise TypeError, "argument must be Enumerable"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param value [Proc | Array | Enumerator | ... | Enumerable]
|
|
32
|
+
def sampled(value = nil, &block)
|
|
33
|
+
if block_given?
|
|
34
|
+
Some.new(block)
|
|
35
|
+
elsif Proc === value
|
|
36
|
+
Some.new(value)
|
|
37
|
+
elsif Range === value or value.respond_to?(:sample)
|
|
38
|
+
Some.new(lambda{|rnd| rnd.sample(value) })
|
|
39
|
+
elsif value.respond_to?(:to_a)
|
|
40
|
+
array = value.to_a
|
|
41
|
+
Some.new(lambda{|rnd| rnd.sample(array) })
|
|
42
|
+
else
|
|
43
|
+
raise TypeError, "argument must be a Proc or respond_to?(:sample) or respond_to?(:to_a)"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def shrink(value = nil, &block)
|
|
49
|
+
if block_given?
|
|
50
|
+
@shrink = block
|
|
51
|
+
self
|
|
52
|
+
else
|
|
53
|
+
@shrink
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Exhaustive list of possible input values
|
|
58
|
+
class All < Input
|
|
59
|
+
def initialize(items)
|
|
60
|
+
@items = items
|
|
61
|
+
@shrink = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def exhaustive?
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def sample(random, count: nil)
|
|
69
|
+
random.sample(@items, count: count)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def each(random, *args)
|
|
73
|
+
@items.each{|input| yield input }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def size
|
|
77
|
+
@items.size
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Randomized sample of possible input values
|
|
82
|
+
class Some < Input
|
|
83
|
+
def initialize(block)
|
|
84
|
+
@block = block
|
|
85
|
+
@shrink = nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def exhaustive?
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def sample(random, count: nil)
|
|
93
|
+
if count.nil?
|
|
94
|
+
@block.call(random)
|
|
95
|
+
else
|
|
96
|
+
count.times.map { @block.call(random) }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def each(random, *args)
|
|
101
|
+
while true
|
|
102
|
+
yield @block.call(random, *args)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Forall
|
|
4
|
+
module Matchers
|
|
5
|
+
# @example:
|
|
6
|
+
# describe "foo" do
|
|
7
|
+
# forall([1,2,3]).check {|x,c| c.skip if x.even?; (x*2+1).even? }
|
|
8
|
+
# forall(...).check(seed: 999) {|x,c| c.skip if x.even?; (x*2+1).even? }
|
|
9
|
+
# forall(...).check(success_limit: 50) {|x,c| c.skip if x.even?; (x*2+1).even? }
|
|
10
|
+
# forall(...).check(discard_limit: 0.10){|x,c| c.skip if x.even?; (x*2+1).even? }
|
|
11
|
+
# forall(...).check(shrink_limit: 10) {|x,c| c.skip if x.even?; (x*2+1).even? }
|
|
12
|
+
#
|
|
13
|
+
# forall(lambda{|rnd, x| rnd.integer(x)}).
|
|
14
|
+
# check(0..9){|x,_| x.between?(0,9)}
|
|
15
|
+
# end
|
|
16
|
+
def forall(input)
|
|
17
|
+
ForallMatcher.new(input)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def sampled(input = nil, &block)
|
|
21
|
+
Input.sampled(input || block)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def exhaustive(input)
|
|
25
|
+
Input.exhaustive(input)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class ForallMatcher
|
|
29
|
+
def initialize(input)
|
|
30
|
+
@input = Input.build(input)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def check(property = nil, seed: nil, &block)
|
|
34
|
+
property ||= block
|
|
35
|
+
random = Forall::Random.new(seed: seed)
|
|
36
|
+
result = Forall.check(@input, random, nil, &property)
|
|
37
|
+
|
|
38
|
+
if defined?(RSpec::Expectations)
|
|
39
|
+
case result
|
|
40
|
+
when Forall::Ok
|
|
41
|
+
when Forall::Vacuous
|
|
42
|
+
message = "gave up (after %d test%s and %d discarded)\nSeed: %d" %
|
|
43
|
+
[result.counter.test,
|
|
44
|
+
result.counter.test == 1 ? "" : "s",
|
|
45
|
+
result.counter.skip,
|
|
46
|
+
result.seed]
|
|
47
|
+
|
|
48
|
+
error = ::RSpec::Expectations::ExpectationNotMetError.new(message)
|
|
49
|
+
source = property.binding.source_location.join(":")
|
|
50
|
+
source << " in `block in ...'"
|
|
51
|
+
error.set_backtrace(source)
|
|
52
|
+
raise ::RSpec::Expectations::ExpectationNotMetError, message
|
|
53
|
+
|
|
54
|
+
else
|
|
55
|
+
if Forall::No === result or RSpec::Expectations::ExpectationNotMetError === result.error
|
|
56
|
+
message = "falsified (after %d test%s%s):\nInput: %s\nSeed: %d" %
|
|
57
|
+
[result.counter.ok,
|
|
58
|
+
result.counter.ok == 1 ? "" : "s",
|
|
59
|
+
(result.counter.shrunk.steps == 1 ? "and 1 shrink" :
|
|
60
|
+
result.counter.shrunk.steps == 0 ? "" : "and #{result.counter.shrinks} shrinks"),
|
|
61
|
+
result.counterexample.inspect,
|
|
62
|
+
result.seed]
|
|
63
|
+
|
|
64
|
+
error = ::RSpec::Expectations::ExpectationNotMetError.new(message)
|
|
65
|
+
source = property.binding.source_location.join(":")
|
|
66
|
+
source << " in `block in ...'"
|
|
67
|
+
error.set_backtrace(source)
|
|
68
|
+
raise error
|
|
69
|
+
else
|
|
70
|
+
message = "exception %s (after %d test%s%s):\nInput: %s\nSeed: %d" %
|
|
71
|
+
[result.error,
|
|
72
|
+
result.counter.ok,
|
|
73
|
+
result.counter.ok == 1 ? "" : "s",
|
|
74
|
+
(result.counter.shrunk.steps == 1 ? "and 1 shrink" :
|
|
75
|
+
result.counter.shrunk.steps == 0 ? "" : "and #{result.counter.shrinks} shrinks"),
|
|
76
|
+
result.counterexample.inspect,
|
|
77
|
+
result.seed]
|
|
78
|
+
|
|
79
|
+
error = ::RSpec::Expectations::ExpectationNotMetError.new(message)
|
|
80
|
+
source = property.binding.source_location.join(":")
|
|
81
|
+
source << " in `block in ...'"
|
|
82
|
+
error.set_backtrace(source)
|
|
83
|
+
raise error
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
result
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Assigns a classification label to a random input
|
|
92
|
+
# def label(name)
|
|
93
|
+
# @labels[name] += 1
|
|
94
|
+
# end
|
|
95
|
+
|
|
96
|
+
# # Declares at least `pct` of inputs should have the given label, or a test
|
|
97
|
+
# # will fail.
|
|
98
|
+
# def cover(name, pct=0.01)
|
|
99
|
+
# @cover[name] = pct
|
|
100
|
+
# end
|
|
101
|
+
|
|
102
|
+
# # Skip over this input and choose a new random input
|
|
103
|
+
# def skip
|
|
104
|
+
# end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Forall
|
|
4
|
+
class Random
|
|
5
|
+
def initialize(seed: nil)
|
|
6
|
+
@prng = ::Random.new(seed || ::Random.new_seed)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# @return [Integer]
|
|
10
|
+
def seed
|
|
11
|
+
@prng.seed
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [TrueClass | FalseClass]
|
|
15
|
+
def boolean
|
|
16
|
+
@prng.rand >= 0.5
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns a randomly chosen integer within the given bounds
|
|
20
|
+
#
|
|
21
|
+
# @param range [Range<Integer>]
|
|
22
|
+
# @return [Integer]
|
|
23
|
+
def integer(range = nil)
|
|
24
|
+
min = range&.min || -2**64-1
|
|
25
|
+
max = range&.max || 2**64+1
|
|
26
|
+
@prng.rand(min..max)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns a randomly chosen floating point number
|
|
30
|
+
#
|
|
31
|
+
# @paoram range [Range<Float>]
|
|
32
|
+
# @return [Float]
|
|
33
|
+
def float(range = nil)
|
|
34
|
+
min = range&.min || Float::MIN
|
|
35
|
+
max = range&.max || Float::MAX
|
|
36
|
+
@prng.rand(min..max)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns a randomly chosen Date within the given bounds
|
|
40
|
+
#
|
|
41
|
+
# @param range [Range<Date>]
|
|
42
|
+
# @return [Date]
|
|
43
|
+
def date(range = nil)
|
|
44
|
+
min = (range&.min || Date.civil(0000, 1, 1)).to_time
|
|
45
|
+
max = (range&.max || Date.civil(9999,12,31)).to_time + 86399
|
|
46
|
+
Time.at(float(min.to_f .. max.to_f)).to_date
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns a randomly chosen Time within the given bounds
|
|
50
|
+
#
|
|
51
|
+
# @param range [Range<Time>]
|
|
52
|
+
# @return [Time]
|
|
53
|
+
def time(range = nil, utc: nil)
|
|
54
|
+
min = range&.min || Time.utc(0000,1,1,0,0,0)
|
|
55
|
+
max = range&.max || Time.utc(9999,12,31,23,59,59)
|
|
56
|
+
rnd = float(min.to_f .. max.to_f)
|
|
57
|
+
|
|
58
|
+
if utc or (utc.nil? and (min.utc? or max.utc?))
|
|
59
|
+
Time.at(rnd).utc
|
|
60
|
+
else
|
|
61
|
+
Time.at(rnd)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns a randomly chosen DateTime within the given bounds
|
|
66
|
+
#
|
|
67
|
+
# @param range [Range<DateTime>]
|
|
68
|
+
# @return [DateTime]
|
|
69
|
+
def datetime(range = nil)
|
|
70
|
+
min = range&.min&.to_time
|
|
71
|
+
max = range&.max&.to_time
|
|
72
|
+
time(min..max).to_datetime
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Returns a randomly chosen range within the given bounds
|
|
76
|
+
#
|
|
77
|
+
# @param range [Range<Object>]
|
|
78
|
+
# @param width [Integer]
|
|
79
|
+
# @return [Range]
|
|
80
|
+
def range(range, width: nil)
|
|
81
|
+
min = range.min
|
|
82
|
+
max = range.max
|
|
83
|
+
|
|
84
|
+
if width.nil?
|
|
85
|
+
case min or max
|
|
86
|
+
when Float
|
|
87
|
+
a = float(range)
|
|
88
|
+
b = float(range)
|
|
89
|
+
when Integer
|
|
90
|
+
a = integer(range)
|
|
91
|
+
b = integer(range)
|
|
92
|
+
when Time
|
|
93
|
+
a = time(range)
|
|
94
|
+
b = time(range)
|
|
95
|
+
when Date
|
|
96
|
+
a = date(range)
|
|
97
|
+
b = date(range)
|
|
98
|
+
when DateTime
|
|
99
|
+
a = datetime(range)
|
|
100
|
+
b = datetime(range)
|
|
101
|
+
else
|
|
102
|
+
a, b = choose(range, count: 2)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if a < b
|
|
106
|
+
min = a
|
|
107
|
+
max = b
|
|
108
|
+
else
|
|
109
|
+
min = b
|
|
110
|
+
max = a
|
|
111
|
+
end
|
|
112
|
+
else
|
|
113
|
+
# Randomly choose a width within given bounds
|
|
114
|
+
width = choose(width) if Enumerable === width
|
|
115
|
+
|
|
116
|
+
case min or max
|
|
117
|
+
when Float
|
|
118
|
+
min = float(min: min, max: max-width)
|
|
119
|
+
max = min+width-1
|
|
120
|
+
when Integer
|
|
121
|
+
min = integer(min: min, max: max-width)
|
|
122
|
+
max = min+width-1
|
|
123
|
+
else
|
|
124
|
+
all = (min..max).to_a
|
|
125
|
+
max = all.size-1
|
|
126
|
+
|
|
127
|
+
# Randomly choose element indices
|
|
128
|
+
min = integer(min: 0, max: max-width)
|
|
129
|
+
max = min+width-1
|
|
130
|
+
|
|
131
|
+
min = all[min]
|
|
132
|
+
max = all[max]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
min..max
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Returns a uniformly random chosen element(s) from the given Enumerable
|
|
140
|
+
#
|
|
141
|
+
# @param items [Input::Some | Input::All | Range | Array]
|
|
142
|
+
# @return [Object]
|
|
143
|
+
def sample(items, count: nil)
|
|
144
|
+
case items
|
|
145
|
+
when Input
|
|
146
|
+
items.sample(self, count: count)
|
|
147
|
+
when Range
|
|
148
|
+
method =
|
|
149
|
+
if Integer === (items.min || items.max) then :integer
|
|
150
|
+
elsif Float === (items.min || items.max) then :float
|
|
151
|
+
elsif Time === (items.min || items.max) then :time
|
|
152
|
+
elsif defined?(Date) and Date === (items.min || items.max) then :date
|
|
153
|
+
elsif defined?(DateTime) and DateTime === (items.min || items.max) then :datetime
|
|
154
|
+
else
|
|
155
|
+
# NOTE: This is memory inefficient
|
|
156
|
+
items = items.to_a
|
|
157
|
+
|
|
158
|
+
if count.nil?
|
|
159
|
+
return items.sample(random: @prng)
|
|
160
|
+
else
|
|
161
|
+
return count.times.map{|_| items.sample(random: @prng) }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if count.nil?
|
|
166
|
+
send(method, items)
|
|
167
|
+
else
|
|
168
|
+
count.times.map{|_| send(method, items) }
|
|
169
|
+
end
|
|
170
|
+
else
|
|
171
|
+
unless items.respond_to?(:sample)
|
|
172
|
+
# NOTE: This works across many types but is memory inefficient
|
|
173
|
+
items = items.to_a
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if count.nil?
|
|
177
|
+
items.sample(random: @prng)
|
|
178
|
+
else
|
|
179
|
+
# Sample *with* replacement
|
|
180
|
+
count.times.map{|_| items.sample(random: @prng) }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
alias_method :choose, :sample
|
|
186
|
+
|
|
187
|
+
# Returns a uniformly random chosen element(s) from the given Enumerable
|
|
188
|
+
#
|
|
189
|
+
# @param items [Array<Object>]
|
|
190
|
+
# @param freqs [Array<Numeric>]
|
|
191
|
+
# @param count [Numeric]
|
|
192
|
+
# @return [Object]
|
|
193
|
+
def weighted(items, freqs, count: nil)
|
|
194
|
+
unless items.size == freqs.size
|
|
195
|
+
raise ArgumentError, "items and frequencies must have same size"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# This runs in O(n) time where n is the number of possible items. This is
|
|
199
|
+
# not dependent on `count`, the number of requested items.
|
|
200
|
+
if count.nil?
|
|
201
|
+
sum = freqs[0].to_f
|
|
202
|
+
res = items[0]
|
|
203
|
+
|
|
204
|
+
(1..items.size - 1).each do |i|
|
|
205
|
+
sum += freqs[i]
|
|
206
|
+
p = freqs[i] / sum
|
|
207
|
+
j = @prng.rand
|
|
208
|
+
res = items[i] if j <= p
|
|
209
|
+
end
|
|
210
|
+
else
|
|
211
|
+
sum = freqs[0, count].sum.to_f
|
|
212
|
+
res = items[0, count]
|
|
213
|
+
|
|
214
|
+
(count..items.size).each do |i|
|
|
215
|
+
sum += freqs[i]
|
|
216
|
+
p = count * freqs[i] / sum
|
|
217
|
+
j = @prng.rand
|
|
218
|
+
res[@prng.rand(count)] = items[i] if j <= p
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
res
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# @param items [Array<A>]
|
|
226
|
+
# @return [Array<A>]
|
|
227
|
+
def shuffle(items)
|
|
228
|
+
items.shuffle(random: @prng)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Generates a random permutation of the given size
|
|
232
|
+
#
|
|
233
|
+
# @param size [Intege]
|
|
234
|
+
# @return [Array<Integer>]
|
|
235
|
+
def permutation(size: nil)
|
|
236
|
+
(0..(size || integer(0..64))-1).to_a.shuffle!(random: @prng)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Generates an Array by repeatedly calling a block that returns a random
|
|
240
|
+
# value.
|
|
241
|
+
#
|
|
242
|
+
# @example
|
|
243
|
+
# rnd.array {|n| n } #=> [0,1,2,3,...]
|
|
244
|
+
# rnd.array(10..50) { integer(0..9) } #=> [8,2,1,1,...]
|
|
245
|
+
#
|
|
246
|
+
# @return Array
|
|
247
|
+
def array(size: nil)
|
|
248
|
+
size ||= integer(0..64)
|
|
249
|
+
size = choose(size) if Range === size
|
|
250
|
+
size.times.map{|n| yield n }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Generates a Hash by repeatedly calling a block that returns a random [key,
|
|
254
|
+
# val] pair.
|
|
255
|
+
#
|
|
256
|
+
# @yieldparam [Integer]
|
|
257
|
+
# @yieldreturn [Array<K, V>]
|
|
258
|
+
# @return [Hash<K, V>]
|
|
259
|
+
def hash(size: nil)
|
|
260
|
+
size ||= integer(0..64)
|
|
261
|
+
size = choose(size) if Range === size
|
|
262
|
+
hash = {}
|
|
263
|
+
|
|
264
|
+
until hash.size >= size
|
|
265
|
+
k, v = yield(hash.size)
|
|
266
|
+
hash[k] = v
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
hash
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def set(size: nil)
|
|
273
|
+
size ||= integer(0..64)
|
|
274
|
+
size = choose(size) if Range === size
|
|
275
|
+
set = Set.new
|
|
276
|
+
|
|
277
|
+
until set.size == size
|
|
278
|
+
set << yield(set.size)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
set
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
class Forall
|
|
2
|
+
class Shrink
|
|
3
|
+
def boolean(x)
|
|
4
|
+
x ? [false] : []
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def integer(x, range = 0..2**64-1)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def float(x, range = 0..Float::MAX)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def string
|
|
14
|
+
# TODO
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def date
|
|
18
|
+
# TODO
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def time
|
|
22
|
+
# TODO
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def datetime
|
|
26
|
+
# TODO
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def range(x, range = nil, width: nil)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def sample(items, count: nil)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
alias_method :choose, :sample
|
|
36
|
+
|
|
37
|
+
def permutation(x, size: nil)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def array(xs, size: nil)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def hash(x, size: nil)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def set(x, size: nil)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: forall
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kyle Putnam
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2021-05-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description:
|
|
14
|
+
email:
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- lib/forall.rb
|
|
20
|
+
- lib/forall/counter.rb
|
|
21
|
+
- lib/forall/input.rb
|
|
22
|
+
- lib/forall/matchers.rb
|
|
23
|
+
- lib/forall/random.rb
|
|
24
|
+
- lib/forall/shrink.rb
|
|
25
|
+
homepage: https://github.com/kputnam/forall
|
|
26
|
+
licenses:
|
|
27
|
+
- MIT
|
|
28
|
+
metadata: {}
|
|
29
|
+
post_install_message:
|
|
30
|
+
rdoc_options: []
|
|
31
|
+
require_paths:
|
|
32
|
+
- lib
|
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
|
+
requirements:
|
|
35
|
+
- - ">="
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: 2.0.0
|
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 2.5.0
|
|
43
|
+
requirements: []
|
|
44
|
+
rubygems_version: 3.1.4
|
|
45
|
+
signing_key:
|
|
46
|
+
specification_version: 4
|
|
47
|
+
summary: Ruby generative property test library (ala QuickCheck)
|
|
48
|
+
test_files: []
|