forall 1.0.0

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