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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +264 -0
  6. data/Rakefile +19 -0
  7. data/docs/README.md +25 -0
  8. data/docs/adr/0001-bind-libhegel-through-fiddle.md +54 -0
  9. data/docs/adr/0002-ship-one-prebuilt-engine-per-platform-specific-gem.md +48 -0
  10. data/docs/adr/0003-publish-as-hegeltest-require-as-hegel.md +39 -0
  11. data/docs/adr/0004-expose-generators-through-a-mixin-with-keyword-options.md +42 -0
  12. data/docs/adr/0005-name-drawn-values-from-the-callers-source-with-prism.md +40 -0
  13. data/docs/adr/0006-verify-the-binding-in-seven-layers-with-full-coverage.md +51 -0
  14. data/docs/adr/0007-ship-a-thin-ruby-skill-shaped-for-donation.md +56 -0
  15. data/docs/adr/0008-revisit-the-binding-after-milestone-c-on-measurement.md +81 -0
  16. data/docs/adr/0009-turn-the-example-database-on-with-a-key.md +89 -0
  17. data/docs/adr/0010-declare-stateful-rules-with-a-class-macro.md +113 -0
  18. data/docs/adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md +83 -0
  19. data/docs/adr/0012-build-a-failure-origin-from-the-callers-own-frame.md +72 -0
  20. data/docs/adr/0013-bind-libhegel-through-the-ffi-gem.md +102 -0
  21. data/docs/architecture.md +182 -0
  22. data/lib/hegel/draw_name.rb +109 -0
  23. data/lib/hegel/errors.rb +47 -0
  24. data/lib/hegel/generator.rb +98 -0
  25. data/lib/hegel/generators.rb +865 -0
  26. data/lib/hegel/lib_hegel/real.rb +1149 -0
  27. data/lib/hegel/lib_hegel.rb +269 -0
  28. data/lib/hegel/libhegel_version.rb +9 -0
  29. data/lib/hegel/locate.rb +188 -0
  30. data/lib/hegel/report.rb +87 -0
  31. data/lib/hegel/runner.rb +464 -0
  32. data/lib/hegel/settings.rb +164 -0
  33. data/lib/hegel/state_machine.rb +89 -0
  34. data/lib/hegel/stateful/pool.rb +111 -0
  35. data/lib/hegel/stateful.rb +120 -0
  36. data/lib/hegel/syntax/methods.rb +173 -0
  37. data/lib/hegel/test_case.rb +523 -0
  38. data/lib/hegel/version.rb +5 -0
  39. data/lib/hegel.rb +92 -0
  40. data/lib/hegeltest.rb +7 -0
  41. data/lib/tasks/libhegel.rake +112 -0
  42. data/lib/tasks/platform_gems.rake +111 -0
  43. data/sig/hegel.rbs +563 -0
  44. data/skills/hegel-ruby/SKILL.md +30 -0
  45. data/skills/hegel-ruby/references/ruby/reference.md +1210 -0
  46. 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