hegeltest 0.1.1 → 0.2.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +32 -9
  4. data/Rakefile +81 -0
  5. data/docs/README.md +9 -0
  6. data/docs/adr/0015-follow-the-hegeldev-interface-and-take-changes-upstream-first.md +101 -0
  7. data/docs/adr/0016-run-mutation-testing-with-mutineer.md +87 -0
  8. data/docs/adr/0017-raise-the-ruby-floor-to-3-4.md +43 -0
  9. data/docs/adr/0018-gate-mutation-testing-on-a-committed-baseline.md +74 -0
  10. data/docs/adr/0019-report-failures-from-the-cases-the-engine-stamps.md +70 -0
  11. data/docs/adr/0020-derive-span-labels-from-generator-names.md +64 -0
  12. data/docs/adr/0021-run-a-state-machine-in-rounds-with-its-own-step-count.md +59 -0
  13. data/docs/adr/0022-keep-microsecond-times-over-a-nanosecond-engine.md +45 -0
  14. data/docs/adr/0023-leave-unset-settings-to-the-engines-profile.md +50 -0
  15. data/docs/architecture.md +11 -6
  16. data/lib/hegel/draw_name.rb +3 -1
  17. data/lib/hegel/generator.rb +50 -9
  18. data/lib/hegel/generators.rb +134 -102
  19. data/lib/hegel/lib_hegel/real.rb +193 -94
  20. data/lib/hegel/lib_hegel.rb +30 -56
  21. data/lib/hegel/libhegel_version.rb +1 -1
  22. data/lib/hegel/report.rb +17 -8
  23. data/lib/hegel/runner.rb +153 -201
  24. data/lib/hegel/settings.rb +9 -14
  25. data/lib/hegel/state_machine.rb +15 -8
  26. data/lib/hegel/stateful/pool.rb +0 -2
  27. data/lib/hegel/stateful.rb +64 -36
  28. data/lib/hegel/syntax/methods.rb +3 -2
  29. data/lib/hegel/test_case.rb +22 -8
  30. data/lib/hegel/version.rb +1 -1
  31. data/lib/hegel.rb +11 -17
  32. data/lib/tasks/libhegel.rake +9 -2
  33. data/sig/hegel.rbs +58 -60
  34. data/skills/hegel-ruby/references/ruby/reference.md +96 -52
  35. metadata +11 -2
@@ -22,7 +22,6 @@ module Hegel
22
22
  # Hegel::Syntax::Methods#booleans. A boolean, true with probability +p+.
23
23
  class BooleanGenerator < Generator
24
24
  def initialize(p:)
25
- super()
26
25
  @p = p
27
26
  end
28
27
 
@@ -45,7 +44,6 @@ module Hegel
45
44
  INT64_MAX = (2**63) - 1
46
45
 
47
46
  def initialize(min_value:, max_value:)
48
- super()
49
47
  @min_value = min_value
50
48
  @max_value = max_value
51
49
  end
@@ -64,22 +62,28 @@ module Hegel
64
62
  end
65
63
 
66
64
  # 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.
65
+ # unbounded by default.
66
+ #
67
+ # allow_nan and allow_infinity are three-valued: nil asks for the
68
+ # default the bounds imply, and true or false overrides it. Three
69
+ # implementations write that default as the same pair of expressions --
70
+ # hegel-rust's src/generators/numeric.rs, hegel-go's primitives.go, and
71
+ # hegel-typescript's src/generators/numeric.ts -- so an unbounded
72
+ # floats() draws NaN and both infinities, and a fully bounded one draws
73
+ # neither. docs/adr/0015 decides to follow that surface rather than the
74
+ # always-off one this generator carried through 0.1.1.
75
+ #
76
+ # The two checks below come with those defaults: hegel-rust and
77
+ # hegel-go each raise on the same two combinations, and the messages
78
+ # here carry the substring theirs do. smallest_nonzero_magnitude and
79
+ # allow_subnormal, which hegel-rust also exposes, stay unexposed; a
80
+ # keyword can be added later without breaking a caller.
76
81
  class FloatGenerator < Generator
77
82
  # hegel_generate_float's width; Ruby has one Float type, the 64-bit
78
83
  # IEEE 754 double, so this is never anything else.
79
84
  WIDTH = 64
80
85
 
81
86
  def initialize(min_value:, max_value:, allow_nan:, allow_infinity:, exclude_min:, exclude_max:)
