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 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: []