pbt 0.0.1 → 0.1.0

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