82
- super()
83
87
  @min_value = min_value
84
88
  @max_value = max_value
85
89
  @allow_nan = allow_nan
@@ -89,13 +93,25 @@ module Hegel
89
93
  end
90
94
 
91
95
  def do_draw(tc)
96
+ has_min = !@min_value.nil?
97
+ has_max = !@max_value.nil?
98
+ allow_nan = @allow_nan.nil? ? !has_min && !has_max : @allow_nan
99
+ allow_infinity = @allow_infinity.nil? ? !has_min || !has_max : @allow_infinity
100
+
101
+ if allow_nan && (has_min || has_max)
102
+ raise Hegel::Error, "floats: cannot have allow_nan=true with min_value or max_value"
103
+ end
104
+ if allow_infinity && has_min && has_max
105
+ raise Hegel::Error, "floats: cannot have allow_infinity=true with both min_value and max_value"
106
+ end
107
+
92
108
  min_value = @min_value || -Float::INFINITY
93
109
  max_value = @max_value || Float::INFINITY
94
110
  raise Hegel::Error, "floats: max_value < min_value" if max_value < min_value
95
111
 
96
112
  tc.generate_float(
97
113
  WIDTH, min_value, max_value,
98
- allow_nan: @allow_nan, allow_infinity: @allow_infinity,
114
+ allow_nan: allow_nan, allow_infinity: allow_infinity,
99
115
  exclude_min: @exclude_min, exclude_max: @exclude_max,
100
116
  smallest_nonzero_magnitude: LibHegel::HEGEL_FLOAT64_SMALLEST_NONZERO_MAGNITUDE_UNRESTRICTED
101
117
  )
@@ -106,7 +122,6 @@ module Hegel
106
122
  # characters, unbounded above by default.
107
123
  class TextGenerator < Generator
108
124
  def initialize(min_size:, max_size:, codec:, min_codepoint:, max_codepoint:)
109
- super()
110
125
  @min_size = min_size
111
126
  @max_size = max_size
112
127
  @codec = codec
@@ -140,25 +155,27 @@ module Hegel
140
155
  # with [min_size, max_size] entries, unbounded above by default.
141
156
  class ArrayGenerator < Generator
142
157
  def initialize(elements, min_size:, max_size:)
143
- super()
144
158
  @elements = elements
145
159
  @min_size = min_size
146
160
  @max_size = max_size
161
+ @label = combined_label(@elements)
147
162
  end
148
163
 
164
+ attr_reader :label
165
+
149
166
  def do_draw(tc)
150
167
  raise Hegel::Error, "arrays: min_size must not be negative" if @min_size.negative?
151
168
 
152
169
  max_size = @max_size || LibHegel::HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED
153
170
  raise Hegel::Error, "arrays: max_size < min_size" if max_size < @min_size
154
171
 
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)
172
+ # One span around the whole array, and one around each element
173
+ # (#draw_element below) labelled with the element generator's own
174
+ # label: the shrinker needs both to shrink a compound draw
175
+ # correctly. A missing or misplaced span here shows up as a
176
+ # larger-than-minimal counterexample, not a test failure (see
177
+ # docs/adr/0006).
178
+ tc.start_span(label)
162
179
  begin
163
180
  draw_elements(tc, max_size)
164
181
  ensure
@@ -180,7 +197,7 @@ module Hegel
180
197
  end
181
198
 
182
199
  def draw_element(tc)
183
- tc.start_span(LibHegel::HEGEL_LABEL_LIST_ELEMENT)
200
+ tc.start_span(@elements.label)
184
201
  @elements.do_draw(tc)
185
202
  ensure
186
203
  tc.stop_span(discard: false)
@@ -193,7 +210,6 @@ module Hegel
193
210
  # nothing here to retry or isolate).
194
211
  class JustGenerator < Generator
195
212
  def initialize(value)
196
- super()
197
213
  @value = value
198
214
  end
199
215
 
@@ -206,14 +222,13 @@ module Hegel
206
222
  # picked by drawing an index in [0, collection.size - 1].
207
223
  class SampledFromGenerator < Generator
208
224
  def initialize(collection)
209
- super()
210
225
  @collection = collection
211
226
  end
212
227
 
213
228
  def do_draw(tc)
214
229
  raise Hegel::Error, "sampled_from: collection must not be empty" if @collection.empty?
215
230
 
216
- tc.start_span(LibHegel::HEGEL_LABEL_SAMPLED_FROM)
231
+ tc.start_span(label)
217
232
  begin
