forall 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|