hegeltest 0.3.4 → 0.4.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 +4 -4
- data/CHANGELOG.md +182 -0
- data/README.ja.md +408 -0
- data/README.md +208 -102
- data/Rakefile +6 -3
- data/docs/README.md +9 -0
- data/docs/adr/0027-report-each-runs-test-location-from-the-callers-frame.md +63 -0
- data/docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md +91 -0
- data/docs/adr/0029-keep-notes-local-and-defer-run-error-kinds.md +72 -0
- data/docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md +48 -0
- data/docs/adr/0031-take-the-alphabet-options-from-hegel-rust.md +52 -0
- data/docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md +147 -0
- data/docs/adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md +80 -0
- data/docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md +120 -0
- data/docs/adr/0035-defer-concurrent-state-machines-and-the-engine-printer.md +136 -0
- data/docs/architecture.md +16 -1
- data/lib/hegel/errors.rb +24 -0
- data/lib/hegel/generator.rb +52 -25
- data/lib/hegel/generators.rb +715 -45
- data/lib/hegel/grammar_cache.rb +36 -0
- data/lib/hegel/lib_hegel/real.rb +320 -17
- data/lib/hegel/lib_hegel.rb +31 -6
- data/lib/hegel/libhegel_version.rb +1 -1
- data/lib/hegel/report.rb +13 -10
- data/lib/hegel/runner.rb +61 -27
- data/lib/hegel/settings.rb +92 -16
- data/lib/hegel/state_machine.rb +17 -4
- data/lib/hegel/stateful/pool.rb +52 -0
- data/lib/hegel/stateful.rb +2 -2
- data/lib/hegel/syntax/methods.rb +99 -15
- data/lib/hegel/test_case.rb +256 -35
- data/lib/hegel/test_location.rb +49 -0
- data/lib/hegel/version.rb +1 -1
- data/lib/hegel.rb +21 -5
- data/sig/hegel.rbs +295 -42
- data/skills/hegel-ruby/references/ruby/reference.md +463 -41
- metadata +13 -1
data/lib/hegel/generators.rb
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require "date"
|
|
4
4
|
require "ipaddr"
|
|
5
|
+
require "pathname"
|
|
5
6
|
require_relative "errors"
|
|
6
7
|
require_relative "generator"
|
|
8
|
+
require_relative "grammar_cache"
|
|
7
9
|
require_relative "lib_hegel"
|
|
8
10
|
|
|
9
11
|
module Hegel
|
|
@@ -19,6 +21,15 @@ module Hegel
|
|
|
19
21
|
# generator built once and drawn from many times fails the same way
|
|
20
22
|
# hegel-rust's does.
|
|
21
23
|
module Generators
|
|
24
|
+
# hegel-rust's DEFAULT_MAX_SIZE for text and binary. A min_size above it
|
|
25
|
+
# moves the default to min_size + 100, so a lone min_size never makes
|
|
26
|
+
# the range empty.
|
|
27
|
+
DEFAULT_MAX_SIZE = 100
|
|
28
|
+
|
|
29
|
+
def self.default_max_size(min_size)
|
|
30
|
+
(min_size > DEFAULT_MAX_SIZE) ? min_size + DEFAULT_MAX_SIZE : DEFAULT_MAX_SIZE
|
|
31
|
+
end
|
|
32
|
+
|
|
22
33
|
# Hegel::Syntax::Methods#booleans. A boolean, true with probability +p+.
|
|
23
34
|
class BooleanGenerator < Generator
|
|
24
35
|
def initialize(p:)
|
|
@@ -75,21 +86,26 @@ module Hegel
|
|
|
75
86
|
#
|
|
76
87
|
# The two checks below come with those defaults: hegel-rust and
|
|
77
88
|
# hegel-go each raise on the same two combinations, and the messages
|
|
78
|
-
# here carry the substring theirs do.
|
|
79
|
-
#
|
|
80
|
-
#
|
|
89
|
+
# here carry the substring theirs do.
|
|
90
|
+
#
|
|
91
|
+
# allow_subnormal is three-valued too. nil allows subnormals whenever
|
|
92
|
+
# the bounds admit one, as hegel-rust's does, and the keyword maps to
|
|
93
|
+
# hegel_generate_float's smallest_nonzero_magnitude: the smallest
|
|
94
|
+
# subnormal when allowed, the smallest normal otherwise.
|
|
81
95
|
class FloatGenerator < Generator
|
|
82
96
|
# hegel_generate_float's width; Ruby has one Float type, the 64-bit
|
|
83
97
|
# IEEE 754 double, so this is never anything else.
|
|
84
98
|
WIDTH = 64
|
|
85
99
|
|
|
86
|
-
def initialize(min_value:, max_value:, allow_nan:, allow_infinity:, exclude_min:, exclude_max
|
|
100
|
+
def initialize(min_value:, max_value:, allow_nan:, allow_infinity:, exclude_min:, exclude_max:,
|
|
101
|
+
allow_subnormal:)
|
|
87
102
|
@min_value = min_value
|
|
88
103
|
@max_value = max_value
|
|
89
104
|
@allow_nan = allow_nan
|
|
90
105
|
@allow_infinity = allow_infinity
|
|
91
106
|
@exclude_min = exclude_min
|
|
92
107
|
@exclude_max = exclude_max
|
|
108
|
+
@allow_subnormal = allow_subnormal
|
|
93
109
|
end
|
|
94
110
|
|
|
95
111
|
def do_draw(tc)
|
|
@@ -113,24 +129,126 @@ module Hegel
|
|
|
113
129
|
WIDTH, min_value, max_value,
|
|
114
130
|
allow_nan: allow_nan, allow_infinity: allow_infinity,
|
|
115
131
|
exclude_min: @exclude_min, exclude_max: @exclude_max,
|
|
116
|
-
smallest_nonzero_magnitude:
|
|
132
|
+
smallest_nonzero_magnitude: smallest_nonzero_magnitude
|
|
117
133
|
)
|
|
118
134
|
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def smallest_nonzero_magnitude
|
|
139
|
+
allow_subnormal = @allow_subnormal.nil? ? subnormal_default : @allow_subnormal
|
|
140
|
+
if allow_subnormal
|
|
141
|
+
if !@min_value.nil? && @min_value >= Float::MIN
|
|
142
|
+
raise Hegel::Error, "floats: InvalidArgument: allow_subnormal=true, but min_value excludes " \
|
|
143
|
+
"all values below the smallest positive normal #{Float::MIN}"
|
|
144
|
+
end
|
|
145
|
+
if !@max_value.nil? && @max_value <= -Float::MIN
|
|
146
|
+
raise Hegel::Error, "floats: InvalidArgument: allow_subnormal=true, but max_value excludes " \
|
|
147
|
+
"all values above the smallest negative normal -#{Float::MIN}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
LibHegel::HEGEL_FLOAT64_SMALLEST_NONZERO_MAGNITUDE_UNRESTRICTED
|
|
151
|
+
else
|
|
152
|
+
if only_subnormals_between_bounds?
|
|
153
|
+
raise Hegel::Error, "floats: InvalidArgument: allow_subnormal=false leaves no #{WIDTH}-bit " \
|
|
154
|
+
"floating-point values in [#{@min_value}, #{@max_value}]"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
Float::MIN
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def only_subnormals_between_bounds?
|
|
162
|
+
return false if @min_value.nil? || @max_value.nil? || 0.0.between?(@min_value, @max_value)
|
|
163
|
+
|
|
164
|
+
@max_value < Float::MIN && @min_value > -Float::MIN
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def subnormal_default
|
|
168
|
+
(@min_value.nil? || @min_value < Float::MIN) && (@max_value.nil? || @max_value > -Float::MIN)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The codecs hegel_string_generator_text accepts. Checked here as well so
|
|
173
|
+
# the message matches hegel-rust's, which rejects an unknown codec before
|
|
174
|
+
# the engine sees it.
|
|
175
|
+
CODECS = %w[ascii latin-1 iso-8859-1 utf-8].freeze
|
|
176
|
+
|
|
177
|
+
# The categories that contain surrogate codepoints. hegel-rust rejects
|
|
178
|
+
# them in +categories+ because its strings cannot hold a surrogate.
|
|
179
|
+
SURROGATE_CATEGORIES = %w[Cs C].freeze
|
|
180
|
+
|
|
181
|
+
# Hegel::Syntax::Methods#characters. A String of exactly one character.
|
|
182
|
+
# It also carries the alphabet options that TextGenerator and
|
|
183
|
+
# FromRegexGenerator share, so all three build their text handle from the
|
|
184
|
+
# same arguments.
|
|
185
|
+
class CharactersGenerator < Generator
|
|
186
|
+
def initialize(codec: nil, min_codepoint: nil, max_codepoint: nil, categories: nil, exclude_categories: nil,
|
|
187
|
+
include_characters: nil, exclude_characters: nil)
|
|
188
|
+
@options = {
|
|
189
|
+
codec: codec, min_codepoint: min_codepoint, max_codepoint: max_codepoint, categories: categories,
|
|
190
|
+
exclude_categories: exclude_categories, include_characters: include_characters,
|
|
191
|
+
exclude_characters: exclude_characters
|
|
192
|
+
}.freeze
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# The alphabet options as given, nil for each one left out.
|
|
196
|
+
attr_reader :options
|
|
197
|
+
|
|
198
|
+
def do_draw(tc)
|
|
199
|
+
with_alphabet(tc, "characters", 1, 1) { |generator| tc.generate_string(generator) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Whether any alphabet option was passed, which TextGenerator needs to
|
|
203
|
+
# refuse an +alphabet+ given beside one.
|
|
204
|
+
def options_given?
|
|
205
|
+
@options.each_value.any? { |value| !value.nil? }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Yields a text handle over this alphabet for strings of
|
|
209
|
+
# [min_size, max_size] characters, freed when the block returns.
|
|
210
|
+
# +name+ is the factory method whose draw this is, for the messages.
|
|
211
|
+
def with_alphabet(tc, name, min_size, max_size, &block)
|
|
212
|
+
tc.with_text_generator(min_size: min_size, max_size: max_size, **engine_options(name), &block)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
def engine_options(name)
|
|
218
|
+
codec = @options[:codec]
|
|
219
|
+
raise Hegel::Error, "#{name}: invalid codec: #{codec}" if codec && !CODECS.include?(codec)
|
|
220
|
+
|
|
221
|
+
categories = @options[:categories]
|
|
222
|
+
exclude_categories = @options[:exclude_categories]
|
|
223
|
+
if categories
|
|
224
|
+
surrogate = categories.find { |category| SURROGATE_CATEGORIES.include?(category) }
|
|
225
|
+
if surrogate
|
|
226
|
+
raise Hegel::Error,
|
|
227
|
+
"#{name}: Category \"#{surrogate}\" includes surrogate codepoints (Cs), " \
|
|
228
|
+
"which a valid UTF-8 String cannot hold."
|
|
229
|
+
end
|
|
230
|
+
# hegel-rust drops exclude_categories when categories is given.
|
|
231
|
+
exclude_categories = nil
|
|
232
|
+
end
|
|
233
|
+
@options.merge(categories: categories, exclude_categories: exclude_categories).compact
|
|
234
|
+
end
|
|
119
235
|
end
|
|
120
236
|
|
|
121
237
|
# Hegel::Syntax::Methods#text. A Unicode string of [min_size, max_size]
|
|
122
|
-
# characters,
|
|
238
|
+
# characters, at most 100 by default. +alphabet+ is a String of the
|
|
239
|
+
# permitted characters, and cannot be combined with any of the character
|
|
240
|
+
# options.
|
|
123
241
|
class TextGenerator < Generator
|
|
124
|
-
def initialize(min_size:, max_size:,
|
|
242
|
+
def initialize(min_size:, max_size:, alphabet: nil, **character_options)
|
|
125
243
|
@min_size = min_size
|
|
126
244
|
@max_size = max_size
|
|
127
|
-
@
|
|
128
|
-
@
|
|
129
|
-
@max_codepoint = max_codepoint
|
|
245
|
+
@alphabet = alphabet
|
|
246
|
+
@characters = CharactersGenerator.new(**character_options)
|
|
130
247
|
end
|
|
131
248
|
|
|
132
249
|
def do_draw(tc)
|
|
133
|
-
|
|
250
|
+
alphabet = alphabet_generator
|
|
251
|
+
max_size = @max_size || Generators.default_max_size(@min_size)
|
|
134
252
|
raise Hegel::Error, "text: max_size < min_size" if max_size < @min_size
|
|
135
253
|
|
|
136
254
|
# A fresh generator handle every draw, freed before this method
|
|
@@ -144,20 +262,33 @@ module Hegel
|
|
|
144
262
|
# free at a fixed, known scope exit; Ruby has no equivalent
|
|
145
263
|
# lifetime to hang a cache on, so this pays the alphabet-building
|
|
146
264
|
# cost every draw instead.
|
|
147
|
-
tc.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
265
|
+
alphabet.with_alphabet(tc, "text", @min_size, max_size) { |generator| tc.generate_string(generator) }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
private
|
|
269
|
+
|
|
270
|
+
def alphabet_generator
|
|
271
|
+
return @characters if @alphabet.nil?
|
|
272
|
+
|
|
273
|
+
raise Hegel::Error, "text: alphabet must be a String, not a #{@alphabet.class}" unless @alphabet.is_a?(String)
|
|
274
|
+
|
|
275
|
+
raise Hegel::Error, "text: Cannot combine .alphabet() with character methods." if @characters.options_given?
|
|
276
|
+
|
|
277
|
+
CharactersGenerator.new(categories: [], include_characters: @alphabet)
|
|
151
278
|
end
|
|
152
279
|
end
|
|
153
280
|
|
|
154
281
|
# Hegel::Syntax::Methods#arrays. An Array of values from +elements+,
|
|
155
|
-
# with [min_size, max_size] entries, unbounded above by default.
|
|
282
|
+
# with [min_size, max_size] entries, unbounded above by default. With
|
|
283
|
+
# +unique+, an element equal to one already drawn is rejected through
|
|
284
|
+
# #collection_reject, as SetGenerator does, so min_size still counts
|
|
285
|
+
# distinct elements.
|
|
156
286
|
class ArrayGenerator < Generator
|
|
157
|
-
def initialize(elements, min_size:, max_size:)
|
|
287
|
+
def initialize(elements, min_size:, max_size:, unique: false)
|
|
158
288
|
@elements = elements
|
|
159
289
|
@min_size = min_size
|
|
160
290
|
@max_size = max_size
|
|
291
|
+
@unique = unique
|
|
161
292
|
@label = combined_label(@elements)
|
|
162
293
|
end
|
|
163
294
|
|
|
@@ -189,7 +320,14 @@ module Hegel
|
|
|
189
320
|
collection = tc.new_collection(@min_size, max_size)
|
|
190
321
|
begin
|
|
191
322
|
result = []
|
|
192
|
-
|
|
323
|
+
while tc.collection_more(collection)
|
|
324
|
+
value = draw_element(tc)
|
|
325
|
+
if @unique && result.include?(value)
|
|
326
|
+
tc.collection_reject(collection, why: "duplicate element")
|
|
327
|
+
else
|
|
328
|
+
result << value
|
|
329
|
+
end
|
|
330
|
+
end
|
|
193
331
|
result
|
|
194
332
|
ensure
|
|
195
333
|
tc.collection_free(collection)
|
|
@@ -429,28 +567,150 @@ module Hegel
|
|
|
429
567
|
end
|
|
430
568
|
end
|
|
431
569
|
|
|
432
|
-
#
|
|
433
|
-
#
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
def
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
570
|
+
# What permutations, subsequences, and samples share: a draw of distinct
|
|
571
|
+
# indices into the caller's sequence, and the span around the whole draw.
|
|
572
|
+
module SequenceDrawing
|
|
573
|
+
private
|
|
574
|
+
|
|
575
|
+
# Raises for a size bound no draw can honour. The max_size check
|
|
576
|
+
# reads as hegel-rust's does; the negative min_size check has no
|
|
577
|
+
# hegel-rust counterpart, since a usize cannot be negative.
|
|
578
|
+
def check_sizes(name, min_size, max_size)
|
|
579
|
+
raise Hegel::Error, "#{name}: min_size must not be negative" if min_size.negative?
|
|
580
|
+
raise Hegel::Error, "#{name}: Cannot have max_size < min_size" if max_size && max_size < min_size
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def with_span(tc)
|
|
584
|
+
tc.start_span(label)
|
|
585
|
+
begin
|
|
586
|
+
yield
|
|
587
|
+
ensure
|
|
588
|
+
tc.stop_span(discard: false)
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# Between +min_size+ and +max_size+ (nil, or anything above +size+,
|
|
593
|
+
# means +size+) distinct indices below +size+, in draw order. Each draw is the index into what is still unchosen, not
|
|
594
|
+
# into the whole sequence, so the shrinker's smallest value is the
|
|
595
|
+
# earliest index not yet taken: the minimal sample is 0...min_size in
|
|
596
|
+
# increasing order.
|
|
597
|
+
def draw_index_sample(tc, size, min_size, max_size)
|
|
598
|
+
remaining = (0...size).to_a
|
|
599
|
+
chosen = []
|
|
600
|
+
collection = tc.new_collection(min_size, [max_size || size, size].min)
|
|
601
|
+
begin
|
|
602
|
+
chosen << remaining.delete_at(tc.generate_integer(0, remaining.size - 1)) while !remaining.empty? && tc.collection_more(collection)
|
|
603
|
+
chosen
|
|
604
|
+
ensure
|
|
605
|
+
tc.collection_free(collection)
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# Hegel::Syntax::Methods#permutations. Every element of +elements+, in an
|
|
611
|
+
# order the engine picks and shrinks towards the original one.
|
|
612
|
+
class PermutationsGenerator < Generator
|
|
613
|
+
include SequenceDrawing
|
|
614
|
+
|
|
615
|
+
def initialize(elements)
|
|
616
|
+
@elements = elements
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def do_draw(tc)
|
|
620
|
+
elements = @elements.to_a
|
|
621
|
+
with_span(tc) do
|
|
622
|
+
draw_index_sample(tc, elements.size, elements.size, elements.size).map { |index| elements[index] }
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# Hegel::Syntax::Methods#subsequences. A subset of +elements+ with
|
|
628
|
+
# [min_size, max_size] entries, in their original order. Duplicate
|
|
629
|
+
# elements count as distinct.
|
|
630
|
+
class SubsequencesGenerator < Generator
|
|
631
|
+
include SequenceDrawing
|
|
632
|
+
|
|
633
|
+
def initialize(elements, min_size:, max_size:)
|
|
634
|
+
@elements = elements
|
|
635
|
+
@min_size = min_size
|
|
636
|
+
@max_size = max_size
|
|
445
637
|
end
|
|
446
638
|
|
|
447
639
|
def do_draw(tc)
|
|
448
|
-
@
|
|
640
|
+
elements = @elements.to_a
|
|
641
|
+
check_sizes("subsequences", @min_size, @max_size)
|
|
642
|
+
if @min_size > elements.size
|
|
643
|
+
raise Hegel::Error,
|
|
644
|
+
"subsequences: Cannot generate a subsequence: min_size #{@min_size} is larger than " \
|
|
645
|
+
"the #{elements.size} elements in the sequence"
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
with_span(tc) do
|
|
649
|
+
indices = draw_index_sample(tc, elements.size, @min_size, @max_size)
|
|
650
|
+
indices.sort.map { |index| elements[index] }
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# Hegel::Syntax::Methods#samples. Between min_size and max_size elements
|
|
656
|
+
# of +elements+, drawn with or without replacement. Without replacement
|
|
657
|
+
# each element is used at most once, and duplicates in +elements+ count
|
|
658
|
+
# as distinct. With replacement the size is unbounded unless max_size
|
|
659
|
+
# bounds it.
|
|
660
|
+
class SamplesGenerator < Generator
|
|
661
|
+
include SequenceDrawing
|
|
662
|
+
|
|
663
|
+
def initialize(elements, min_size:, max_size:, replacement:)
|
|
664
|
+
@elements = elements
|
|
665
|
+
@min_size = min_size
|
|
666
|
+
@max_size = max_size
|
|
667
|
+
@replacement = replacement
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def do_draw(tc)
|
|
671
|
+
elements = @elements.to_a
|
|
672
|
+
check_sizes("samples", @min_size, @max_size)
|
|
673
|
+
validate_for(elements.size)
|
|
674
|
+
|
|
675
|
+
with_span(tc) do
|
|
676
|
+
if @replacement
|
|
677
|
+
draw_with_replacement(tc, elements)
|
|
678
|
+
else
|
|
679
|
+
indices = draw_index_sample(tc, elements.size, @min_size, @max_size)
|
|
680
|
+
indices.map { |index| elements[index] }
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
private
|
|
686
|
+
|
|
687
|
+
def validate_for(size)
|
|
688
|
+
if @replacement
|
|
689
|
+
if size.zero? && @min_size.positive?
|
|
690
|
+
raise Hegel::Error, "samples: Cannot generate a non-empty sample from an empty sequence"
|
|
691
|
+
end
|
|
692
|
+
elsif @min_size > size
|
|
693
|
+
raise Hegel::Error,
|
|
694
|
+
"samples: Cannot generate a sample without replacement: min_size #{@min_size} is larger than " \
|
|
695
|
+
"the #{size} elements in the sequence"
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def draw_with_replacement(tc, elements)
|
|
700
|
+
max_size = elements.empty? ? 0 : @max_size || LibHegel::HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED
|
|
701
|
+
collection = tc.new_collection(@min_size, max_size)
|
|
702
|
+
begin
|
|
703
|
+
result = []
|
|
704
|
+
result << elements[tc.generate_integer(0, elements.size - 1)] while tc.collection_more(collection)
|
|
705
|
+
result
|
|
706
|
+
ensure
|
|
707
|
+
tc.collection_free(collection)
|
|
708
|
+
end
|
|
449
709
|
end
|
|
450
710
|
end
|
|
451
711
|
|
|
452
712
|
# Hegel::Syntax::Methods#binary. A byte String of [min_size, max_size]
|
|
453
|
-
# bytes,
|
|
713
|
+
# bytes, at most 100 by default.
|
|
454
714
|
class BinaryGenerator < Generator
|
|
455
715
|
def initialize(min_size:, max_size:)
|
|
456
716
|
@min_size = min_size
|
|
@@ -458,7 +718,7 @@ module Hegel
|
|
|
458
718
|
end
|
|
459
719
|
|
|
460
720
|
def do_draw(tc)
|
|
461
|
-
max_size = @max_size ||
|
|
721
|
+
max_size = @max_size || Generators.default_max_size(@min_size)
|
|
462
722
|
raise Hegel::Error, "binary: max_size < min_size" if max_size < @min_size
|
|
463
723
|
|
|
464
724
|
# hegel_generate_bytes already returns an Encoding::BINARY String
|
|
@@ -473,8 +733,8 @@ module Hegel
|
|
|
473
733
|
# the header documents as Python `re` syntax -- a different grammar
|
|
474
734
|
# from Ruby's own Regexp, even though the two agree on simple patterns
|
|
475
735
|
# (character classes, quantifiers, alternation). fullmatch defaults to
|
|
476
|
-
#
|
|
477
|
-
#
|
|
736
|
+
# true, as hegel-rust's from_regex does: the whole string matches,
|
|
737
|
+
# rather than merely containing a match.
|
|
478
738
|
#
|
|
479
739
|
# +pattern+ must be a String, checked at draw time like every other
|
|
480
740
|
# validation in this file. A Ruby Regexp is rejected rather than
|
|
@@ -489,25 +749,245 @@ module Hegel
|
|
|
489
749
|
# confirmed a pattern needs no flags and uses only syntax the two
|
|
490
750
|
# grammars share can still pass `my_regexp.source` explicitly.
|
|
491
751
|
#
|
|
492
|
-
# alphabet is
|
|
493
|
-
# text
|
|
494
|
-
#
|
|
495
|
-
#
|
|
496
|
-
# how a Ruby caller writes an alphabet that constrains a Python regex,
|
|
497
|
-
# and that question is worth answering on its own rather than in
|
|
498
|
-
# passing here.
|
|
752
|
+
# +alphabet+ is a CharactersGenerator that constrains every character of
|
|
753
|
+
# the drawn string, padding included. Its text handle is sized 0..0, so an
|
|
754
|
+
# alphabet with no characters still builds: the engine then only rejects
|
|
755
|
+
# a pattern that needs one.
|
|
499
756
|
class FromRegexGenerator < Generator
|
|
500
|
-
def initialize(pattern, fullmatch:)
|
|
757
|
+
def initialize(pattern, fullmatch:, alphabet: nil)
|
|
501
758
|
@pattern = pattern
|
|
502
759
|
@fullmatch = fullmatch
|
|
760
|
+
@alphabet = alphabet
|
|
503
761
|
end
|
|
504
762
|
|
|
505
763
|
def do_draw(tc)
|
|
506
764
|
unless @pattern.is_a?(String)
|
|
507
765
|
raise Hegel::Error, "from_regex: pattern must be a String in Python re syntax, not a #{@pattern.class}"
|
|
508
766
|
end
|
|
767
|
+
unless @alphabet.nil? || @alphabet.is_a?(CharactersGenerator)
|
|
768
|
+
raise Hegel::Error, "from_regex: alphabet must be a characters generator, not a #{@alphabet.class}"
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
return draw_regex(tc, nil) if @alphabet.nil?
|
|
772
|
+
|
|
773
|
+
# Built fresh each draw, like TextGenerator's handle, and freed after
|
|
774
|
+
# the regex handle that was built from it.
|
|
775
|
+
@alphabet.with_alphabet(tc, "from_regex", 0, 0) { |alphabet| draw_regex(tc, alphabet) }
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
private
|
|
779
|
+
|
|
780
|
+
def draw_regex(tc, alphabet)
|
|
781
|
+
tc.with_regex_generator(@pattern, fullmatch: @fullmatch, alphabet: alphabet) do |generator|
|
|
782
|
+
tc.generate_string(generator)
|
|
783
|
+
end
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
# Hegel::Syntax::Methods#from_lark. A String that a grammar in Lark's
|
|
788
|
+
# syntax accepts, ported from hegel-rust's LarkGenerator.
|
|
789
|
+
#
|
|
790
|
+
# The compiled grammar lives in the run's Hegel::GrammarCache, under a key
|
|
791
|
+
# holding everything the compilation reads, so a generator built inside
|
|
792
|
+
# the test body compiles once per run as well (docs/adr/0033). The key is
|
|
793
|
+
# built here because a draw writes nothing to the generator (ADR 0020).
|
|
794
|
+
class FromLarkGenerator < Generator
|
|
795
|
+
DEFAULT_MAX_SIZE = 8192
|
|
796
|
+
ATTEMPTS = 3
|
|
797
|
+
ATTEMPT_LABEL = LibHegel.label_from_name("hegel-ruby.Hegel::Generators::FromLarkGenerator.attempt")
|
|
798
|
+
|
|
799
|
+
# The widest values hegel_grammar_new takes, so a larger one reaches
|
|
800
|
+
# the engine as a value it reports on rather than failing to marshal.
|
|
801
|
+
MAX_SIZE_LIMIT = (2**64) - 1
|
|
802
|
+
MAX_DEPTH_LIMIT = (2**32) - 1
|
|
803
|
+
|
|
804
|
+
def initialize(grammar, start:, explicit:, alphabet:, max_size:, max_depth:, import_path:)
|
|
805
|
+
@grammar = frozen(grammar)
|
|
806
|
+
@start = frozen(start)
|
|
807
|
+
@explicit = explicit
|
|
808
|
+
@alphabet = alphabet
|
|
809
|
+
@max_size = max_size
|
|
810
|
+
@max_depth = max_depth
|
|
811
|
+
@import_path = frozen(import_path.is_a?(Pathname) ? import_path.to_s : import_path)
|
|
812
|
+
names = explicit.keys.map { |name| frozen(name.is_a?(Symbol) ? name.to_s : name) } if explicit.is_a?(Hash)
|
|
813
|
+
@label = lark_label(names)
|
|
814
|
+
@key = [
|
|
815
|
+
@grammar, @start, max_size, max_depth, alphabet.is_a?(CharactersGenerator) ? alphabet.options : alphabet,
|
|
816
|
+
names, @import_path
|
|
817
|
+
].freeze
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
attr_reader :label
|
|
821
|
+
|
|
822
|
+
def do_draw(tc)
|
|
823
|
+
terminals = explicit_terminals
|
|
824
|
+
names = terminals.map(&:first)
|
|
825
|
+
tc.with_grammar(@key, -> { compile(tc, names) }) do |compiled|
|
|
826
|
+
next tc.generate_grammar(compiled.grammar, 0) if terminals.empty?
|
|
827
|
+
|
|
828
|
+
tc.start_span(label)
|
|
829
|
+
begin
|
|
830
|
+
draw_with_values(tc, compiled, terminals)
|
|
831
|
+
ensure
|
|
832
|
+
tc.stop_span(discard: false)
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
private
|
|
838
|
+
|
|
839
|
+
def frozen(value)
|
|
840
|
+
value.is_a?(String) ? -value : value
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
# hegel-rust's ingredients: the grammar, the start rule, and each
|
|
844
|
+
# explicit terminal's name and generator.
|
|
845
|
+
def lark_label(names)
|
|
846
|
+
labels = [self.class.label, text_label(@grammar), text_label(@start || "start")]
|
|
847
|
+
names&.zip(@explicit.values)&.each { |name, generator| labels << text_label(name) << Generator.label_of(generator) }
|
|
848
|
+
LibHegel.label_combine(labels)
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
def text_label(value)
|
|
852
|
+
value.is_a?(String) ? LibHegel.label_from_name(value) : 0
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
# The explicit terminals as [name, generator] pairs, once every option
|
|
856
|
+
# has been checked.
|
|
857
|
+
def explicit_terminals
|
|
858
|
+
raise Hegel::Error, "from_lark: grammar must be a String, not a #{@grammar.class}" unless @grammar.is_a?(String)
|
|
859
|
+
unless @start.nil? || @start.is_a?(String)
|
|
860
|
+
raise Hegel::Error, "from_lark: start must be a String, not a #{@start.class}"
|
|
861
|
+
end
|
|
862
|
+
raise Hegel::Error, "from_lark: max_size must be at least 1" if @max_size && @max_size < 1
|
|
863
|
+
raise Hegel::Error, "from_lark: max_depth must be at least 1" if @max_depth && @max_depth < 1
|
|
864
|
+
raise Hegel::Error, "from_lark: explicit must be a Hash, not a #{@explicit.class}" unless @explicit.is_a?(Hash)
|
|
865
|
+
|
|
866
|
+
terminals = @explicit.map { |name, generator| explicit_terminal(name, generator) }
|
|
867
|
+
names = terminals.map(&:first)
|
|
868
|
+
duplicate = names.find { |name| names.count(name) > 1 }
|
|
869
|
+
raise Hegel::Error, "from_lark: explicit terminal '#{duplicate}' is given more than one generator" if duplicate
|
|
870
|
+
unless @alphabet.nil? || @alphabet.is_a?(CharactersGenerator)
|
|
871
|
+
raise Hegel::Error, "from_lark: alphabet must be a characters generator, not a #{@alphabet.class}"
|
|
872
|
+
end
|
|
873
|
+
unless @import_path.nil? || @import_path.is_a?(String)
|
|
874
|
+
raise Hegel::Error, "from_lark: import_path must be a String or a Pathname, not a #{@import_path.class}"
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
terminals
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
def explicit_terminal(name, generator)
|
|
881
|
+
unless name.is_a?(String) || name.is_a?(Symbol)
|
|
882
|
+
raise Hegel::Error, "from_lark: an explicit terminal name must be a String or a Symbol, not a #{name.class}"
|
|
883
|
+
end
|
|
884
|
+
unless generator.is_a?(Generator)
|
|
885
|
+
raise Hegel::Error, "from_lark: explicit terminal '#{name}' must be given a generator, not a #{generator.class}"
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
[name.to_s, generator]
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
def compile(tc, names)
|
|
892
|
+
return compile_with(tc, names, nil) if @alphabet.nil?
|
|
893
|
+
|
|
894
|
+
# Sized 0..0, as hegel-rust builds a grammar's alphabet, and freed
|
|
895
|
+
# once the grammar is built from it.
|
|
896
|
+
@alphabet.with_alphabet(tc, "from_lark", 0, 0) { |alphabet| compile_with(tc, names, alphabet) }
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
def compile_with(tc, names, alphabet)
|
|
900
|
+
builder = tc.grammar_builder_new(@grammar)
|
|
901
|
+
begin
|
|
902
|
+
provide_files(tc, builder)
|
|
903
|
+
grammar = tc.grammar_new(
|
|
904
|
+
builder, @start, [@max_size || DEFAULT_MAX_SIZE, MAX_SIZE_LIMIT].min, [@max_depth || 0, MAX_DEPTH_LIMIT].min,
|
|
905
|
+
alphabet, names
|
|
906
|
+
)
|
|
907
|
+
ensure
|
|
908
|
+
tc.grammar_builder_free(builder)
|
|
909
|
+
end
|
|
910
|
+
begin
|
|
911
|
+
GrammarCache::Compiled.new(grammar: grammar, markers: names.map { |name| tc.grammar_placeholder(grammar, name) })
|
|
912
|
+
rescue
|
|
913
|
+
tc.grammar_free(grammar)
|
|
914
|
+
raise
|
|
915
|
+
end
|
|
916
|
+
end
|
|
509
917
|
|
|
510
|
-
|
|
918
|
+
def provide_files(tc, builder)
|
|
919
|
+
index = 0
|
|
920
|
+
while (path = tc.grammar_builder_file(builder, index))
|
|
921
|
+
tc.grammar_builder_provide(builder, index, @import_path && read_import(File.join(@import_path, path)))
|
|
922
|
+
index += 1
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
# The text of +file+, or nil when there is no such file, which lets a
|
|
927
|
+
# bundled grammar stand in for it. hegel-rust reads it the same way.
|
|
928
|
+
def read_import(file)
|
|
929
|
+
text = File.binread(file).force_encoding(Encoding::UTF_8)
|
|
930
|
+
return text if text.valid_encoding?
|
|
931
|
+
|
|
932
|
+
raise Hegel::Error, "from_lark: Could not read grammar `#{file}`: stream did not contain valid UTF-8"
|
|
933
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
934
|
+
nil
|
|
935
|
+
rescue SystemCallError => e
|
|
936
|
+
raise Hegel::Error, "from_lark: Could not read grammar `#{file}`: #{e.message}"
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
# Draws the grammar's string under a shrinking budget until the string
|
|
940
|
+
# with its values spliced in fits max_size, or rejects the case, as
|
|
941
|
+
# hegel-rust does.
|
|
942
|
+
def draw_with_values(tc, compiled, terminals)
|
|
943
|
+
max_size = @max_size || DEFAULT_MAX_SIZE
|
|
944
|
+
pattern = Regexp.union(compiled.markers)
|
|
945
|
+
budget = max_size
|
|
946
|
+
ATTEMPTS.times do
|
|
947
|
+
oversize = false
|
|
948
|
+
tc.start_span(ATTEMPT_LABEL)
|
|
949
|
+
begin
|
|
950
|
+
out, own, markers, size = splice(tc, compiled, pattern, terminals, budget)
|
|
951
|
+
oversize = size > max_size
|
|
952
|
+
ensure
|
|
953
|
+
tc.stop_span(discard: oversize)
|
|
954
|
+
end
|
|
955
|
+
return out unless oversize
|
|
956
|
+
|
|
957
|
+
budget = [max_size * (own + markers) / size, budget - 1].min.clamp(1..)
|
|
958
|
+
end
|
|
959
|
+
tc.reject
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
# The grammar's string with a drawn value in place of each marker, how
|
|
963
|
+
# many of its characters the grammar drew, how many markers it held,
|
|
964
|
+
# and how many characters it has. One leftmost pass that does not
|
|
965
|
+
# rescan a spliced value, as in hegel-rust.
|
|
966
|
+
def splice(tc, compiled, pattern, terminals, budget)
|
|
967
|
+
drawn = tc.generate_grammar(compiled.grammar, budget)
|
|
968
|
+
markers = 0
|
|
969
|
+
marker_chars = 0
|
|
970
|
+
value_chars = 0
|
|
971
|
+
out = drawn.gsub(pattern) do |marker|
|
|
972
|
+
name, generator = terminals[compiled.markers.index(marker)]
|
|
973
|
+
value = draw_value(tc, name, generator)
|
|
974
|
+
markers += 1
|
|
975
|
+
marker_chars += marker.length
|
|
976
|
+
value_chars += value.length
|
|
977
|
+
value
|
|
978
|
+
end
|
|
979
|
+
own = drawn.length - marker_chars
|
|
980
|
+
[out, own, markers, own + value_chars]
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
def draw_value(tc, name, generator)
|
|
984
|
+
tc.start_span(generator.label)
|
|
985
|
+
value = generator.do_draw(tc)
|
|
986
|
+
return value if value.is_a?(String)
|
|
987
|
+
|
|
988
|
+
raise Hegel::Error, "from_lark: explicit terminal '#{name}' drew a #{value.class}, not a String"
|
|
989
|
+
ensure
|
|
990
|
+
tc.stop_span(discard: false)
|
|
511
991
|
end
|
|
512
992
|
end
|
|
513
993
|
|
|
@@ -768,6 +1248,83 @@ module Hegel
|
|
|
768
1248
|
end
|
|
769
1249
|
end
|
|
770
1250
|
|
|
1251
|
+
# Formats a drawn [year, month, day] as hegel-rust's format_date does,
|
|
1252
|
+
# "YYYY-MM-DD" with the year zero-padded to four digits.
|
|
1253
|
+
def self.format_date(date)
|
|
1254
|
+
format("%04d-%02d-%02d", *date)
|
|
1255
|
+
end
|
|
1256
|
+
|
|
1257
|
+
# Formats a drawn [hour, minute, second, nanosecond] as hegel-rust's
|
|
1258
|
+
# format_time does: nine fractional digits, present only when the
|
|
1259
|
+
# nanosecond is not zero.
|
|
1260
|
+
def self.format_time(time)
|
|
1261
|
+
hour, minute, second, nanosecond = time
|
|
1262
|
+
base = format("%02d:%02d:%02d", hour, minute, second)
|
|
1263
|
+
nanosecond.zero? ? base : format("%s.%09d", base, nanosecond)
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
# Hegel::Syntax::Methods#date_strings. A "YYYY-MM-DD" String over the
|
|
1267
|
+
# years 1 through 9999, the range hegel-rust passes. Takes no options,
|
|
1268
|
+
# as in hegel-rust.
|
|
1269
|
+
#
|
|
1270
|
+
# Opens no span, for the reason UuidsGenerator gives: one native call.
|
|
1271
|
+
class DateStringsGenerator < Generator
|
|
1272
|
+
def do_draw(tc)
|
|
1273
|
+
Generators.format_date(tc.generate_date([1, 1, 1], [9999, 12, 31]))
|
|
1274
|
+
end
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
# Hegel::Syntax::Methods#time_strings. An "HH:MM:SS" String over the
|
|
1278
|
+
# whole day, with ".fffffffff" appended when the drawn nanosecond is not
|
|
1279
|
+
# zero. Unlike #times, it keeps the engine's nanoseconds, as hegel-rust
|
|
1280
|
+
# does (see docs/adr/0022).
|
|
1281
|
+
#
|
|
1282
|
+
# Opens no span, for the reason UuidsGenerator gives: one native call.
|
|
1283
|
+
class TimeStringsGenerator < Generator
|
|
1284
|
+
def do_draw(tc)
|
|
1285
|
+
Generators.format_time(tc.generate_time([0, 0, 0, 0], [23, 59, 59, 999_999_999]))
|
|
1286
|
+
end
|
|
1287
|
+
end
|
|
1288
|
+
|
|
1289
|
+
# Hegel::Syntax::Methods#datetime_strings. A "YYYY-MM-DDTHH:MM:SS" String
|
|
1290
|
+
# over years 1 through 9999, formatted as #date_strings and #time_strings
|
|
1291
|
+
# are.
|
|
1292
|
+
#
|
|
1293
|
+
# Opens no span, for the reason UuidsGenerator gives: one native call.
|
|
1294
|
+
class DatetimeStringsGenerator < Generator
|
|
1295
|
+
def do_draw(tc)
|
|
1296
|
+
date, time = tc.generate_datetime([1, 1, 1], [0, 0, 0, 0], [9999, 12, 31], [23, 59, 59, 999_999_999])
|
|
1297
|
+
"#{Generators.format_date(date)}T#{Generators.format_time(time)}"
|
|
1298
|
+
end
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
# Hegel::Syntax::Methods#durations. A span of time as whole nanoseconds
|
|
1302
|
+
# in [min_value, max_value], at most u64::MAX (about 584 years) as in
|
|
1303
|
+
# hegel-rust, whose generator is the model for the two messages below
|
|
1304
|
+
# (docs/adr/0030).
|
|
1305
|
+
class DurationsGenerator < Generator
|
|
1306
|
+
MAX_NANOSECONDS = (2**64) - 1
|
|
1307
|
+
|
|
1308
|
+
def initialize(min_value:, max_value:)
|
|
1309
|
+
@min_value = min_value
|
|
1310
|
+
@max_value = max_value
|
|
1311
|
+
end
|
|
1312
|
+
|
|
1313
|
+
def do_draw(tc)
|
|
1314
|
+
raise Hegel::Error, "durations: min_value must not be negative" if @min_value.negative?
|
|
1315
|
+
if @min_value > MAX_NANOSECONDS
|
|
1316
|
+
raise Hegel::Error, "durations: min_value exceeds the largest generatable Duration " \
|
|
1317
|
+
"(u64::MAX nanoseconds, about 584 years)"
|
|
1318
|
+
end
|
|
1319
|
+
|
|
1320
|
+
# Saturates rather than raises, as hegel-rust's max_value does.
|
|
1321
|
+
max_value = [@max_value, MAX_NANOSECONDS].min
|
|
1322
|
+
raise Hegel::Error, "durations: Cannot have max_value < min_value" if max_value < @min_value
|
|
1323
|
+
|
|
1324
|
+
IntegerGenerator.new(min_value: @min_value, max_value: max_value).do_draw(tc)
|
|
1325
|
+
end
|
|
1326
|
+
end
|
|
1327
|
+
|
|
771
1328
|
# Hegel::Syntax::Methods#composite. Builds a generator from imperative
|
|
772
1329
|
# code, the way Hypothesis's @composite decorator and hegel-rust's
|
|
773
1330
|
# compose!/hegel-go's Composite let a caller assemble one value out of
|
|
@@ -893,5 +1450,118 @@ module Hegel
|
|
|
893
1450
|
@inner.do_draw(tc)
|
|
894
1451
|
end
|
|
895
1452
|
end
|
|
1453
|
+
|
|
1454
|
+
# Hegel::Syntax::Methods#recursive. A value built from +leaf+ values by
|
|
1455
|
+
# +branch+, which receives a SubtreeGenerator for the sub-values and
|
|
1456
|
+
# returns a generator combining them. The engine decides every
|
|
1457
|
+
# leaf-or-branch choice and owns the depth and leaf budgets, as
|
|
1458
|
+
# hegel-rust's generators/recursive.rs leaves them to it.
|
|
1459
|
+
class RecursiveGenerator < Generator
|
|
1460
|
+
UINT64_MAX = (2**64) - 1
|
|
1461
|
+
|
|
1462
|
+
def initialize(leaf, max_depth:, max_leaves:, &branch)
|
|
1463
|
+
@leaf = leaf
|
|
1464
|
+
@max_depth = max_depth
|
|
1465
|
+
@max_leaves = max_leaves
|
|
1466
|
+
@branch = branch
|
|
1467
|
+
@label = combined_label(@leaf)
|
|
1468
|
+
end
|
|
1469
|
+
|
|
1470
|
+
attr_reader :label
|
|
1471
|
+
|
|
1472
|
+
def do_draw(tc)
|
|
1473
|
+
raise Hegel::Error, "recursive: block is required" unless @branch
|
|
1474
|
+
raise Hegel::Error, "recursive: leaf must be a generator, got #{@leaf.inspect}" unless @leaf.is_a?(Generator)
|
|
1475
|
+
|
|
1476
|
+
validate_bound("max_depth", @max_depth)
|
|
1477
|
+
validate_bound("max_leaves", @max_leaves)
|
|
1478
|
+
|
|
1479
|
+
tc.start_span(label)
|
|
1480
|
+
begin
|
|
1481
|
+
draw_recursion(tc)
|
|
1482
|
+
ensure
|
|
1483
|
+
tc.stop_span(discard: false)
|
|
1484
|
+
end
|
|
1485
|
+
end
|
|
1486
|
+
|
|
1487
|
+
private
|
|
1488
|
+
|
|
1489
|
+
def validate_bound(name, value)
|
|
1490
|
+
return if value.is_a?(Integer) && value.between?(0, UINT64_MAX)
|
|
1491
|
+
|
|
1492
|
+
raise Hegel::Error, "recursive: #{name} must be an Integer between 0 and 2**64 - 1, got #{value.inspect}"
|
|
1493
|
+
end
|
|
1494
|
+
|
|
1495
|
+
# An attempt abandoned over the leaf budget is retried even when a
|
|
1496
|
+
# body rescued the exception and returned a value instead: that value
|
|
1497
|
+
# was built from draws the engine is about to discard.
|
|
1498
|
+
def draw_recursion(tc)
|
|
1499
|
+
recursion = tc.new_recursion(@max_depth, @max_leaves)
|
|
1500
|
+
begin
|
|
1501
|
+
root = SubtreeGenerator.new(@leaf, @branch, recursion, 0, label)
|
|
1502
|
+
loop do
|
|
1503
|
+
begin
|
|
1504
|
+
value = root.draw_subtree(tc)
|
|
1505
|
+
rescue LeafBudgetExceeded => e
|
|
1506
|
+
raise unless e.recursion.equal?(recursion)
|
|
1507
|
+
end
|
|
1508
|
+
if tc.abandoned_by?(recursion)
|
|
1509
|
+
tc.recursion_retry(recursion)
|
|
1510
|
+
elsif tc.recursion_finish(recursion)
|
|
1511
|
+
return value
|
|
1512
|
+
end
|
|
1513
|
+
end
|
|
1514
|
+
ensure
|
|
1515
|
+
tc.recursion_free(recursion)
|
|
1516
|
+
end
|
|
1517
|
+
end
|
|
1518
|
+
end
|
|
1519
|
+
|
|
1520
|
+
# What a #recursive block receives: a generator of one sub-value, itself
|
|
1521
|
+
# a leaf or a further branch. Every depth shares the recursive
|
|
1522
|
+
# generator's label, which is what lets the shrinker replace a tree with
|
|
1523
|
+
# one of its own subtrees.
|
|
1524
|
+
class SubtreeGenerator < Generator
|
|
1525
|
+
def initialize(leaf, branch, recursion, depth, label)
|
|
1526
|
+
@leaf = leaf
|
|
1527
|
+
@branch = branch
|
|
1528
|
+
@recursion = recursion
|
|
1529
|
+
@depth = depth
|
|
1530
|
+
@label = label
|
|
1531
|
+
end
|
|
1532
|
+
|
|
1533
|
+
attr_reader :label
|
|
1534
|
+
|
|
1535
|
+
def do_draw(tc)
|
|
1536
|
+
if @recursion.freed
|
|
1537
|
+
raise Hegel::Error, "recursive: a subtree generator was drawn outside the draw that made it"
|
|
1538
|
+
end
|
|
1539
|
+
|
|
1540
|
+
tc.start_span(label)
|
|
1541
|
+
begin
|
|
1542
|
+
draw_subtree(tc)
|
|
1543
|
+
ensure
|
|
1544
|
+
tc.stop_span(discard: false)
|
|
1545
|
+
end
|
|
1546
|
+
end
|
|
1547
|
+
|
|
1548
|
+
# Called directly for the root, which needs no span of its own: the
|
|
1549
|
+
# recursive generator's span around it carries the same label. The
|
|
1550
|
+
# leaf and the branch generator get no span here either, unlike
|
|
1551
|
+
# hegel-rust's draw_silent; docs/adr/0032 has the measurement.
|
|
1552
|
+
def draw_subtree(tc)
|
|
1553
|
+
if tc.recursion_branch(@recursion, @depth)
|
|
1554
|
+
generator = @branch.call(SubtreeGenerator.new(@leaf, @branch, @recursion, @depth + 1, label))
|
|
1555
|
+
unless generator.is_a?(Generator)
|
|
1556
|
+
raise Hegel::Error, "recursive: the block must return a generator, got #{generator.inspect}"
|
|
1557
|
+
end
|
|
1558
|
+
|
|
1559
|
+
generator.do_draw(tc)
|
|
1560
|
+
else
|
|
1561
|
+
tc.recursion_leaf(@recursion)
|
|
1562
|
+
@leaf.do_draw(tc)
|
|
1563
|
+
end
|
|
1564
|
+
end
|
|
1565
|
+
end
|
|
896
1566
|
end
|
|
897
1567
|
end
|