218
233
  index = tc.generate_integer(0, @collection.size - 1)
219
234
  @collection.to_a[index]
@@ -227,14 +242,16 @@ module Hegel
227
242
  # by drawing an index in [0, generators.size - 1].
228
243
  class OneOfGenerator < Generator
229
244
  def initialize(generators)
230
- super()
231
245
  @generators = generators
246
+ @label = combined_label(*@generators)
232
247
  end
233
248
 
249
+ attr_reader :label
250
+
234
251
  def do_draw(tc)
235
252
  raise Hegel::Error, "one_of: at least one generator is required" if @generators.empty?
236
253
 
237
- tc.start_span(LibHegel::HEGEL_LABEL_ONE_OF)
254
+ tc.start_span(label)
238
255
  begin
239
256
  index = tc.generate_integer(0, @generators.size - 1)
240
257
  @generators[index].do_draw(tc)
@@ -249,12 +266,14 @@ module Hegel
249
266
  # milestone leaves generate_boolean at its own default probability.
250
267
  class OptionalGenerator < Generator
251
268
  def initialize(generator)
252
- super()
253
269
  @generator = generator
270
+ @label = combined_label(@generator)
254
271
  end
255
272
 
273
+ attr_reader :label
274
+
256
275
  def do_draw(tc)
257
- tc.start_span(LibHegel::HEGEL_LABEL_OPTIONAL)
276
+ tc.start_span(label)
258
277
  begin
259
278
  tc.generate_boolean ? @generator.do_draw(tc) : nil
260
279
  ensure
@@ -264,18 +283,20 @@ module Hegel
264
283
  end
265
284
 
266
285
  # 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.
286
+ # into an Array (Ruby has no tuple type; see docs/adr/0004). One span
287
+ # goes around the whole draw, unlike arrays/sets/hashes: each position
288
+ # is a distinct generator already, not repeated draws of one, so there
289
+ # is no per-element span to open.
271
290
  class TupleGenerator < Generator
272
291
  def initialize(generators)
273
- super()
274
292
  @generators = generators
293
+ @label = combined_label(*@generators)
275
294
  end
276
295
 
296
+ attr_reader :label
297
+
277
298
  def do_draw(tc)
278
- tc.start_span(LibHegel::HEGEL_LABEL_TUPLE)
299
+ tc.start_span(label)
279
300
  begin
280
301
  @generators.map { |generator| generator.do_draw(tc) }
281
302
  ensure
@@ -288,19 +309,21 @@ module Hegel
288
309
  # [min_size, max_size] entries, unbounded above by default.
289
310
  class SetGenerator < Generator
290
311
  def initialize(elements, min_size:, max_size:)
291
- super()
292
312
  @elements = elements
293
313
  @min_size = min_size
294
314
  @max_size = max_size
315
+ @label = combined_label(@elements)
295
316
  end
296
317
 
318
+ attr_reader :label
319
+
297
320
  def do_draw(tc)
298
321
  raise Hegel::Error, "sets: min_size must not be negative" if @min_size.negative?
299
322
 
300
323
  max_size = @max_size || LibHegel::HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED
301
324
  raise Hegel::Error, "sets: max_size < min_size" if max_size < @min_size
302
325
 
303
- tc.start_span(LibHegel::HEGEL_LABEL_SET)
326
+ tc.start_span(label)
304
327
  begin
305
328
  draw_elements(tc, max_size)
306
329
  ensure
@@ -334,7 +357,7 @@ module Hegel
334
357
  end
335
358
 
336
359
  def draw_element(tc)
337
- tc.start_span(LibHegel::HEGEL_LABEL_SET_ELEMENT)
360
+ tc.start_span(@elements.label)
338
361
  @elements.do_draw(tc)
339
362
  ensure
340
363
  tc.stop_span(discard: false)
@@ -348,21 +371,26 @@ module Hegel
348
371
  # nothing, so it is rejected the same way SetGenerator rejects a
349
372
  # duplicate element.
350
373
  class HashGenerator < Generator
374
+ ENTRY_LABEL = LibHegel.label_from_name("hegel-ruby.Hegel::Generators::HashGenerator.entry")
375
+
351
376
  def initialize(keys, values, min_size:, max_size:)
352
- super()
353
377
  @keys = keys
354
378
  @values = values
355
379
  @min_size = min_size
356
380
  @max_size = max_size
