hegeltest 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +264 -0
- data/Rakefile +19 -0
- data/docs/README.md +25 -0
- data/docs/adr/0001-bind-libhegel-through-fiddle.md +54 -0
- data/docs/adr/0002-ship-one-prebuilt-engine-per-platform-specific-gem.md +48 -0
- data/docs/adr/0003-publish-as-hegeltest-require-as-hegel.md +39 -0
- data/docs/adr/0004-expose-generators-through-a-mixin-with-keyword-options.md +42 -0
- data/docs/adr/0005-name-drawn-values-from-the-callers-source-with-prism.md +40 -0
- data/docs/adr/0006-verify-the-binding-in-seven-layers-with-full-coverage.md +51 -0
- data/docs/adr/0007-ship-a-thin-ruby-skill-shaped-for-donation.md +56 -0
- data/docs/adr/0008-revisit-the-binding-after-milestone-c-on-measurement.md +81 -0
- data/docs/adr/0009-turn-the-example-database-on-with-a-key.md +89 -0
- data/docs/adr/0010-declare-stateful-rules-with-a-class-macro.md +113 -0
- data/docs/adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md +83 -0
- data/docs/adr/0012-build-a-failure-origin-from-the-callers-own-frame.md +72 -0
- data/docs/adr/0013-bind-libhegel-through-the-ffi-gem.md +102 -0
- data/docs/architecture.md +182 -0
- data/lib/hegel/draw_name.rb +109 -0
- data/lib/hegel/errors.rb +47 -0
- data/lib/hegel/generator.rb +98 -0
- data/lib/hegel/generators.rb +865 -0
- data/lib/hegel/lib_hegel/real.rb +1149 -0
- data/lib/hegel/lib_hegel.rb +269 -0
- data/lib/hegel/libhegel_version.rb +9 -0
- data/lib/hegel/locate.rb +188 -0
- data/lib/hegel/report.rb +87 -0
- data/lib/hegel/runner.rb +464 -0
- data/lib/hegel/settings.rb +164 -0
- data/lib/hegel/state_machine.rb +89 -0
- data/lib/hegel/stateful/pool.rb +111 -0
- data/lib/hegel/stateful.rb +120 -0
- data/lib/hegel/syntax/methods.rb +173 -0
- data/lib/hegel/test_case.rb +523 -0
- data/lib/hegel/version.rb +5 -0
- data/lib/hegel.rb +92 -0
- data/lib/hegeltest.rb +7 -0
- data/lib/tasks/libhegel.rake +112 -0
- data/lib/tasks/platform_gems.rake +111 -0
- data/sig/hegel.rbs +563 -0
- data/skills/hegel-ruby/SKILL.md +30 -0
- data/skills/hegel-ruby/references/ruby/reference.md +1210 -0
- metadata +113 -0
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "ipaddr"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "generator"
|
|
7
|
+
require_relative "lib_hegel"
|
|
8
|
+
|
|
9
|
+
module Hegel
|
|
10
|
+
# The generators Hegel::Syntax::Methods exposes as bare, include-able
|
|
11
|
+
# methods (see docs/adr/0004): booleans, integers, floats, text, and
|
|
12
|
+
# arrays.
|
|
13
|
+
#
|
|
14
|
+
# Each class validates its own options at #do_draw time, not at
|
|
15
|
+
# construction: hegel-rust's own contributor documentation states that
|
|
16
|
+
# every invalid combination of builder values must be caught at draw
|
|
17
|
+
# time, and that the resulting message is public API, asserted against
|
|
18
|
+
# by tests as a stable substring. This binding follows the same rule so a
|
|
19
|
+
# generator built once and drawn from many times fails the same way
|
|
20
|
+
# hegel-rust's does.
|
|
21
|
+
module Generators
|
|
22
|
+
# Hegel::Syntax::Methods#booleans. A boolean, true with probability +p+.
|
|
23
|
+
class BooleanGenerator < Generator
|
|
24
|
+
def initialize(p:)
|
|
25
|
+
super()
|
|
26
|
+
@p = p
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def do_draw(tc)
|
|
30
|
+
raise Hegel::Error, "booleans: p must be between 0.0 and 1.0, got #{@p}" unless (0.0..1.0).cover?(@p)
|
|
31
|
+
|
|
32
|
+
tc.generate_boolean(@p)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Hegel::Syntax::Methods#integers. An integer in [min_value, max_value],
|
|
37
|
+
# defaulting to the full 64-bit range when either bound is omitted.
|
|
38
|
+
class IntegerGenerator < Generator
|
|
39
|
+
# hegel_generate_integer takes int64_t bounds; a bound outside this
|
|
40
|
+
# range dispatches to hegel_generate_integer_big instead (see
|
|
41
|
+
# #do_draw), so integers()'s own caller-facing surface does not
|
|
42
|
+
# change depending on which native call ends up making the draw --
|
|
43
|
+
# only the dispatch threshold these two constants mark.
|
|
44
|
+
INT64_MIN = -(2**63)
|
|
45
|
+
INT64_MAX = (2**63) - 1
|
|
46
|
+
|
|
47
|
+
def initialize(min_value:, max_value:)
|
|
48
|
+
super()
|
|
49
|
+
@min_value = min_value
|
|
50
|
+
@max_value = max_value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def do_draw(tc)
|
|
54
|
+
min_value = @min_value || INT64_MIN
|
|
55
|
+
max_value = @max_value || INT64_MAX
|
|
56
|
+
raise Hegel::Error, "integers: max_value < min_value" if max_value < min_value
|
|
57
|
+
|
|
58
|
+
if min_value.between?(INT64_MIN, INT64_MAX) && max_value.between?(INT64_MIN, INT64_MAX)
|
|
59
|
+
tc.generate_integer(min_value, max_value)
|
|
60
|
+
else
|
|
61
|
+
tc.generate_integer_big(min_value, max_value)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Hegel::Syntax::Methods#floats. A double in [min_value, max_value],
|
|
67
|
+
# unbounded (the full finite range) by default. allow_nan and
|
|
68
|
+
# allow_infinity both default to false here, unlike hegel-rust's
|
|
69
|
+
# floats() (true when neither bound is set): this milestone exposes a
|
|
70
|
+
# plain, always-off-by-default surface and leaves
|
|
71
|
+
# smallest_nonzero_magnitude, and the allow_nan/allow_infinity/bounds
|
|
72
|
+
# interaction hegel-rust validates, unexposed. A caller who never asks
|
|
73
|
+
# for NaN never has to reason about it, and a keyword can be added to
|
|
74
|
+
# this list later without breaking anyone, where a default flipped from
|
|
75
|
+
# true to false would.
|
|
76
|
+
class FloatGenerator < Generator
|
|
77
|
+
# hegel_generate_float's width; Ruby has one Float type, the 64-bit
|
|
78
|
+
# IEEE 754 double, so this is never anything else.
|
|
79
|
+
WIDTH = 64
|
|
80
|
+
|
|
81
|
+
def initialize(min_value:, max_value:, allow_nan:, allow_infinity:, exclude_min:, exclude_max:)
|
|
82
|
+
super()
|
|
83
|
+
@min_value = min_value
|
|
84
|
+
@max_value = max_value
|
|
85
|
+
@allow_nan = allow_nan
|
|
86
|
+
@allow_infinity = allow_infinity
|
|
87
|
+
@exclude_min = exclude_min
|
|
88
|
+
@exclude_max = exclude_max
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def do_draw(tc)
|
|
92
|
+
min_value = @min_value || -Float::INFINITY
|
|
93
|
+
max_value = @max_value || Float::INFINITY
|
|
94
|
+
raise Hegel::Error, "floats: max_value < min_value" if max_value < min_value
|
|
95
|
+
|
|
96
|
+
tc.generate_float(
|
|
97
|
+
WIDTH, min_value, max_value,
|
|
98
|
+
allow_nan: @allow_nan, allow_infinity: @allow_infinity,
|
|
99
|
+
exclude_min: @exclude_min, exclude_max: @exclude_max,
|
|
100
|
+
smallest_nonzero_magnitude: LibHegel::HEGEL_FLOAT64_SMALLEST_NONZERO_MAGNITUDE_UNRESTRICTED
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Hegel::Syntax::Methods#text. A Unicode string of [min_size, max_size]
|
|
106
|
+
# characters, unbounded above by default.
|
|
107
|
+
class TextGenerator < Generator
|
|
108
|
+
def initialize(min_size:, max_size:, codec:, min_codepoint:, max_codepoint:)
|
|
109
|
+
super()
|
|
110
|
+
@min_size = min_size
|
|
111
|
+
@max_size = max_size
|
|
112
|
+
@codec = codec
|
|
113
|
+
@min_codepoint = min_codepoint
|
|
114
|
+
@max_codepoint = max_codepoint
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def do_draw(tc)
|
|
118
|
+
max_size = @max_size || LibHegel::HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED
|
|
119
|
+
raise Hegel::Error, "text: max_size < min_size" if max_size < @min_size
|
|
120
|
+
|
|
121
|
+
# A fresh generator handle every draw, freed before this method
|
|
122
|
+
# returns via Hegel::TestCase#with_text_generator, rather than
|
|
123
|
+
# cached on this generator instance: hegel_string_generator_free
|
|
124
|
+
# takes the context, so the handle cannot outlive it, and this
|
|
125
|
+
# generator object can be drawn from again in a later run against a
|
|
126
|
+
# different context. Caching the handle here would free it under
|
|
127
|
+
# the wrong run's context the second time. hegel-rust's own text()
|
|
128
|
+
# can cache its handle in a OnceLock because Rust's Drop runs the
|
|
129
|
+
# free at a fixed, known scope exit; Ruby has no equivalent
|
|
130
|
+
# lifetime to hang a cache on, so this pays the alphabet-building
|
|
131
|
+
# cost every draw instead.
|
|
132
|
+
tc.with_text_generator(
|
|
133
|
+
min_size: @min_size, max_size: max_size, codec: @codec,
|
|
134
|
+
min_codepoint: @min_codepoint || 0, max_codepoint: @max_codepoint || 0xFFFFFFFF
|
|
135
|
+
) { |generator| tc.generate_string(generator) }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Hegel::Syntax::Methods#arrays. An Array of values from +elements+,
|
|
140
|
+
# with [min_size, max_size] entries, unbounded above by default.
|
|
141
|
+
class ArrayGenerator < Generator
|
|
142
|
+
def initialize(elements, min_size:, max_size:)
|
|
143
|
+
super()
|
|
144
|
+
@elements = elements
|
|
145
|
+
@min_size = min_size
|
|
146
|
+
@max_size = max_size
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def do_draw(tc)
|
|
150
|
+
raise Hegel::Error, "arrays: min_size must not be negative" if @min_size.negative?
|
|
151
|
+
|
|
152
|
+
max_size = @max_size || LibHegel::HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED
|
|
153
|
+
raise Hegel::Error, "arrays: max_size < min_size" if max_size < @min_size
|
|
154
|
+
|
|
155
|
+
# HEGEL_LABEL_LIST around the whole array, HEGEL_LABEL_LIST_ELEMENT
|
|
156
|
+
# around each element (#draw_element below): the reference binding
|
|
157
|
+
# (hegel-typescript's drawList) wraps both, and the shrinker needs
|
|
158
|
+
# both to shrink a compound draw correctly -- a missing or
|
|
159
|
+
# misplaced span here shows up as a larger-than-minimal
|
|
160
|
+
# counterexample, not a test failure (see docs/adr/0006).
|
|
161
|
+
tc.start_span(LibHegel::HEGEL_LABEL_LIST)
|
|
162
|
+
begin
|
|
163
|
+
draw_elements(tc, max_size)
|
|
164
|
+
ensure
|
|
165
|
+
tc.stop_span(discard: false)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def draw_elements(tc, max_size)
|
|
172
|
+
collection = tc.new_collection(@min_size, max_size)
|
|
173
|
+
begin
|
|
174
|
+
result = []
|
|
175
|
+
result << draw_element(tc) while tc.collection_more(collection)
|
|
176
|
+
result
|
|
177
|
+
ensure
|
|
178
|
+
tc.collection_free(collection)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def draw_element(tc)
|
|
183
|
+
tc.start_span(LibHegel::HEGEL_LABEL_LIST_ELEMENT)
|
|
184
|
+
@elements.do_draw(tc)
|
|
185
|
+
ensure
|
|
186
|
+
tc.stop_span(discard: false)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Hegel::Syntax::Methods#just. Always returns +value+, drawing nothing:
|
|
191
|
+
# there is no choice for libhegel to make, so this opens no span (a
|
|
192
|
+
# span exists to let the shrinker retry or isolate a draw, and there is
|
|
193
|
+
# nothing here to retry or isolate).
|
|
194
|
+
class JustGenerator < Generator
|
|
195
|
+
def initialize(value)
|
|
196
|
+
super()
|
|
197
|
+
@value = value
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def do_draw(_tc)
|
|
201
|
+
@value
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Hegel::Syntax::Methods#sampled_from. One element of +collection+,
|
|
206
|
+
# picked by drawing an index in [0, collection.size - 1].
|
|
207
|
+
class SampledFromGenerator < Generator
|
|
208
|
+
def initialize(collection)
|
|
209
|
+
super()
|
|
210
|
+
@collection = collection
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def do_draw(tc)
|
|
214
|
+
raise Hegel::Error, "sampled_from: collection must not be empty" if @collection.empty?
|
|
215
|
+
|
|
216
|
+
tc.start_span(LibHegel::HEGEL_LABEL_SAMPLED_FROM)
|
|
217
|
+
begin
|
|
218
|
+
index = tc.generate_integer(0, @collection.size - 1)
|
|
219
|
+
@collection.to_a[index]
|
|
220
|
+
ensure
|
|
221
|
+
tc.stop_span(discard: false)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Hegel::Syntax::Methods#one_of. Draws from one of +generators+, picked
|
|
227
|
+
# by drawing an index in [0, generators.size - 1].
|
|
228
|
+
class OneOfGenerator < Generator
|
|
229
|
+
def initialize(generators)
|
|
230
|
+
super()
|
|
231
|
+
@generators = generators
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def do_draw(tc)
|
|
235
|
+
raise Hegel::Error, "one_of: at least one generator is required" if @generators.empty?
|
|
236
|
+
|
|
237
|
+
tc.start_span(LibHegel::HEGEL_LABEL_ONE_OF)
|
|
238
|
+
begin
|
|
239
|
+
index = tc.generate_integer(0, @generators.size - 1)
|
|
240
|
+
@generators[index].do_draw(tc)
|
|
241
|
+
ensure
|
|
242
|
+
tc.stop_span(discard: false)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Hegel::Syntax::Methods#optional. Draws from +generator+ with
|
|
248
|
+
# probability 0.5, nil otherwise. Does not expose a p: keyword: this
|
|
249
|
+
# milestone leaves generate_boolean at its own default probability.
|
|
250
|
+
class OptionalGenerator < Generator
|
|
251
|
+
def initialize(generator)
|
|
252
|
+
super()
|
|
253
|
+
@generator = generator
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def do_draw(tc)
|
|
257
|
+
tc.start_span(LibHegel::HEGEL_LABEL_OPTIONAL)
|
|
258
|
+
begin
|
|
259
|
+
tc.generate_boolean ? @generator.do_draw(tc) : nil
|
|
260
|
+
ensure
|
|
261
|
+
tc.stop_span(discard: false)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Hegel::Syntax::Methods#tuples. Draws each of +generators+ in order
|
|
267
|
+
# into an Array (Ruby has no tuple type; see docs/adr/0004). The label
|
|
268
|
+
# table assigns tuples a single span around the whole draw, unlike
|
|
269
|
+
# arrays/sets/hashes: each position is a distinct generator already,
|
|
270
|
+
# not repeated draws of one, so there is no per-element span to open.
|
|
271
|
+
class TupleGenerator < Generator
|
|
272
|
+
def initialize(generators)
|
|
273
|
+
super()
|
|
274
|
+
@generators = generators
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def do_draw(tc)
|
|
278
|
+
tc.start_span(LibHegel::HEGEL_LABEL_TUPLE)
|
|
279
|
+
begin
|
|
280
|
+
@generators.map { |generator| generator.do_draw(tc) }
|
|
281
|
+
ensure
|
|
282
|
+
tc.stop_span(discard: false)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Hegel::Syntax::Methods#sets. A Set of values from +elements+, with
|
|
288
|
+
# [min_size, max_size] entries, unbounded above by default.
|
|
289
|
+
class SetGenerator < Generator
|
|
290
|
+
def initialize(elements, min_size:, max_size:)
|
|
291
|
+
super()
|
|
292
|
+
@elements = elements
|
|
293
|
+
@min_size = min_size
|
|
294
|
+
@max_size = max_size
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def do_draw(tc)
|
|
298
|
+
raise Hegel::Error, "sets: min_size must not be negative" if @min_size.negative?
|
|
299
|
+
|
|
300
|
+
max_size = @max_size || LibHegel::HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED
|
|
301
|
+
raise Hegel::Error, "sets: max_size < min_size" if max_size < @min_size
|
|
302
|
+
|
|
303
|
+
tc.start_span(LibHegel::HEGEL_LABEL_SET)
|
|
304
|
+
begin
|
|
305
|
+
draw_elements(tc, max_size)
|
|
306
|
+
ensure
|
|
307
|
+
tc.stop_span(discard: false)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
private
|
|
312
|
+
|
|
313
|
+
# Set, unlike Array, cannot hold a duplicate: every #collection_more
|
|
314
|
+
# loop iteration that draws a value already in +result+ calls
|
|
315
|
+
# #collection_reject instead of adding it, so libhegel offers the
|
|
316
|
+
# same slot another attempt rather than counting a discarded
|
|
317
|
+
# duplicate toward min_size (see Hegel::TestCase#collection_reject).
|
|
318
|
+
def draw_elements(tc, max_size)
|
|
319
|
+
collection = tc.new_collection(@min_size, max_size)
|
|
320
|
+
begin
|
|
321
|
+
result = Set.new
|
|
322
|
+
while tc.collection_more(collection)
|
|
323
|
+
value = draw_element(tc)
|
|
324
|
+
if result.include?(value)
|
|
325
|
+
tc.collection_reject(collection)
|
|
326
|
+
else
|
|
327
|
+
result << value
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
result
|
|
331
|
+
ensure
|
|
332
|
+
tc.collection_free(collection)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def draw_element(tc)
|
|
337
|
+
tc.start_span(LibHegel::HEGEL_LABEL_SET_ELEMENT)
|
|
338
|
+
@elements.do_draw(tc)
|
|
339
|
+
ensure
|
|
340
|
+
tc.stop_span(discard: false)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Hegel::Syntax::Methods#hashes. A Hash from +keys+ drawing each key and
|
|
345
|
+
# +values+ each value, with [min_size, max_size] entries, unbounded
|
|
346
|
+
# above by default. Uniqueness is judged on the key alone, matching
|
|
347
|
+
# Ruby's own Hash: a later draw with a key already present replaces
|
|
348
|
+
# nothing, so it is rejected the same way SetGenerator rejects a
|
|
349
|
+
# duplicate element.
|
|
350
|
+
class HashGenerator < Generator
|
|
351
|
+
def initialize(keys, values, min_size:, max_size:)
|
|
352
|
+
super()
|
|
353
|
+
@keys = keys
|
|
354
|
+
@values = values
|
|
355
|
+
@min_size = min_size
|
|
356
|
+
@max_size = max_size
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def do_draw(tc)
|
|
360
|
+
raise Hegel::Error, "hashes: min_size must not be negative" if @min_size.negative?
|
|
361
|
+
|
|
362
|
+
max_size = @max_size || LibHegel::HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED
|
|
363
|
+
raise Hegel::Error, "hashes: max_size < min_size" if max_size < @min_size
|
|
364
|
+
|
|
365
|
+
tc.start_span(LibHegel::HEGEL_LABEL_MAP)
|
|
366
|
+
begin
|
|
367
|
+
draw_entries(tc, max_size)
|
|
368
|
+
ensure
|
|
369
|
+
tc.stop_span(discard: false)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
private
|
|
374
|
+
|
|
375
|
+
def draw_entries(tc, max_size)
|
|
376
|
+
collection = tc.new_collection(@min_size, max_size)
|
|
377
|
+
begin
|
|
378
|
+
result = {}
|
|
379
|
+
while tc.collection_more(collection)
|
|
380
|
+
key, value = draw_entry(tc)
|
|
381
|
+
if result.key?(key)
|
|
382
|
+
tc.collection_reject(collection)
|
|
383
|
+
else
|
|
384
|
+
result[key] = value
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
result
|
|
388
|
+
ensure
|
|
389
|
+
tc.collection_free(collection)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Draws the key and its value as one unit inside a single
|
|
394
|
+
# HEGEL_LABEL_MAP_ENTRY span (the label table gives hashes one span
|
|
395
|
+
# per entry, not one per key and a second per value), so the
|
|
396
|
+
# shrinker can retry the whole pair together.
|
|
397
|
+
def draw_entry(tc)
|
|
398
|
+
tc.start_span(LibHegel::HEGEL_LABEL_MAP_ENTRY)
|
|
399
|
+
[@keys.do_draw(tc), @values.do_draw(tc)]
|
|
400
|
+
ensure
|
|
401
|
+
tc.stop_span(discard: false)
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Hegel::Syntax::Methods#characters. A String of exactly one character,
|
|
406
|
+
# sharing TextGenerator's own alphabet options (codec, min_codepoint,
|
|
407
|
+
# max_codepoint) by delegating to a TextGenerator built with min_size
|
|
408
|
+
# and max_size both fixed at 1 -- the same alphabet-building logic,
|
|
409
|
+
# just bounded to a single character instead of a run of them. Opens
|
|
410
|
+
# no span of its own for the same reason TextGenerator does not: the
|
|
411
|
+
# delegated #do_draw is the whole draw, not a composition of several.
|
|
412
|
+
class CharactersGenerator < Generator
|
|
413
|
+
def initialize(codec:, min_codepoint:, max_codepoint:)
|
|
414
|
+
super()
|
|
415
|
+
@text = TextGenerator.new(
|
|
416
|
+
min_size: 1, max_size: 1, codec: codec,
|
|
417
|
+
min_codepoint: min_codepoint, max_codepoint: max_codepoint
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def do_draw(tc)
|
|
422
|
+
@text.do_draw(tc)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Hegel::Syntax::Methods#binary. A byte String of [min_size, max_size]
|
|
427
|
+
# bytes, unbounded above by default.
|
|
428
|
+
class BinaryGenerator < Generator
|
|
429
|
+
def initialize(min_size:, max_size:)
|
|
430
|
+
super()
|
|
431
|
+
@min_size = min_size
|
|
432
|
+
@max_size = max_size
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def do_draw(tc)
|
|
436
|
+
max_size = @max_size || LibHegel::HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED
|
|
437
|
+
raise Hegel::Error, "binary: max_size < min_size" if max_size < @min_size
|
|
438
|
+
|
|
439
|
+
# hegel_generate_bytes already returns an Encoding::BINARY String
|
|
440
|
+
# (see LibHegel::Real#generate_bytes); force_encoding here would
|
|
441
|
+
# be redundant at best, and wrong the moment a caller's own bytes
|
|
442
|
+
# happened to be valid UTF-8 and got silently relabelled as text.
|
|
443
|
+
tc.generate_bytes(@min_size, max_size)
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Hegel::Syntax::Methods#from_regex. A String matching +pattern+, which
|
|
448
|
+
# the header documents as Python `re` syntax -- a different grammar
|
|
449
|
+
# from Ruby's own Regexp, even though the two agree on simple patterns
|
|
450
|
+
# (character classes, quantifiers, alternation). fullmatch defaults to
|
|
451
|
+
# false, matching hegel_string_generator_regex's own documented
|
|
452
|
+
# default: the drawn string only has to contain a match, not equal one.
|
|
453
|
+
#
|
|
454
|
+
# +pattern+ must be a String, checked at draw time like every other
|
|
455
|
+
# validation in this file. A Ruby Regexp is rejected rather than
|
|
456
|
+
# accepted and translated: Regexp#source drops modifiers such as /i,
|
|
457
|
+
# /m, and /x silently, and the two grammars give the same characters
|
|
458
|
+
# different meanings in places -- Ruby's ^ and $ are always line
|
|
459
|
+
# anchors, where Python re's are string anchors unless re.MULTILINE is
|
|
460
|
+
# set, and Ruby's [[:alpha:]] POSIX bracket syntax has no Python re
|
|
461
|
+
# counterpart at all. Accepting a Regexp here would build a generator
|
|
462
|
+
# whose output quietly stopped matching the flags or anchors its
|
|
463
|
+
# caller wrote, with nothing to signal the mismatch. A caller who has
|
|
464
|
+
# confirmed a pattern needs no flags and uses only syntax the two
|
|
465
|
+
# grammars share can still pass `my_regexp.source` explicitly.
|
|
466
|
+
#
|
|
467
|
+
# alphabet is not exposed here: the header's third argument accepts a
|
|
468
|
+
# text generator to constrain the wildcard/padding characters this
|
|
469
|
+
# regex can produce. nil, the header's documented "no particular
|
|
470
|
+
# alphabet" default, is passed in its place: exposing it means deciding
|
|
471
|
+
# how a Ruby caller writes an alphabet that constrains a Python regex,
|
|
472
|
+
# and that question is worth answering on its own rather than in
|
|
473
|
+
# passing here.
|
|
474
|
+
class FromRegexGenerator < Generator
|
|
475
|
+
def initialize(pattern, fullmatch:)
|
|
476
|
+
super()
|
|
477
|
+
@pattern = pattern
|
|
478
|
+
@fullmatch = fullmatch
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def do_draw(tc)
|
|
482
|
+
unless @pattern.is_a?(String)
|
|
483
|
+
raise Hegel::Error, "from_regex: pattern must be a String in Python re syntax, not a #{@pattern.class}"
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
tc.with_regex_generator(@pattern, fullmatch: @fullmatch) { |generator| tc.generate_string(generator) }
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Hegel::Syntax::Methods#emails. An RFC 5321/5322 email address String.
|
|
491
|
+
# hegel_string_generator_email takes no arguments, so there is nothing
|
|
492
|
+
# to validate and nothing for #initialize to hold.
|
|
493
|
+
class EmailsGenerator < Generator
|
|
494
|
+
def do_draw(tc)
|
|
495
|
+
tc.with_email_generator { |generator| tc.generate_string(generator) }
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Hegel::Syntax::Methods#urls. An RFC 3986 http/https URL String.
|
|
500
|
+
# hegel_string_generator_url takes no arguments, for the same reason
|
|
501
|
+
# EmailsGenerator has no #initialize.
|
|
502
|
+
class UrlsGenerator < Generator
|
|
503
|
+
def do_draw(tc)
|
|
504
|
+
tc.with_url_generator { |generator| tc.generate_string(generator) }
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Hegel::Syntax::Methods#domains. A fully-qualified domain name String
|
|
509
|
+
# of at most +max_length+ characters (default 255, the header's own
|
|
510
|
+
# upper bound). The header documents the valid range as 4..=255; that
|
|
511
|
+
# range is not checked here -- hegel_string_generator_domain already
|
|
512
|
+
# returns HEGEL_E_INVALID_ARG outside it, which LibHegel.check! turns
|
|
513
|
+
# into a Hegel::Error naming that code, so a local check here would
|
|
514
|
+
# only repeat the engine's own validation with a worse message.
|
|
515
|
+
class DomainsGenerator < Generator
|
|
516
|
+
def initialize(max_length:)
|
|
517
|
+
super()
|
|
518
|
+
@max_length = max_length
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def do_draw(tc)
|
|
522
|
+
tc.with_domain_generator(max_length: @max_length) { |generator| tc.generate_string(generator) }
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Hegel::Syntax::Methods#ip_addresses. An IPAddr, v4 or v6 depending on
|
|
527
|
+
# +v4+/+v6+. Both true (the default) draws a boolean to pick the
|
|
528
|
+
# family for each value; both false has no family left to draw from,
|
|
529
|
+
# so it raises here rather than reaching hegel_generate_ipv4 or
|
|
530
|
+
# hegel_generate_ipv6 at all.
|
|
531
|
+
#
|
|
532
|
+
# Spanned with HEGEL_LABEL_IP_ADDRESS around the whole draw, the same
|
|
533
|
+
# way OptionalGenerator spans its own boolean-then-delegate draw
|
|
534
|
+
# (see the HEGEL_LABEL_* table): when both families are enabled this
|
|
535
|
+
# generator makes two native calls (the family choice, then the
|
|
536
|
+
# address) to produce one value, and the span is what lets the
|
|
537
|
+
# shrinker retry that pair together instead of the two calls
|
|
538
|
+
# separately.
|
|
539
|
+
class IpAddressesGenerator < Generator
|
|
540
|
+
def initialize(v4:, v6:)
|
|
541
|
+
super()
|
|
542
|
+
@v4 = v4
|
|
543
|
+
@v6 = v6
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def do_draw(tc)
|
|
547
|
+
raise Hegel::Error, "ip_addresses: v4 and v6 must not both be false" if !@v4 && !@v6
|
|
548
|
+
|
|
549
|
+
tc.start_span(LibHegel::HEGEL_LABEL_IP_ADDRESS)
|
|
550
|
+
begin
|
|
551
|
+
draw_address(tc)
|
|
552
|
+
ensure
|
|
553
|
+
tc.stop_span(discard: false)
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
private
|
|
558
|
+
|
|
559
|
+
# hegel_generate_ipv4/hegel_generate_ipv6 return the address's raw
|
|
560
|
+
# network-order bytes (see TestCase#generate_ipv4/#generate_ipv6).
|
|
561
|
+
# IPAddr.new_ntoh builds an IPAddr straight from that byte string,
|
|
562
|
+
# picking v4 or v6 by its length (4 or 16), so no manual byte-to-
|
|
563
|
+
# integer conversion belongs here.
|
|
564
|
+
def draw_address(tc)
|
|
565
|
+
use_v4 = (@v4 && @v6) ? tc.generate_boolean : @v4
|
|
566
|
+
IPAddr.new_ntoh(use_v4 ? tc.generate_ipv4 : tc.generate_ipv6)
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Hegel::Syntax::Methods#uuids. A UUID String in the standard 8-4-4-4-12
|
|
571
|
+
# hex form, matching SecureRandom.uuid's own format. It returns a
|
|
572
|
+
# String because Ruby's stdlib has no dedicated UUID type and
|
|
573
|
+
# SecureRandom itself returns one; a richer type would mean a runtime
|
|
574
|
+
# dependency, which this gem does not take for a formatting choice.
|
|
575
|
+
# version: nil (the default)
|
|
576
|
+
# draws uniform random bits except the nil UUID, per the header; an
|
|
577
|
+
# explicit version forces the RFC 4122 version and variant nibbles.
|
|
578
|
+
#
|
|
579
|
+
# Opens no span: HEGEL_LABEL_UUID is a per-draw label the engine itself
|
|
580
|
+
# emits inside hegel_generate_uuid, not something this binding opens --
|
|
581
|
+
# the header's own comment on HEGEL_LABEL_INTEGER says the same of
|
|
582
|
+
# hegel_generate_integer/_big ("Emitted internally, like every per-draw
|
|
583
|
+
# label"). BooleanGenerator, IntegerGenerator, and FloatGenerator each
|
|
584
|
+
# make exactly one native call to produce their own value the same way
|
|
585
|
+
# uuids() does here, and each opens no span of its own for the same
|
|
586
|
+
# reason. A span belongs only around a generator that composes more
|
|
587
|
+
# than one native call into one draw (see IpAddressesGenerator, whose
|
|
588
|
+
# family choice and address draw are two calls under one span).
|
|
589
|
+
class UuidsGenerator < Generator
|
|
590
|
+
def initialize(version:)
|
|
591
|
+
super()
|
|
592
|
+
@version = version
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def do_draw(tc)
|
|
596
|
+
has_version = !@version.nil?
|
|
597
|
+
raw = tc.generate_uuid(@version || 0, has_version)
|
|
598
|
+
hex = raw.unpack1("H*")
|
|
599
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Hegel::Syntax::Methods#dates. A proleptic Gregorian calendar Date in
|
|
604
|
+
# [min_value, max_value], defaulting to the conventional full range
|
|
605
|
+
# (year 1 through year 9999) when either bound is omitted -- hegel-rust's
|
|
606
|
+
# own src/test_case.rs names this full_ranges::MIN_DATE/MAX_DATE, "what
|
|
607
|
+
# Hypothesis's dates() spans".
|
|
608
|
+
#
|
|
609
|
+
# Opens no span: hegel_generate_date makes exactly one native call to
|
|
610
|
+
# produce its own value, and the header's own comment on
|
|
611
|
+
# HEGEL_LABEL_REGEX ("callers normally never open this span themselves.
|
|
612
|
+
# Likewise for the other engine-side compound draws below") covers
|
|
613
|
+
# HEGEL_LABEL_DATE too, since it sits below REGEX in that same list --
|
|
614
|
+
# the same one-native-call, no-span-of-our-own reasoning UuidsGenerator's
|
|
615
|
+
# own comment already gives for HEGEL_LABEL_UUID.
|
|
616
|
+
class DatesGenerator < Generator
|
|
617
|
+
MIN_DATE = Date.new(1, 1, 1)
|
|
618
|
+
MAX_DATE = Date.new(9999, 12, 31)
|
|
619
|
+
|
|
620
|
+
def initialize(min_value:, max_value:)
|
|
621
|
+
super()
|
|
622
|
+
@min_value = min_value
|
|
623
|
+
@max_value = max_value
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def do_draw(tc)
|
|
627
|
+
min_value = @min_value || MIN_DATE
|
|
628
|
+
max_value = @max_value || MAX_DATE
|
|
629
|
+
raise Hegel::Error, "dates: max_value < min_value" if max_value < min_value
|
|
630
|
+
|
|
631
|
+
year, month, day = tc.generate_date(
|
|
632
|
+
[min_value.year, min_value.month, min_value.day], [max_value.year, max_value.month, max_value.day]
|
|
633
|
+
)
|
|
634
|
+
Date.new(year, month, day)
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Hegel::Syntax::Methods#times. A time of day String, "HH:MM:SS.ffffff",
|
|
639
|
+
# in [min_value, max_value] (also "HH:MM:SS.ffffff" Strings), defaulting
|
|
640
|
+
# to the conventional full day (00:00:00.000000 through 23:59:59.999999
|
|
641
|
+
# -- hegel-rust's own full_ranges::MIDNIGHT/LAST_MICROSECOND) when either
|
|
642
|
+
# bound is omitted.
|
|
643
|
+
#
|
|
644
|
+
# Returns a String, not a Time: Ruby's stdlib has no type for a bare
|
|
645
|
+
# time of day (hour/minute/second/microsecond, with no date), and
|
|
646
|
+
# building one from Time would attach an arbitrary date component to a
|
|
647
|
+
# value that has none -- printing, comparing, or inspecting it would
|
|
648
|
+
# read as though that date meant something, when it is only ever a
|
|
649
|
+
# placeholder this generator invented to satisfy Time's own
|
|
650
|
+
# constructor. min_value/max_value share that same String
|
|
651
|
+
# representation, both for symmetry with the return value and because
|
|
652
|
+
# it is the only representation available on the input side either.
|
|
653
|
+
#
|
|
654
|
+
# Opens no span, for the same reason DatesGenerator does not (one
|
|
655
|
+
# native call, HEGEL_LABEL_TIME sits below HEGEL_LABEL_REGEX in the same
|
|
656
|
+
# header list).
|
|
657
|
+
class TimesGenerator < Generator
|
|
658
|
+
MIDNIGHT = "00:00:00.000000"
|
|
659
|
+
LAST_MICROSECOND = "23:59:59.999999"
|
|
660
|
+
|
|
661
|
+
# Matches exactly what #do_draw's own format("%02d:%02d:%02d.%06d",
|
|
662
|
+
# ...) produces, so parsing and formatting agree on the same shape.
|
|
663
|
+
FORMAT = /\A(\d{2}):(\d{2}):(\d{2})\.(\d{6})\z/
|
|
664
|
+
|
|
665
|
+
def initialize(min_value:, max_value:)
|
|
666
|
+
super()
|
|
667
|
+
@min_value = min_value
|
|
668
|
+
@max_value = max_value
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def do_draw(tc)
|
|
672
|
+
min_parts = parse(@min_value || MIDNIGHT, "min_value")
|
|
673
|
+
max_parts = parse(@max_value || LAST_MICROSECOND, "max_value")
|
|
674
|
+
raise Hegel::Error, "times: max_value < min_value" if (max_parts <=> min_parts).negative?
|
|
675
|
+
|
|
676
|
+
hour, minute, second, microsecond = tc.generate_time(min_parts, max_parts)
|
|
677
|
+
format("%02d:%02d:%02d.%06d", hour, minute, second, microsecond)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
private
|
|
681
|
+
|
|
682
|
+
# +value+'s hour/minute/second/microsecond as an Array of Integers,
|
|
683
|
+
# or raises if it is not a String matching FORMAT. This layer does
|
|
684
|
+
# not range-check the parsed fields (an hour of 99, say): measured
|
|
685
|
+
# against libhegel 0.32.5, hegel_generate_time already returns
|
|
686
|
+
# HEGEL_E_INVALID_ARG for an invalid time, translated by
|
|
687
|
+
# LibHegel.check! once #do_draw calls tc.generate_time, the same
|
|
688
|
+
# division of labor DomainsGenerator follows for its own
|
|
689
|
+
# out-of-range max_length.
|
|
690
|
+
def parse(value, name)
|
|
691
|
+
match = value.is_a?(String) && FORMAT.match(value)
|
|
692
|
+
raise Hegel::Error, "times: #{name} must be \"HH:MM:SS.ffffff\", got #{value.inspect}" unless match
|
|
693
|
+
|
|
694
|
+
match.captures.map(&:to_i)
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Hegel::Syntax::Methods#datetimes. A naive (no timezone) Time in
|
|
699
|
+
# [min_value, max_value], defaulting to the conventional full range
|
|
700
|
+
# (0001-01-01T00:00:00.000000 through 9999-12-31T23:59:59.999999 --
|
|
701
|
+
# hegel-rust's own full_ranges::MIN_DATETIME/MAX_DATETIME) when either
|
|
702
|
+
# bound is omitted.
|
|
703
|
+
#
|
|
704
|
+
# Built with Time.utc, not Time.new/Time.local: the drawn value has no
|
|
705
|
+
# timezone of its own (hegel.h calls hegel_datetime_t "a date plus a
|
|
706
|
+
# time of day, no timezone"), and UTC is the one zone every machine
|
|
707
|
+
# running this gem's own tests agrees on, so a drawn value's own
|
|
708
|
+
# year/month/day/hour/min/sec/usec read back exactly the fields that
|
|
709
|
+
# were drawn, not shifted by whatever zone the process happens to run
|
|
710
|
+
# in. min_value/max_value are read the same way: #do_draw takes
|
|
711
|
+
# whatever zone the caller's own Time is already in at face value
|
|
712
|
+
# (its own #year/#month/#day/#hour/#min/#sec/#usec), rather than
|
|
713
|
+
# converting to UTC first, so a caller who wants a specific wall-clock
|
|
714
|
+
# bound does not have to convert it themselves.
|
|
715
|
+
#
|
|
716
|
+
# Opens no span, for the same reason DatesGenerator does not (one
|
|
717
|
+
# native call, HEGEL_LABEL_DATETIME sits below HEGEL_LABEL_REGEX in the
|
|
718
|
+
# same header list).
|
|
719
|
+
class DatetimesGenerator < Generator
|
|
720
|
+
MIN_DATETIME = Time.utc(1, 1, 1, 0, 0, 0, 0)
|
|
721
|
+
MAX_DATETIME = Time.utc(9999, 12, 31, 23, 59, 59, 999_999)
|
|
722
|
+
|
|
723
|
+
def initialize(min_value:, max_value:)
|
|
724
|
+
super()
|
|
725
|
+
@min_value = min_value
|
|
726
|
+
@max_value = max_value
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def do_draw(tc)
|
|
730
|
+
min_value = @min_value || MIN_DATETIME
|
|
731
|
+
max_value = @max_value || MAX_DATETIME
|
|
732
|
+
raise Hegel::Error, "datetimes: max_value < min_value" if max_value < min_value
|
|
733
|
+
|
|
734
|
+
date, time = tc.generate_datetime(
|
|
735
|
+
[min_value.year, min_value.month, min_value.day],
|
|
736
|
+
[min_value.hour, min_value.min, min_value.sec, min_value.usec],
|
|
737
|
+
[max_value.year, max_value.month, max_value.day],
|
|
738
|
+
[max_value.hour, max_value.min, max_value.sec, max_value.usec]
|
|
739
|
+
)
|
|
740
|
+
Time.utc(*date, *time)
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
# Hegel::Syntax::Methods#composite. Builds a generator from imperative
|
|
745
|
+
# code, the way Hypothesis's @composite decorator and hegel-rust's
|
|
746
|
+
# compose!/hegel-go's Composite let a caller assemble one value out of
|
|
747
|
+
# several draws without writing a Generator subclass: +block+ receives
|
|
748
|
+
# a draw surface (BlockTestCase below) and can call #draw on it any
|
|
749
|
+
# number of times.
|
|
750
|
+
#
|
|
751
|
+
# Spanned with HEGEL_LABEL_FLAT_MAP, not a label of its own: the header
|
|
752
|
+
# documents that label as the span around "a `flat_map` / monadic
|
|
753
|
+
# dependent draw", and a composite block is exactly that shape -- one
|
|
754
|
+
# or more draws, each free to depend on values the block already
|
|
755
|
+
# built, folded into a single result. No composite-specific label
|
|
756
|
+
# exists in the table this binding draws from (see lib_hegel.rb); if a
|
|
757
|
+
# true flat_map combinator is added later it can share this label, or
|
|
758
|
+
# the table can grow a dedicated one (hegel.h documents that a library
|
|
759
|
+
# may mint its own stable u64).
|
|
760
|
+
class CompositeGenerator < Generator
|
|
761
|
+
def initialize(&block)
|
|
762
|
+
super()
|
|
763
|
+
@block = block
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def do_draw(tc)
|
|
767
|
+
raise Hegel::Error, "composite: block is required" unless @block
|
|
768
|
+
|
|
769
|
+
tc.start_span(LibHegel::HEGEL_LABEL_FLAT_MAP)
|
|
770
|
+
begin
|
|
771
|
+
@block.call(BlockTestCase.new(tc))
|
|
772
|
+
ensure
|
|
773
|
+
tc.stop_span(discard: false)
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# The draw surface a composite block receives. #draw reaches an
|
|
778
|
+
# inner generator's own #do_draw directly -- the same non-recording
|
|
779
|
+
# path ArrayGenerator#draw_element and every other compound
|
|
780
|
+
# generator's do_draw already takes -- rather than the real
|
|
781
|
+
# Hegel::TestCase#draw, which records. TestCase's own class comment
|
|
782
|
+
# states the invariant this preserves: a compound generator making
|
|
783
|
+
# several native calls produces exactly one report line for the
|
|
784
|
+
# value it built, not one per call. A composite block draws exactly
|
|
785
|
+
# the way a generator author's own do_draw does, so it is held to
|
|
786
|
+
# the same rule; letting the block reach the recording #draw instead
|
|
787
|
+
# would turn one composite draw into as many report lines as it made
|
|
788
|
+
# nested draws.
|
|
789
|
+
#
|
|
790
|
+
# #draw_integer/#draw_boolean are defined here for the same reason,
|
|
791
|
+
# not left to a method_missing delegation to the wrapped TestCase:
|
|
792
|
+
# the real TestCase already exposes both as recording methods, and a
|
|
793
|
+
# blanket delegation would let those two reach the engine
|
|
794
|
+
# unrecorded-in-name only, still bypassing the single-report-line
|
|
795
|
+
# rule #draw exists to keep the same way plain #draw would.
|
|
796
|
+
class BlockTestCase
|
|
797
|
+
def initialize(tc)
|
|
798
|
+
@tc = tc
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
# Draws +generator+ without recording a report line of its own.
|
|
802
|
+
def draw(generator)
|
|
803
|
+
generator.do_draw(@tc)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
# hegel_generate_integer, without recording (see #draw).
|
|
807
|
+
def draw_integer(min_value, max_value)
|
|
808
|
+
@tc.generate_integer(min_value, max_value)
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# hegel_generate_boolean, without recording (see #draw).
|
|
812
|
+
def draw_boolean(p = 0.5)
|
|
813
|
+
@tc.generate_boolean(p)
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# Hegel::Syntax::Methods#deferred. A forward reference to a generator
|
|
819
|
+
# whose definition is not known yet, so a generator can refer to
|
|
820
|
+
# itself (or to another deferred still being built) before #set
|
|
821
|
+
# installs the real one -- enabling self-recursive and mutually
|
|
822
|
+
# recursive generators, the same shape as hegel-rust's
|
|
823
|
+
# DeferredGeneratorDefinition, hegel-cpp's DeferredGeneratorDefinition,
|
|
824
|
+
# and hegel-java's Deferred:
|
|
825
|
+
#
|
|
826
|
+
# tree = deferred
|
|
827
|
+
# tree.set(one_of(integers, arrays(tree)))
|
|
828
|
+
# tc.draw(tree)
|
|
829
|
+
#
|
|
830
|
+
# Opens no span: #do_draw makes no native call of its own, only
|
|
831
|
+
# forwarding to whatever #set installed (one_of, above, already opens
|
|
832
|
+
# its own HEGEL_LABEL_ONE_OF span around that draw). This is the same
|
|
833
|
+
# "a generator opens a span only around more than one native call it
|
|
834
|
+
# makes itself" rule UuidsGenerator's own comment gives for a
|
|
835
|
+
# generator that makes exactly one -- applied here to a generator that
|
|
836
|
+
# makes zero. Every reference binding checked (hegel-rust's
|
|
837
|
+
# DeferredGenerator, hegel-cpp's DeferredGenerator, hegel-java's
|
|
838
|
+
# Deferred) opens none either.
|
|
839
|
+
class DeferredGenerator < Generator
|
|
840
|
+
def initialize
|
|
841
|
+
super
|
|
842
|
+
@inner = nil
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
# Installs the generator this reference forwards to. May be called
|
|
846
|
+
# only once: hegel-java's Deferred#set raises
|
|
847
|
+
# IllegalStateException on a second call and hegel-cpp's throws;
|
|
848
|
+
# hegel-rust's consumes self, making a second call a compile error.
|
|
849
|
+
# Three independent bindings agree a second #set is a caller
|
|
850
|
+
# mistake, not a silent overwrite or no-op, so this raises the same
|
|
851
|
+
# way.
|
|
852
|
+
def set(generator)
|
|
853
|
+
raise Hegel::Error, "deferred: set called more than once" if @inner
|
|
854
|
+
|
|
855
|
+
@inner = generator
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
def do_draw(tc)
|
|
859
|
+
raise Hegel::Error, "deferred: draw called before set" unless @inner
|
|
860
|
+
|
|
861
|
+
@inner.do_draw(tc)
|
|
862
|
+
end
|
|
863
|
+
end
|
|
864
|
+
end
|
|
865
|
+
end
|