pbt 0.0.1 → 0.1.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.
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pbt/arbitrary/arbitrary"
4
+ require "pbt/arbitrary/constant"
5
+ require "pbt/arbitrary/constant_arbitrary"
6
+ require "pbt/arbitrary/array_arbitrary"
7
+ require "pbt/arbitrary/integer_arbitrary"
8
+ require "pbt/arbitrary/tuple_arbitrary"
9
+ require "pbt/arbitrary/fixed_hash_arbitrary"
10
+ require "pbt/arbitrary/choose_arbitrary"
11
+ require "pbt/arbitrary/one_of_arbitrary"
12
+ require "pbt/arbitrary/map_arbitrary"
13
+ require "pbt/arbitrary/filter_arbitrary"
14
+
15
+ module Pbt
16
+ module Arbitrary
17
+ module ArbitraryMethods
18
+ # For integers between min (included) and max (included).
19
+ #
20
+ # @param min [Integer] Lower limit for the generated integers (included).
21
+ # @param max [Integer] Upper limit for the generated integers (included).
22
+ # @return [Arbitrary<Integer>]
23
+ def integer(min: -1000000, max: 1000000)
24
+ IntegerArbitrary.new(min, max)
25
+ end
26
+
27
+ # For natural numbers (non-negative integers) between 0 (included) and max (included).
28
+ #
29
+ # @param max [Integer] Upper limit for the generated integers (included).
30
+ # @return [Arbitrary<Integer>]
31
+ def nat(max: nil)
32
+ integer(min: 0, max: max)
33
+ end
34
+
35
+ # For arrays of values generated by `arb`.
36
+ #
37
+ # @param arb [Arbitrary<T>] Arbitrary used to generate the values of the array.
38
+ # @param min [Integer] Lower limit of the generated array size.
39
+ # @param max [Integer] Upper limit of the generated array size.
40
+ # @param empty [Boolean] Whether the array can be empty or not.
41
+ # @return [Arbitrary<T>]
42
+ def array(arb, min: 0, max: 10, empty: true)
43
+ raise ArgumentError if min < 0
44
+ min = 1 if min.zero? && !empty
45
+
46
+ ArrayArbitrary.new(arb, min, max)
47
+ end
48
+
49
+ # For tuples of values generated by `arbs`.
50
+ #
51
+ # @param arbs [Array<Arbitrary<...T>>] Arbitraries used to generate the values of the tuple.
52
+ # @return [Arbitrary<Array<...T>>]
53
+ def tuple(*arbs)
54
+ TupleArbitrary.new(*arbs)
55
+ end
56
+
57
+ # For fixed hashes of values generated by `hash`.
58
+ #
59
+ # @example
60
+ # arb = Pbt.fixed_hash(x: Pbt.integer, y: Pbt.integer)
61
+ # arb.generate(Random.new) # => {x: -450108, y: 42}
62
+ #
63
+ # @param hash [Hash<Object, Arbitrary<T>>] Hash with any keys and arbitraries as values.
64
+ # @return [Arbitrary<Hash<Object, T>>]
65
+ def fixed_hash(hash)
66
+ FixedHashArbitrary.new(hash)
67
+ end
68
+
69
+ # Picks a random integer in the given range.
70
+ #
71
+ # @see Pbt.one_of
72
+ # @param range [Range<Integer>] Range of integers to choose from.
73
+ # @return [Arbitrary<Integer>]
74
+ def choose(range)
75
+ ChooseArbitrary.new(range)
76
+ end
77
+
78
+ # Picks a random element from the given choices.
79
+ # The choices can be of any type.
80
+ #
81
+ # @see Pbt.one_of
82
+ # @param choices [Array<Object>] Array of choices.
83
+ # @return [Arbitrary<Object>]
84
+ def one_of(*choices)
85
+ OneOfArbitrary.new(choices)
86
+ end
87
+
88
+ # For a lowercase hexadecimal character.
89
+ #
90
+ # @return [Arbitrary<String>]
91
+ def hexa
92
+ one_of(*HEXA_CHARS)
93
+ end
94
+
95
+ # For lowercase hexadecimal stings.
96
+ #
97
+ # @see Pbt.array
98
+ # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
99
+ # @return [Arbitrary<String>]
100
+ def hexa_string(**kwargs)
101
+ array(hexa, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER)
102
+ end
103
+
104
+ # For a single unicode character (including printable and non-printable).
105
+ #
106
+ # @return [Arbitrary<String>]
107
+ def char
108
+ choose(CHAR_RANGE).map(CHAR_MAPPER, CHAR_UNMAPPER)
109
+ end
110
+
111
+ # For an alphanumeric character.
112
+ #
113
+ # @return [Arbitrary<String>]
114
+ def alphanumeric_char
115
+ one_of(*ALPHANUMERIC_CHARS)
116
+ end
117
+
118
+ # For alphanumeric strings.
119
+ #
120
+ # @see Pbt.array
121
+ # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
122
+ # @return [Arbitrary<String>]
123
+ def alphanumeric_string(**kwargs)
124
+ array(alphanumeric_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER)
125
+ end
126
+
127
+ # For an ascii character.
128
+ #
129
+ # @return [Arbitrary<String>]
130
+ def ascii_char
131
+ one_of(*ASCII_CHARS)
132
+ end
133
+
134
+ # For ascii strings.
135
+ #
136
+ # @see Pbt.array
137
+ # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
138
+ # @return [Arbitrary<String>]
139
+ def ascii_string(**kwargs)
140
+ array(ascii_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER)
141
+ end
142
+
143
+ # For a printable ascii character.
144
+ #
145
+ # @return [Arbitrary<String>]
146
+ def printable_ascii_char
147
+ one_of(*PRINTABLE_ASCII_CHARS)
148
+ end
149
+
150
+ # For printable ascii strings.
151
+ #
152
+ # @see Pbt.array
153
+ # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
154
+ # @return [Arbitrary<String>]
155
+ def printable_ascii_string(**kwargs)
156
+ array(printable_ascii_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER)
157
+ end
158
+
159
+ # For a printable character.
160
+ #
161
+ # @return [Arbitrary<String>]
162
+ def printable_char
163
+ one_of(*PRINTABLE_CHARS)
164
+ end
165
+
166
+ # For printable strings.
167
+ #
168
+ # @see Pbt.array
169
+ # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
170
+ # @return [Arbitrary<String>]
171
+ def printable_string(**kwargs)
172
+ array(printable_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER)
173
+ end
174
+
175
+ # For symbols.
176
+ #
177
+ # @see Pbt.array
178
+ # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
179
+ # @return [Arbitrary<Symbol>]
180
+ def symbol(**kwargs)
181
+ array(one_of(*SYMBOL_SAFE_CHARS), empty: false, **kwargs).map(SYMBOL_MAPPER, SYMBOL_UNMAPPER)
182
+ end
183
+
184
+ # For floats.
185
+ #
186
+ # @return [Arbitrary<Float>]
187
+ def float
188
+ tuple(integer, integer).map(FLOAT_MAPPER, FLOAT_UNMAPPER)
189
+ end
190
+
191
+ # For symbols.
192
+ #
193
+ # @param arb [Arbitrary<T>] Arbitrary used to generate the values of the array.
194
+ # @param min [Integer] Lower limit of the generated set size.
195
+ # @param max [Integer] Upper limit of the generated set size.
196
+ # @param empty [Boolean] Whether the array can be empty or not.
197
+ # @return [Arbitrary<T>]
198
+ def set(arb, min: 0, max: 10, empty: true)
199
+ array(arb, min: min, max: max, empty: empty).map(SET_MAPPER, SET_UNMAPPER)
200
+ end
201
+
202
+ # For hashes of any keys and values.
203
+ # If you want to call `Object#hash` for `Pbt`, call this method without arguments.
204
+ #
205
+ # @example
206
+ # hash_generator = Pbt.hash(Pbt.symbol, Pbt.integer)
207
+ # hash_generator.generate(Random.new) # => {:buo=>466214, :cwftzvglq=>331431, :wweccnzg=>-848867}
208
+ #
209
+ # @see Pbt.array
210
+ # @param args [Array<Arbitrary<T,U>>] Arbitraries to generate Hash. First one is for key and second is for value.
211
+ # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
212
+ # @return [Arbitrary<Hash<T,U>>]
213
+ def hash(*args, **kwargs)
214
+ if args.size == 2
215
+ key_arbitrary, value_arbitrary = args
216
+ array(tuple(key_arbitrary, value_arbitrary), **kwargs).map(HASH_MAPPER, HASH_UNMAPPER)
217
+ else
218
+ super # call `Object#hash`
219
+ end
220
+ end
221
+
222
+ # For booleans.
223
+ #
224
+ # @return [Arbitrary<Boolean>]
225
+ def boolean
226
+ one_of(true, false)
227
+ end
228
+
229
+ # For any constant values.
230
+ # It's useful when you want to use a constant value that behaves like an arbitrary.
231
+ #
232
+ # @example
233
+ # Pbt.constant(42).generate(Random.new) # => 42
234
+ #
235
+ # @param val [Object]
236
+ # @return [Arbitrary<Object>]
237
+ def constant(val)
238
+ ConstantArbitrary.new(val)
239
+ end
240
+
241
+ # For nil.
242
+ #
243
+ # @return [Arbitrary<nil>]
244
+ def nil
245
+ constant(nil)
246
+ end
247
+
248
+ # For dates between `base_date + past_offset_days` and `base_date + future_offset_days`.
249
+ #
250
+ # @param base_date [Date] Base date for the generated dates.
251
+ # @param past_offset_days [Integer] Offset days for the past. Default is -18250 (about 50 years).
252
+ # @param future_offset_days [Integer] Offset days for the future. Default is 18250 (about 50 years).
253
+ # @return [Arbitrary<Date>]
254
+ def date(base_date: Date.today, past_offset_days: -18250, future_offset_days: 18250)
255
+ offset_arb = integer(min: past_offset_days, max: future_offset_days)
256
+ offset_arb.map(DATE_MAPPER.call(base_date), DATE_UNMAPPER.call(base_date))
257
+ end
258
+
259
+ # For past dates between `base_date - past_offset_days` and `base_date`.
260
+ #
261
+ # @param base_date [Date] Base date for the generated dates.
262
+ # @param past_offset_days [Integer] Offset days for the past. Default is -18250 (about 50 years).
263
+ # @return [Arbitrary<Date>]
264
+ def past_date(base_date: Date.today, past_offset_days: -18250)
265
+ date(base_date: base_date, past_offset_days: past_offset_days, future_offset_days: 0)
266
+ end
267
+
268
+ # For future dates between `base_date` and `base_date - future_offset_days`.
269
+ #
270
+ # @param base_date [Date] Base date for the generated dates.
271
+ # @param future_offset_days [Integer] Offset days for the future. Default is 18250 (about 50 years).
272
+ # @return [Arbitrary<Date>]
273
+ def future_date(base_date: Date.today, future_offset_days: 18250)
274
+ date(base_date: base_date, past_offset_days: 0, future_offset_days: future_offset_days)
275
+ end
276
+
277
+ # For times between `base_time + past_offset_seconds` and `base_time + future_offset_seconds`.
278
+ #
279
+ # @param base_time [Date] Base time for the generated times.
280
+ # @param past_offset_seconds [Integer] Offset seconds for the past. Default is -1576800000 (about 50 years).
281
+ # @param future_offset_seconds [Integer] Offset seconds for the future. Default is 1576800000 (about 50 years).
282
+ # @return [Arbitrary<Time>]
283
+ def time(base_time: Time.now, past_offset_seconds: -1576800000, future_offset_seconds: 1576800000)
284
+ offset_arb = integer(min: past_offset_seconds, max: future_offset_seconds)
285
+ offset_arb.map(TIME_MAPPER.call(base_time), TIME_UNMAPPER.call(base_time))
286
+ end
287
+
288
+ # For past times between `base_time + past_offset_seconds` and `base_time`
289
+ #
290
+ # @param base_time [Date] Base time for the generated times.
291
+ # @param past_offset_seconds [Integer] Offset seconds for the past. Default is -1576800000 (about 50 years).
292
+ # @return [Arbitrary<Time>]
293
+ def past_time(base_time: Time.now, past_offset_seconds: -1576800000)
294
+ time(base_time: base_time, past_offset_seconds: past_offset_seconds, future_offset_seconds: 0)
295
+ end
296
+
297
+ # For future times between `base_time` and `base_time + future_offset_seconds`.
298
+ #
299
+ # @param base_time [Date] Base time for the generated times.
300
+ # @param future_offset_seconds [Integer] Offset seconds for the future. Default is 1576800000 (about 50 years).
301
+ # @return [Arbitrary<Time>]
302
+ def future_time(base_time: Time.now, future_offset_seconds: 1576800000)
303
+ time(base_time: base_time, past_offset_seconds: 0, future_offset_seconds: future_offset_seconds)
304
+ end
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pbt
4
+ module Arbitrary
5
+ # Generates arrays of values generated by `value_arb` with type `T`.
6
+ class ArrayArbitrary < Arbitrary
7
+ DEFAULT_MAX_SIZE = 10
8
+ private_constant :DEFAULT_MAX_SIZE
9
+
10
+ # @param value_arb [Arbitrary] Arbitrary to generate values.
11
+ # @param min_length [Integer] Minimum length of the generated array. Default is 0.
12
+ # @param max_length [Integer] Maximum length of the generated array. Default is 10.
13
+ def initialize(value_arb, min_length = 0, max_length = DEFAULT_MAX_SIZE)
14
+ raise ArgumentError, "min_length must be zero or positive number" if min_length < 0
15
+
16
+ @min_length = min_length
17
+ @max_length = max_length
18
+ @value_arb = value_arb
19
+ @length_arb = IntegerArbitrary.new(min_length, max_length)
20
+ end
21
+
22
+ # @see Arbitrary#generate
23
+ def generate(rng)
24
+ length = @length_arb.generate(rng)
25
+ length.times.map { @value_arb.generate(rng) }
26
+ end
27
+
28
+ # @see Arbitrary#shrink
29
+ def shrink(current)
30
+ return Enumerator.new { |_| } if current.size == @min_length
31
+
32
+ Enumerator.new do |y|
33
+ # First, shrink the length of the array and try combinations of elements.
34
+ # But this doesn't try all possible combinations since it'd be too huge and slow.
35
+ @length_arb.shrink(current.size).each do |length|
36
+ if length == 0
37
+ y.yield []
38
+ next
39
+ end
40
+ current.each_cons(length) do |con|
41
+ y.yield con
42
+ end
43
+ end
44
+
45
+ # Second, shrink each element of the array.
46
+ current.each_with_index do |item, i|
47
+ @value_arb.shrink(item).each do |val|
48
+ y.yield [*current[...i], val, *current[i + 1..]]
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pbt
4
+ module Arbitrary
5
+ # Generates a random value from a range.
6
+ class ChooseArbitrary < Arbitrary
7
+ # @param range [Range<Integer>]
8
+ def initialize(range)
9
+ @range = range
10
+ end
11
+
12
+ # @see Arbitrary#generate
13
+ def generate(rng)
14
+ rng.rand(@range)
15
+ end
16
+
17
+ # @see Arbitrary#shrink
18
+ def shrink(current)
19
+ # Range is ordered from min to max, so we can just shrink towards min.
20
+ min, max = [@range.begin, @range.end].sort
21
+ IntegerArbitrary.new(min, max).shrink(current, target: min)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "set" # For 3.1 or prior. Set became built-in library since Ruby 3.2.0
5
+
6
+ module Pbt
7
+ module Arbitrary
8
+ HEXA_CHARS = [*("0".."9"), *("a".."f")].freeze
9
+ CHAR_RANGE = (0..0x10FFFF)
10
+ SYMBOL_SAFE_CHARS = [*("a".."z"), "-"].freeze
11
+ ALPHANUMERIC_CHARS = [*("a".."z"), *("A".."Z"), *("0".."9")].freeze
12
+ PRINTABLE_ASCII_CHARS = [*(" ".."~")].freeze
13
+ ASCII_CHARS = [*PRINTABLE_ASCII_CHARS, "\n", "\r", "\t", "\v", "\b", "\f", "\e", "\d", "\a"].freeze
14
+ PRINTABLE_CHARS = [
15
+ *ASCII_CHARS,
16
+ *("\u{A0}".."\u{D7FF}"),
17
+ *("\u{E000}".."\u{FFFD}"),
18
+ *("\u{10000}".."\u{10FFFF}")
19
+ ].freeze
20
+ CHAR_MAPPER = ->(v) { [v].pack("U") }
21
+ CHAR_UNMAPPER = ->(v) { v.unpack1("U") }
22
+ STRING_MAPPER = ->(v) { v.join }
23
+ STRING_UNMAPPER = ->(v) { v.chars }
24
+ SYMBOL_MAPPER = ->(v) { v.join.to_sym }
25
+ SYMBOL_UNMAPPER = ->(v) { v.to_s.chars }
26
+ FLOAT_MAPPER = ->((x, y)) { x.to_f / (y.to_f.abs + 1.0) }
27
+ FLOAT_UNMAPPER = ->(v) { [v, 0] }
28
+ SET_MAPPER = ->(v) { v.to_set }
29
+ SET_UNMAPPER = ->(v) { v.to_a }
30
+ HASH_MAPPER = ->(v) { v.to_h }
31
+ HASH_UNMAPPER = ->(v) { v.to_a }
32
+ DATE_MAPPER = ->(epoch) { ->(v) { epoch + v } }
33
+ DATE_UNMAPPER = ->(epoch) { ->(v) { (v - epoch).to_i } }
34
+ TIME_MAPPER = DATE_MAPPER
35
+ TIME_UNMAPPER = DATE_UNMAPPER
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pbt
4
+ module Arbitrary
5
+ # Generates a constant value.
6
+ class ConstantArbitrary < Arbitrary
7
+ # @param val [Object]
8
+ def initialize(val)
9
+ @val = val
10
+ end
11
+
12
+ # @see Arbitrary#generate
13
+ def generate(rng)
14
+ @val
15
+ end
16
+
17
+ # @see Arbitrary#shrink
18
+ def shrink(current)
19
+ Enumerator.new {}
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pbt
4
+ module Arbitrary
5
+ # Generates values from another arbitrary, but only if they pass a predicate.
6
+ class FilterArbitrary < Arbitrary
7
+ # @param arb [Arbitrary] Arbitrary to generate values to be filtered.
8
+ # @param refinement [Proc] Predicate proc to test each produced element. Return true to keep the element, false otherwise.
9
+ def initialize(arb, &refinement)
10
+ @arb = arb
11
+ @refinement = refinement
12
+ end
13
+
14
+ # @see Arbitrary#generate
15
+ def generate(rng)
16
+ loop do
17
+ val = @arb.generate(rng)
18
+ return val if @refinement.call(val)
19
+ end
20
+ end
21
+
22
+ # @see Arbitrary#shrink
23
+ def shrink(current)
24
+ Enumerator.new do |y|
25
+ @arb.shrink(current).each do |v|
26
+ if @refinement.call(v)
27
+ y.yield v
28
+ else
29
+ next
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pbt
4
+ module Arbitrary
5
+ # Generates a hash with fixed keys and arbitrary values.
6
+ class FixedHashArbitrary < Arbitrary
7
+ # @param hash [Hash<Object, Arbitrary<T>>] Hash with any keys and arbitraries as values.
8
+ def initialize(hash)
9
+ @keys = hash.keys
10
+ @arb = TupleArbitrary.new(*hash.values)
11
+ end
12
+
13
+ # @see Arbitrary#generate
14
+ def generate(rng)
15
+ values = @arb.generate(rng)
16
+ @keys.zip(values).to_h
17
+ end
18
+
19
+ # @see Arbitrary#shrink
20
+ def shrink(current)
21
+ # This is not the most comprehensive but allows a reasonable number of entries in the shrink
22
+ Enumerator.new do |y|
23
+ @arb.shrink(current.values).each do |next_values|
24
+ y << @keys.zip(next_values).to_h
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pbt
4
+ module Arbitrary
5
+ # Generates random integers between `min` and `max`.
6
+ class IntegerArbitrary < Arbitrary
7
+ DEFAULT_TARGET = 0
8
+ private_constant :DEFAULT_TARGET
9
+
10
+ # @param min [Integer] Minimum value to generate.
11
+ # @param max [Integer] Maximum value to generate.
12
+ def initialize(min, max)
13
+ @min = min
14
+ @max = max
15
+ end
16
+
17
+ # @see Arbitrary#generate
18
+ def generate(rng)
19
+ rng.rand(@min..@max)
20
+ end
21
+
22
+ # @see Arbitrary#shrink
23
+ def shrink(current, target: DEFAULT_TARGET)
24
+ gap = current - target
25
+ return Enumerator.new { |_| } if gap == 0
26
+
27
+ is_positive_gap = gap > 0
28
+
29
+ Enumerator.new do |y|
30
+ while (diff = (current - target).abs) > 1
31
+ halved = diff / 2
32
+ current -= is_positive_gap ? halved : -halved
33
+ y.yield current
34
+ end
35
+ y.yield target # no diff here
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pbt
4
+ module Arbitrary
5
+ # Generates a mapped value from another arbitrary.
6
+ class MapArbitrary < Arbitrary
7
+ # @param arb [Arbitrary] Arbitrary to generate values to be mapped.
8
+ # @param mapper [Proc] Proc to map generated values. Mainly used for generation.
9
+ # @param unmapper [Proc] Proc to unmap generated values. Used for shrinking.
10
+ def initialize(arb, mapper, unmapper)
11
+ @arb = arb
12
+ @mapper = mapper
13
+ @unmapper = unmapper
14
+ end
15
+
16
+ # @see Arbitrary#generate
17
+ def generate(rng)
18
+ @mapper.call(@arb.generate(rng))
19
+ end
20
+
21
+ # @see Arbitrary#shrink
22
+ def shrink(current)
23
+ Enumerator.new do |y|
24
+ @arb.shrink(@unmapper.call(current)).each do |v|
25
+ y.yield @mapper.call(v)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pbt
4
+ module Arbitrary
5
+ # Generates one of the given choices.
6
+ class OneOfArbitrary < Arbitrary
7
+ # @param choices [Array] List of choices.
8
+ def initialize(choices)
9
+ @choices = choices
10
+ @idx_arb = IntegerArbitrary.new(0, choices.size - 1)
11
+ end
12
+
13
+ # @see Arbitrary#generate
14
+ def generate(rng)
15
+ @choices[@idx_arb.generate(rng)]
16
+ end
17
+
18
+ # @see Arbitrary#shrink
19
+ def shrink(current)
20
+ # Shrinks to values earlier in the list of `choices`.
21
+ Enumerator.new do |y|
22
+ @idx_arb.shrink(@choices.index(current)).map do |idx|
23
+ y << @choices[idx]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pbt
4
+ module Arbitrary
5
+ # Generates a tuple of arbitrary values.
6
+ class TupleArbitrary < Arbitrary
7
+ # @param arbs [Array<Arbitrary>] Arbitraries used to generate the values of the tuple.
8
+ def initialize(*arbs)
9
+ @arbs = arbs
10
+ end
11
+
12
+ # @see Arbitrary#generate
13
+ def generate(rng)
14
+ @arbs.map { |arb| arb.generate(rng) }
15
+ end
16
+
17
+ # @see Arbitrary#shrink
18
+ def shrink(current)
19
+ # This is not the most comprehensive but allows a reasonable number of entries in the shrink.
20
+ Enumerator.new do |y|
21
+ @arbs.each_with_index do |arb, idx|
22
+ arb.shrink(current[idx]).each do |v|
23
+ next_values = current.dup
24
+ next_values[idx] = v
25
+ y << next_values
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pbt
4
+ module Check
5
+ # @private
6
+ Case = Struct.new(:val, :ractor, :exception, :index, keyword_init: true)
7
+ end
8
+ end