381
+ @label = combined_label(@keys, @values)
382
+ @entry_label = LibHegel.label_combine([ENTRY_LABEL, *[@keys, @values].map { |part| Generator.label_of(part) }])
357
383
  end
358
384
 
385
+ attr_reader :label
386
+
359
387
  def do_draw(tc)
360
388
  raise Hegel::Error, "hashes: min_size must not be negative" if @min_size.negative?
361
389
 
362
390
  max_size = @max_size || LibHegel::HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED
363
391
  raise Hegel::Error, "hashes: max_size < min_size" if max_size < @min_size
364
392
 
365
- tc.start_span(LibHegel::HEGEL_LABEL_MAP)
393
+ tc.start_span(label)
366
394
  begin
367
395
  draw_entries(tc, max_size)
368
396
  ensure
@@ -390,12 +418,11 @@ module Hegel
390
418
  end
391
419
  end
392
420
 
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.
421
+ # Draws the key and its value as one unit inside a single span, so the
422
+ # shrinker can retry the whole pair together. The entry label combines
423
+ # the key and value labels, as the hash's own label does.
397
424
  def draw_entry(tc)
398
- tc.start_span(LibHegel::HEGEL_LABEL_MAP_ENTRY)
425
+ tc.start_span(@entry_label)
399
426
  [@keys.do_draw(tc), @values.do_draw(tc)]
400
427
  ensure
401
428
  tc.stop_span(discard: false)
@@ -411,7 +438,6 @@ module Hegel
411
438
  # delegated #do_draw is the whole draw, not a composition of several.
412
439
  class CharactersGenerator < Generator
413
440
  def initialize(codec:, min_codepoint:, max_codepoint:)
414
- super()
415
441
  @text = TextGenerator.new(
416
442
  min_size: 1, max_size: 1, codec: codec,
417
443
  min_codepoint: min_codepoint, max_codepoint: max_codepoint
@@ -427,7 +453,6 @@ module Hegel
427
453
  # bytes, unbounded above by default.
428
454
  class BinaryGenerator < Generator
429
455
  def initialize(min_size:, max_size:)
430
- super()
431
456
  @min_size = min_size
432
457
  @max_size = max_size
433
458
  end
@@ -473,7 +498,6 @@ module Hegel
473
498
  # passing here.
474
499
  class FromRegexGenerator < Generator
475
500
  def initialize(pattern, fullmatch:)
476
- super()
477
501
  @pattern = pattern
478
502
  @fullmatch = fullmatch
479
503
  end
@@ -514,7 +538,6 @@ module Hegel
514
538
  # only repeat the engine's own validation with a worse message.
515
539
  class DomainsGenerator < Generator
516
540
  def initialize(max_length:)
517
- super()
518
541
  @max_length = max_length
519
542
  end
520
543
 
@@ -529,16 +552,14 @@ module Hegel
529
552
  # so it raises here rather than reaching hegel_generate_ipv4 or
530
553
  # hegel_generate_ipv6 at all.
531
554
  #
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
555
+ # Spanned around the whole draw, the same way OptionalGenerator spans
556
+ # its own boolean-then-delegate draw: when both families are enabled this
535
557
  # generator makes two native calls (the family choice, then the
536
558
  # address) to produce one value, and the span is what lets the
537
559
  # shrinker retry that pair together instead of the two calls
538
560
  # separately.
539
561
  class IpAddressesGenerator < Generator
540
562
  def initialize(v4:, v6:)
541
- super()
542
563
  @v4 = v4
543
564
  @v6 = v6
544
565
  end
@@ -546,7 +567,7 @@ module Hegel
546
567
  def do_draw(tc)
547
568
  raise Hegel::Error, "ip_addresses: v4 and v6 must not both be false" if !@v4 && !@v6
548
569
 
549
- tc.start_span(LibHegel::HEGEL_LABEL_IP_ADDRESS)
570
+ tc.start_span(label)
550
571
  begin
551
572
  draw_address(tc)
552
573
  ensure
@@ -576,19 +597,15 @@ module Hegel
576
597
  # draws uniform random bits except the nil UUID, per the header; an
577
598
  # explicit version forces the RFC 4122 version and variant nibbles.
578
599
  #
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).
600
+ # Opens no span: hegel_generate_uuid is one native call, and the engine
601
+ # labels the spans around its own draws itself. BooleanGenerator,
602
+ # IntegerGenerator, and FloatGenerator each make exactly one native call
603
+ # the same way, and open no span of their own for the same reason. A
604
+ # span belongs only around a generator that composes more than one
605
+ # native call into one draw (see IpAddressesGenerator, whose family
606
+ # choice and address draw are two calls under one span).
589
607
  class UuidsGenerator < Generator
590
608
  def initialize(version:)
591
- super()
592
609
  @version = version
593
610
  end
594
611
 
@@ -606,19 +623,12 @@ module Hegel
606
623
  # own src/test_case.rs names this full_ranges::MIN_DATE/MAX_DATE, "what
607
624
  # Hypothesis's dates() spans".
608
625
  #
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.
626
+ # Opens no span, for the reason UuidsGenerator gives: one native call.
616
627
  class DatesGenerator < Generator
617
628
  MIN_DATE = Date.new(1, 1, 1)
618
629
  MAX_DATE = Date.new(9999, 12, 31)
619
630
 
620
631
  def initialize(min_value:, max_value:)
621
- super()
622
632
  @min_value = min_value
623
633
  @max_value = max_value
624
634
  end
@@ -635,6 +645,22 @@ module Hegel
635
645
  end
636
646
  end
637
647
 
648
+ # Since libhegel 0.36 the engine draws a time of day in whole
649
+ # nanoseconds. times and datetimes keep microseconds, because hegel-rust,
650
+ # hegel-cpp, and hegel-typescript do not agree on a finer precision. A
651
+ # bound's microsecond widens to every nanosecond inside it, and the
652
+ # drawn value rounds down, so every microsecond in range stays
653
+ # reachable, both bounds included.
654
+ def self.first_nanosecond(parts)
655
+ hour, minute, second, microsecond = parts
656
+ [hour, minute, second, microsecond * 1000]
657
+ end
658
+
659
+ def self.last_nanosecond(parts)
660
+ hour, minute, second, microsecond = parts
661
+ [hour, minute, second, (microsecond * 1000) + 999]
662
+ end
663
+
638
664
  # Hegel::Syntax::Methods#times. A time of day String, "HH:MM:SS.ffffff",
639
665
  # in [min_value, max_value] (also "HH:MM:SS.ffffff" Strings), defaulting
640
666
  # to the conventional full day (00:00:00.000000 through 23:59:59.999999
@@ -651,9 +677,7 @@ module Hegel
651
677
  # representation, both for symmetry with the return value and because
652
678
  # it is the only representation available on the input side either.
653
679
  #
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).
680
+ # Opens no span, for the reason UuidsGenerator gives: one native call.
657
681
  class TimesGenerator < Generator
658
682
  MIDNIGHT = "00:00:00.000000"
659
683
  LAST_MICROSECOND = "23:59:59.999999"
@@ -663,7 +687,6 @@ module Hegel
663
687
  FORMAT = /\A(\d{2}):(\d{2}):(\d{2})\.(\d{6})\z/
664
688
 
665
689
  def initialize(min_value:, max_value:)
666
- super()
667
690
  @min_value = min_value
668
691
  @max_value = max_value
669
692
  end
@@ -673,8 +696,10 @@ module Hegel
673
696
  max_parts = parse(@max_value || LAST_MICROSECOND, "max_value")
674
697
  raise Hegel::Error, "times: max_value < min_value" if (max_parts <=> min_parts).negative?
675
698
 
676
- hour, minute, second, microsecond = tc.generate_time(min_parts, max_parts)
677
- format("%02d:%02d:%02d.%06d", hour, minute, second, microsecond)
699
+ hour, minute, second, nanosecond = tc.generate_time(
700
+ Generators.first_nanosecond(min_parts), Generators.last_nanosecond(max_parts)
701
+ )
702
+ format("%02d:%02d:%02d.%06d", hour, minute, second, nanosecond / 1000)
678
703
  end
679
704
 
680
705
  private
@@ -682,7 +707,7 @@ module Hegel
682
707
  # +value+'s hour/minute/second/microsecond as an Array of Integers,
683
708
  # or raises if it is not a String matching FORMAT. This layer does
684
709
  # not range-check the parsed fields (an hour of 99, say): measured
685
- # against libhegel 0.32.5, hegel_generate_time already returns
710
+ # against libhegel 0.45.0, hegel_generate_time already returns
686
711
  # HEGEL_E_INVALID_ARG for an invalid time, translated by
687
712
  # LibHegel.check! once #do_draw calls tc.generate_time, the same
688
713
  # division of labor DomainsGenerator follows for its own
@@ -713,15 +738,12 @@ module Hegel
713
738
  # converting to UTC first, so a caller who wants a specific wall-clock
714
739
  # bound does not have to convert it themselves.
715
740
  #
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).
741
+ # Opens no span, for the reason UuidsGenerator gives: one native call.
719
742
  class DatetimesGenerator < Generator
720
743
  MIN_DATETIME = Time.utc(1, 1, 1, 0, 0, 0, 0)
721
744
  MAX_DATETIME = Time.utc(9999, 12, 31, 23, 59, 59, 999_999)
722
745
 
723
746
  def initialize(min_value:, max_value:)
724
- super()
725
747
  @min_value = min_value
726
748
  @max_value = max_value
727
749
  end
@@ -731,13 +753,18 @@ module Hegel
731
753
  max_value = @max_value || MAX_DATETIME
732
754
  raise Hegel::Error, "datetimes: max_value < min_value" if max_value < min_value
733
755
 
734
- date, time = tc.generate_datetime(
756
+ # A drawn value has whole microseconds, so a bound with a fraction of
757
+ # one narrows to the whole microseconds inside the range. The lower
758
+ # bound rounds up, in its own zone; the upper bound's #usec below
759
+ # already drops its fraction.
760
+ min_value = min_value.ceil(6)
761
+ date, (hour, minute, second, nanosecond) = tc.generate_datetime(
735
762
  [min_value.year, min_value.month, min_value.day],
736
- [min_value.hour, min_value.min, min_value.sec, min_value.usec],
763
+ Generators.first_nanosecond([min_value.hour, min_value.min, min_value.sec, min_value.usec]),
737
764
  [max_value.year, max_value.month, max_value.day],
738
- [max_value.hour, max_value.min, max_value.sec, max_value.usec]
765
+ Generators.last_nanosecond([max_value.hour, max_value.min, max_value.sec, max_value.usec])
739
766
  )
740
- Time.utc(*date, *time)
767
+ Time.utc(*date, hour, minute, second, nanosecond / 1000)
741
768
  end
742
769
  end
743
770
 
@@ -748,25 +775,23 @@ module Hegel
748
775
  # a draw surface (BlockTestCase below) and can call #draw on it any
749
776
  # number of times.
750
777
  #
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).
778
+ # One span goes around the block's draws. Its label names the block by
779
+ # where it is written, so two different composites do not share a
780
+ # label, the way hegel-rust's compose! labels each body by its source.
760
781
  class CompositeGenerator < Generator
761
782
  def initialize(&block)
762
- super()
763
783
  @block = block
784
+ @label = LibHegel.label_combine(
785
+ [self.class.label, LibHegel.label_from_name(Array(@block&.source_location).join(":"))]
786
+ )
764
787
  end
765
788
 
789
+ attr_reader :label
790
+
766
791
  def do_draw(tc)
767
792
  raise Hegel::Error, "composite: block is required" unless @block
768
793
 
769
- tc.start_span(LibHegel::HEGEL_LABEL_FLAT_MAP)
794
+ tc.start_span(label)
770
795
  begin
771
796
  @block.call(BlockTestCase.new(tc))
772
797
  ensure
@@ -829,7 +854,7 @@ module Hegel
829
854
  #
830
855
  # Opens no span: #do_draw makes no native call of its own, only
831
856
  # 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
857
+ # its own span around that draw). This is the same
833
858
  # "a generator opens a span only around more than one native call it
834
859
  # makes itself" rule UuidsGenerator's own comment gives for a
835
860
  # generator that makes exactly one -- applied here to a generator that
@@ -838,7 +863,6 @@ module Hegel
838
863
  # Deferred) opens none either.
839
864
  class DeferredGenerator < Generator
840
865
  def initialize
841
- super
842
866
  @inner = nil
843
867
  end
844
868
 
@@ -855,6 +879,14 @@ module Hegel
855
879
  @inner = generator
856
880
  end
857
881
 
882
+ # The installed generator's label, or this class's own before #set.
883
+ # A generator built from this one before #set keeps the class label,
884
+ # which is how a self-referential definition ends its own cycle, as
885
+ # hegel-rust's DeferredGenerator does.
886
+ def label
887
+ @inner ? @inner.label : self.class.label
888
+ end
889
+
858
890
  def do_draw(tc)
859
891
  raise Hegel::Error, "deferred: draw called before set" unless @inner
860
892