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
@@ -33,7 +33,7 @@ module Hegel
33
33
  end
34
34
 
35
35
  class TimeStruct < FFI::Struct
36
- layout :hour, :uint8, :minute, :uint8, :second, :uint8, :microsecond, :uint32
36
+ layout :hour, :uint8, :minute, :uint8, :second, :uint8, :nanosecond, :uint32
37
37
  end
38
38
 
39
39
  class DatetimeStruct < FFI::Struct
@@ -95,9 +95,6 @@ module Hegel
95
95
  @hegel_settings_set_database_fn = bind(
96
96
  "hegel_settings_set_database", [:pointer, :pointer, :string], :int32
97
97
  )
98
- @hegel_settings_set_stateful_step_count_fn = bind(
99
- "hegel_settings_set_stateful_step_count", [:pointer, :pointer, :int64], :int32
100
- )
101
98
  @hegel_settings_set_report_multiple_failures_fn = bind(
102
99
  "hegel_settings_set_report_multiple_failures", [:pointer, :pointer, :bool], :int32
103
100
  )
@@ -114,7 +111,13 @@ module Hegel
114
111
  @hegel_run_start_fn = bind(
115
112
  "hegel_run_start", [:pointer, :pointer, :pointer, :pointer, :pointer], :int32
116
113
  )
114
+ @hegel_run_start_blob_fn = bind(
115
+ "hegel_run_start_blob", [:pointer, :pointer, :string, :pointer, :pointer, :pointer], :int32
116
+ )
117
117
  @hegel_next_test_case_fn = bind("hegel_next_test_case", [:pointer, :pointer, :pointer], :int32)
118
+ @hegel_test_case_should_capture_fn = bind(
119
+ "hegel_test_case_should_capture", [:pointer, :pointer, :pointer], :int32
120
+ )
118
121
  @hegel_run_free_fn = bind("hegel_run_free", [:pointer, :pointer], :int32)
119
122
  @hegel_test_case_free_fn = bind("hegel_test_case_free", [:pointer, :pointer], :int32)
120
123
  @hegel_mark_complete_fn = bind("hegel_mark_complete", [:pointer, :pointer, :uint32, :string], :int32)
@@ -135,9 +138,7 @@ module Hegel
135
138
  @hegel_failure_reproduction_blob_fn = bind(
136
139
  "hegel_failure_reproduction_blob", [:pointer, :pointer, :pointer], :int32
137
140
  )
138
- @hegel_test_case_from_blob_fn = bind(
139
- "hegel_test_case_from_blob", [:pointer, :pointer, :string, :pointer, :pointer, :pointer], :int32
140
- )
141
+ @hegel_failure_caveat_fn = bind("hegel_failure_caveat", [:pointer, :pointer, :pointer], :int32)
141
142
 
142
143
  @hegel_generate_boolean_fn = bind(
143
144
  "hegel_generate_boolean", [:pointer, :pointer, :double, :bool, :bool, :pointer], :int32
@@ -171,13 +172,22 @@ module Hegel
171
172
 
172
173
  @hegel_new_state_machine_fn = bind(
173
174
  "hegel_new_state_machine",
174
- [:pointer, :pointer, :pointer, :size_t, :pointer, :size_t, :pointer], :int32
175
+ [
176
+ :pointer, :pointer, :pointer, :pointer, :pointer, :size_t, :pointer, :pointer, :size_t,
177
+ :int64, :int64, :int64, :pointer, :pointer
178
+ ], :int32
179
+ )
180
+ @hegel_state_machine_next_group_fn = bind(
181
+ "hegel_state_machine_next_group", [:pointer, :pointer, :pointer, :pointer], :int32
175
182
  )
176
183
  @hegel_state_machine_next_rule_fn = bind(
177
- "hegel_state_machine_next_rule", [:pointer, :pointer, :pointer, :pointer], :int32
184
+ "hegel_state_machine_next_rule", [:pointer, :pointer, :pointer, :int64, :pointer], :int32
178
185
  )
179
186
  @hegel_state_machine_rule_rejected_fn = bind(
180
- "hegel_state_machine_rule_rejected", [:pointer, :pointer, :pointer], :int32
187
+ "hegel_state_machine_rule_rejected", [:pointer, :pointer, :pointer, :int64], :int32
188
+ )
189
+ @hegel_state_machine_should_check_invariant_fn = bind(
190
+ "hegel_state_machine_should_check_invariant", [:pointer, :pointer, :pointer, :int64, :pointer], :int32
181
191
  )
182
192
  @hegel_state_machine_free_fn = bind("hegel_state_machine_free", [:pointer, :pointer], :int32)
183
193
 
@@ -213,7 +223,7 @@ module Hegel
213
223
  )
214
224
 
215
225
  @hegel_string_generator_regex_fn = bind(
216
- "hegel_string_generator_regex", [:pointer, :string, :bool, :pointer, :pointer], :int32
226
+ "hegel_string_generator_regex", [:pointer, :pointer, :size_t, :bool, :pointer, :pointer], :int32
217
227
  )
218
228
  @hegel_string_generator_email_fn = bind("hegel_string_generator_email", [:pointer, :pointer], :int32)
219
229
  @hegel_string_generator_url_fn = bind("hegel_string_generator_url", [:pointer, :pointer], :int32)
@@ -253,7 +263,9 @@ module Hegel
253
263
  # side. The result code is not translated: the header documents this
254
264
  # call as always returning HEGEL_OK, so there is nothing to raise.
255
265
  def context_free(ctx)
256
- @hegel_context_free_fn.call(ctx)
266
+ # Removing this call only leaks native memory; Ruby has no way to
267
+ # observe the leak.
268
+ @hegel_context_free_fn.call(ctx) # mutineer:disable-line statement_removal
257
269
  nil
258
270
  end
259
271
 
@@ -269,13 +281,16 @@ module Hegel
269
281
  def version(ctx)
270
282
  out = FFI::MemoryPointer.new(:pointer)
271
283
  code = @hegel_version_fn.call(ctx, out)
272
- LibHegel.check!(self, ctx, code)
284
+ # This call takes only ctx and this method's own out argument; neither
285
+ # makes the real engine return anything but HEGEL_OK.
286
+ LibHegel.check!(self, ctx, code) # mutineer:disable-line statement_removal
273
287
  utf8(out.read_pointer)
274
288
  end
275
289
 
276
- # Returns a settings handle initialized with libhegel's defaults, or
277
- # raises the exception LibHegel.check! translates this call's result
278
- # code to.
290
+ # Returns a settings handle initialized from the engine's resolved
291
+ # profile, or raises the exception LibHegel.check! translates this
292
+ # call's result code to. Since libhegel 0.40 the call can fail: a
293
+ # malformed hegel.toml or HEGEL_* variable returns HEGEL_E_INVALID_ARG.
279
294
  def settings_new(ctx)
280
295
  out = FFI::MemoryPointer.new(:pointer)
281
296
  code = @hegel_settings_new_fn.call(ctx, out)
@@ -288,7 +303,9 @@ module Hegel
288
303
  # #context_free: the header documents this call as always returning
289
304
  # HEGEL_OK.
290
305
  def settings_free(ctx, s)
291
- @hegel_settings_free_fn.call(ctx, s)
306
+ # Removing this call only leaks native memory; Ruby has no way to
307
+ # observe the leak.
308
+ @hegel_settings_free_fn.call(ctx, s) # mutineer:disable-line statement_removal
292
309
  nil
293
310
  end
294
311
 
@@ -326,12 +343,6 @@ module Hegel
326
343
  nil
327
344
  end
328
345
 
329
- def settings_set_stateful_step_count(ctx, s, n)
330
- code = @hegel_settings_set_stateful_step_count_fn.call(ctx, s, n)
331
- LibHegel.check!(self, ctx, code)
332
- nil
333
- end
334
-
335
346
  def settings_set_report_multiple_failures(ctx, s, yes)
336
347
  code = @hegel_settings_set_report_multiple_failures_fn.call(ctx, s, yes)
337
348
  LibHegel.check!(self, ctx, code)
@@ -374,6 +385,29 @@ module Hegel
374
385
  out.read_pointer
375
386
  end
376
387
 
388
+ # Like #run_start, but the run replays +blob+ (from
389
+ # #failure_reproduction_blob) until a replay fails, under the engine's
390
+ # own bounded budget. A run with no failures means the blob no longer
391
+ # reproduces. Raises HEGEL_E_INVALID_ARG (via LibHegel.check!) for a
392
+ # blob that is corrupt, non-UTF-8, or from an incompatible Hegel
393
+ # version.
394
+ def run_start_blob(ctx, settings, blob)
395
+ out = FFI::MemoryPointer.new(:pointer)
396
+ code = @hegel_run_start_blob_fn.call(ctx, settings, blob, nil, nil, out)
397
+ LibHegel.check!(self, ctx, code)
398
+ out.read_pointer
399
+ end
400
+
401
+ # Whether the engine stamped +tc+ for capture: a failure on a stamped
402
+ # case is material for that failure's report. Read once, when the
403
+ # case starts.
404
+ def test_case_should_capture(ctx, tc)
405
+ out = FFI::MemoryPointer.new(:bool)
406
+ code = @hegel_test_case_should_capture_fn.call(ctx, tc, out)
407
+ LibHegel.check!(self, ctx, code)
408
+ out.read_uint8 != 0
409
+ end
410
+
377
411
  # Returns the next test case, or nil once the run has finished (the
378
412
  # header documents *out_test_case as NULL at that point, with a
379
413
  # HEGEL_OK result rather than an error).
@@ -389,7 +423,9 @@ module Hegel
389
423
  # no-op-on-NULL contract; not translated, for the same reason as
390
424
  # #context_free.
391
425
  def run_free(ctx, run)
392
- @hegel_run_free_fn.call(ctx, run)
426
+ # Removing this call only leaks native memory; Ruby has no way to
427
+ # observe the leak.
428
+ @hegel_run_free_fn.call(ctx, run) # mutineer:disable-line statement_removal
393
429
  nil
394
430
  end
395
431
 
@@ -397,7 +433,9 @@ module Hegel
397
433
  # no-op-on-NULL contract; not translated, for the same reason as
398
434
  # #context_free.
399
435
  def test_case_free(ctx, tc)
400
- @hegel_test_case_free_fn.call(ctx, tc)
436
+ # Removing this call only leaks native memory; Ruby has no way to
437
+ # observe the leak.
438
+ @hegel_test_case_free_fn.call(ctx, tc) # mutineer:disable-line statement_removal
401
439
  nil
402
440
  end
403
441
 
@@ -439,7 +477,9 @@ module Hegel
439
477
  # no-op-on-NULL contract; not translated, for the same reason as
440
478
  # #context_free.
441
479
  def run_result_free(ctx, r)
442
- @hegel_run_result_free_fn.call(ctx, r)
480
+ # Removing this call only leaks native memory; Ruby has no way to
481
+ # observe the leak.
482
+ @hegel_run_result_free_fn.call(ctx, r) # mutineer:disable-line statement_removal
443
483
  nil
444
484
  end
445
485
 
@@ -474,7 +514,7 @@ module Hegel
474
514
 
475
515
  # +index+ must be less than #run_result_failure_count's value, per the
476
516
  # header. Returns a caller-owned failure handle, released separately
477
- # with #failure_free. Measured against libhegel 0.32.5: an
517
+ # with #failure_free. Measured against libhegel 0.45.0: an
478
518
  # out-of-range +index+ comes back HEGEL_E_INVALID_ARG, even though
479
519
  # the header's Returns line for this call names only HEGEL_OK.
480
520
  def run_result_failure(ctx, r, index)
@@ -488,7 +528,9 @@ module Hegel
488
528
  # no-op-on-NULL contract; not translated, for the same reason as
489
529
  # #context_free.
490
530
  def failure_free(ctx, f)
491
- @hegel_failure_free_fn.call(ctx, f)
531
+ # Removing this call only leaks native memory; Ruby has no way to
532
+ # observe the leak.
533
+ @hegel_failure_free_fn.call(ctx, f) # mutineer:disable-line statement_removal
492
534
  nil
493
535
  end
494
536
 
@@ -512,28 +554,18 @@ module Hegel
512
554
  nullable_out_string(out)
513
555
  end
514
556
 
515
- # Replays +blob+ (from #failure_reproduction_blob) against +settings+
516
- # with no run handle and no run loop involved, per the header.
517
- # callback and user_data are always NULL here, for the same reason as
518
- # #run_start. +blob+ is declared :string, the same as
519
- # #settings_set_database's own const char* argument. Raises
520
- # HEGEL_E_INVALID_ARG (via LibHegel.check!) for a blob that is
521
- # corrupt, non-UTF-8, or from an incompatible Hegel version.
522
- #
523
- # A blob whose choices no longer match the caller's generators is a
524
- # different case, and the header places it elsewhere: it "returns
525
- # HEGEL_E_STOP_TEST from the draw that overruns", so the replay is
526
- # built here and fails later, inside the body. Measured against
527
- # 0.32.5, replaying a two-draw blob against a five-draw body builds
528
- # fine and overruns at a draw.
529
- def test_case_from_blob(ctx, settings, blob)
557
+ # Returns how reliably +f+ reproduced under the run's nondeterministic
558
+ # handling, quoting the engine's own replay evidence, or nil for a
559
+ # deterministic failure. See #nullable_out_string for the shared
560
+ # ownership note.
561
+ def failure_caveat(ctx, f)
530
562
  out = FFI::MemoryPointer.new(:pointer)
531
- code = @hegel_test_case_from_blob_fn.call(ctx, settings, blob, nil, nil, out)
563
+ code = @hegel_failure_caveat_fn.call(ctx, f, out)
532
564
  LibHegel.check!(self, ctx, code)
533
- out.read_pointer
565
+ nullable_out_string(out)
534
566
  end
535
567
 
536
- # Forcing has to agree with +p+. Measured against libhegel 0.32.5:
568
+ # Forcing has to agree with +p+. Measured against libhegel 0.45.0:
537
569
  # forcing true at p = 0.0 and forcing false at p = 1.0 both come back
538
570
  # HEGEL_E_INVALID_ARG ("generate_boolean: cannot force ..."), while
539
571
  # forcing either way succeeds at any p between them. The header
@@ -589,8 +621,8 @@ module Hegel
589
621
  LibHegel.decode_integer_le(out_value.read_bytes(len))
590
622
  end
591
623
 
592
- # Opens a span labelled +label+ (one of the HEGEL_LABEL_* constants,
593
- # or a caller-defined value that avoids them). Must be paired with
624
+ # Opens a span labelled +label+, an opaque u64 naming the generator
625
+ # (see LibHegel.label_from_name). Must be paired with
594
626
  # exactly one #stop_span call, per the header.
595
627
  def start_span(ctx, tc, label)
596
628
  code = @hegel_start_span_fn.call(ctx, tc, label)
@@ -643,7 +675,9 @@ module Hegel
643
675
  # call takes no test-case handle: the header documents a collection
644
676
  # as independent of the test case and run it was created under.
645
677
  def collection_free(ctx, collection)
646
- @hegel_collection_free_fn.call(ctx, collection)
678
+ # Removing this call only leaks native memory; Ruby has no way to
679
+ # observe the leak.
680
+ @hegel_collection_free_fn.call(ctx, collection) # mutineer:disable-line statement_removal
647
681
  nil
648
682
  end
649
683
 
@@ -691,7 +725,9 @@ module Hegel
691
725
  # no-op-on-NULL contract; not translated, for the same reason as
692
726
  # #context_free.
693
727
  def pool_free(ctx, pool)
694
- @hegel_pool_free_fn.call(ctx, pool)
728
+ # Removing this call only leaks native memory; Ruby has no way to
729
+ # observe the leak.
730
+ @hegel_pool_free_fn.call(ctx, pool) # mutineer:disable-line statement_removal
695
731
  nil
696
732
  end
697
733
 
@@ -699,12 +735,21 @@ module Hegel
699
735
  # with #state_machine_free. +rule_names+ and +invariant_names+ are
700
736
  # each an Array of Ruby Strings, packed into the const char *const *
701
737
  # arguments hegel_new_state_machine expects by #pack_name_array; see
702
- # that method's own comment for how and why. Validating
703
- # +rule_names+ as non-empty (the header's own requirement) is left
704
- # to the caller, the same division of labor #new_collection leaves
705
- # to the caller for its own min_size/max_size ordering.
706
- def new_state_machine(ctx, tc, rule_names, invariant_names)
738
+ # that method's own comment for how and why. +always_check+ holds one
739
+ # flag per invariant name. Validating +rule_names+ as non-empty (the
740
+ # header's own requirement) is left to the caller, the same division
741
+ # of labor #new_collection leaves to the caller for its own
742
+ # min_size/max_size ordering.
743
+ #
744
+ # The machine is always sequential: one concurrency group, equal rule
745
+ # weights, and a concurrency of exactly 1, so the engine draws no
746
+ # concurrency level and the drawn level needs no reading. These
747
+ # bindings drive the engine from one thread, and a concurrent machine
748
+ # needs one worker thread per drawn level. For the same reason the
749
+ # rule calls below pass worker index 0, the only worker there is.
750
+ def new_state_machine(ctx, tc, rule_names, invariant_names, always_check, step_count)
707
751
  out = FFI::MemoryPointer.new(:pointer)
752
+ out_concurrency = FFI::MemoryPointer.new(:int64)
708
753
  # _rule_pointers / _invariant_pointers are unread past the call
709
754
  # below, the same shape #generate_integer_big's own
710
755
  # min_value_ptr/max_value_ptr already have; keeping them as local
@@ -714,46 +759,69 @@ module Hegel
714
759
  # same way it would for a block argument the block never reads.
715
760
  rule_names_ptr, _rule_pointers = pack_name_array(rule_names)
716
761
  invariant_names_ptr, _invariant_pointers = pack_name_array(invariant_names)
762
+ # A fresh MemoryPointer is zero-filled, which puts every rule in
763
+ # group 0.
764
+ rule_groups = FFI::MemoryPointer.new(:int64, rule_names.size)
717
765
 
718
766
  code = @hegel_new_state_machine_fn.call(
719
- ctx, tc, rule_names_ptr, rule_names.size, invariant_names_ptr, invariant_names.size, out
767
+ ctx, tc, rule_names_ptr, rule_groups, nil, rule_names.size,
768
+ invariant_names_ptr, pack_flags(always_check), invariant_names.size,
769
+ 1, 1, step_count, out, out_concurrency
720
770
  )
721
771
  LibHegel.check!(self, ctx, code)
722
772
  out.read_pointer
723
773
  end
724
774
 
725
- # Returns the index (in 0...num_rules) of the next stateful-testing
726
- # rule to run, or HEGEL_STATE_MACHINE_DONE (-1) once +state_machine+'s
727
- # step budget is exhausted -- returned as the raw sentinel value, not
728
- # translated to nil. Unlike #next_test_case's out-parameter, which is
729
- # NULL (no value) at the equivalent boundary, the header documents
730
- # this out-parameter as holding a real value, -1, at that point; a
731
- # caller comparing against HEGEL_STATE_MACHINE_DONE is the layer that
732
- # should decide what that value means, the same way #run_result_status
733
- # hands back its raw HEGEL_RUN_STATUS_* value unexamined.
775
+ # Starts the machine's next round. Returns the round's group id, or
776
+ # HEGEL_STATE_MACHINE_DONE once the machine is done, as the raw
777
+ # value: the caller compares against the sentinel, the same way
778
+ # #run_result_status hands back its raw HEGEL_RUN_STATUS_* value
779
+ # unexamined.
780
+ def state_machine_next_group(ctx, tc, state_machine)
781
+ out = FFI::MemoryPointer.new(:int64)
782
+ code = @hegel_state_machine_next_group_fn.call(ctx, tc, state_machine, out)
783
+ LibHegel.check!(self, ctx, code)
784
+ out.read_int64
785
+ end
786
+
787
+ # Returns the index (in 0...num_rules) of the next rule to run this
788
+ # round, or HEGEL_STATE_MACHINE_DONE once the round is over, as the
789
+ # raw value for the reason #state_machine_next_group gives.
734
790
  def state_machine_next_rule(ctx, tc, state_machine)
735
791
  out = FFI::MemoryPointer.new(:int64)
736
- code = @hegel_state_machine_next_rule_fn.call(ctx, tc, state_machine, out)
792
+ code = @hegel_state_machine_next_rule_fn.call(ctx, tc, state_machine, 0, out)
737
793
  LibHegel.check!(self, ctx, code)
738
794
  out.read_int64
739
795
  end
740
796
 
741
797
  # Reports the rule most recently returned by #state_machine_next_rule
742
- # as rejected (an assumption failed before it completed), so it does
743
- # not count toward the step budget. Raises HEGEL_E_INVALID_ARG (via
744
- # LibHegel.check!, translated to Hegel::Error) when no rule is
798
+ # as rejected (an assumption failed before it completed), so its round
799
+ # does not count toward the step budget. Raises HEGEL_E_INVALID_ARG
800
+ # (via LibHegel.check!, translated to Hegel::Error) when no rule is
745
801
  # outstanding, per the header.
746
802
  def state_machine_rule_rejected(ctx, tc, state_machine)
747
- code = @hegel_state_machine_rule_rejected_fn.call(ctx, tc, state_machine)
803
+ code = @hegel_state_machine_rule_rejected_fn.call(ctx, tc, state_machine, 0)
748
804
  LibHegel.check!(self, ctx, code)
749
805
  nil
750
806
  end
751
807
 
808
+ # Whether to run the invariant at +index+ at this join point: always
809
+ # true for an invariant flagged at creation, otherwise a recorded draw
810
+ # that is true with probability 1 / step_count.
811
+ def state_machine_should_check_invariant(ctx, tc, state_machine, index)
812
+ out = FFI::MemoryPointer.new(:bool)
813
+ code = @hegel_state_machine_should_check_invariant_fn.call(ctx, tc, state_machine, index, out)
814
+ LibHegel.check!(self, ctx, code)
815
+ out.read_uint8 != 0
816
+ end
817
+
752
818
  # No-op when +state_machine+ is nil, matching
753
819
  # hegel_state_machine_free's documented no-op-on-NULL contract; not
754
820
  # translated, for the same reason as #context_free.
755
821
  def state_machine_free(ctx, state_machine)
756
- @hegel_state_machine_free_fn.call(ctx, state_machine)
822
+ # Removing this call only leaks native memory; Ruby has no way to
823
+ # observe the leak.
824
+ @hegel_state_machine_free_fn.call(ctx, state_machine) # mutineer:disable-line statement_removal
757
825
  nil
758
826
  end
759
827
 
@@ -793,7 +861,9 @@ module Hegel
793
861
  # hegel_string_generator_free's documented no-op-on-NULL contract;
794
862
  # not translated, for the same reason as #context_free.
795
863
  def string_generator_free(ctx, generator)
796
- @hegel_string_generator_free_fn.call(ctx, generator)
864
+ # Removing this call only leaks native memory; Ruby has no way to
865
+ # observe the leak.
866
+ @hegel_string_generator_free_fn.call(ctx, generator) # mutineer:disable-line statement_removal
797
867
  nil
798
868
  end
799
869
 
@@ -828,7 +898,9 @@ module Hegel
828
898
  # contract (also safe on an already-freed, zeroed struct); not
829
899
  # translated, for the same reason as #context_free.
830
900
  def generate_string_result_free(ctx, result)
831
- @hegel_generate_string_result_free_fn.call(ctx, result)
901
+ # Removing this call only leaks native memory; Ruby has no way to
902
+ # observe the leak.
903
+ @hegel_generate_string_result_free_fn.call(ctx, result) # mutineer:disable-line statement_removal
832
904
  nil
833
905
  end
834
906
 
@@ -864,7 +936,9 @@ module Hegel
864
936
  # contract (also safe on an already-freed, zeroed struct); not
865
937
  # translated, for the same reason as #context_free.
866
938
  def generate_bytes_result_free(ctx, result)
867
- @hegel_generate_bytes_result_free_fn.call(ctx, result)
939
+ # Removing this call only leaks native memory; Ruby has no way to
940
+ # observe the leak.
941
+ @hegel_generate_bytes_result_free_fn.call(ctx, result) # mutineer:disable-line statement_removal
868
942
  nil
869
943
  end
870
944
 
@@ -874,10 +948,15 @@ module Hegel
874
948
  # generator handle (built via #string_generator_text, scoped with
875
949
  # Hegel::TestCase#with_text_generator) whose character set constrains the
876
950
  # padding and wildcard characters; nil (the default) marshals to NULL,
877
- # the header's documented "no particular alphabet" case.
951
+ # the header's documented "no particular alphabet" case. The pattern
952
+ # goes over as UTF-8 bytes with a length rather than as a C string, so
953
+ # a NUL character in it reaches the engine instead of ending it.
878
954
  def string_generator_regex(ctx, pattern, fullmatch, alphabet = nil)
879
955
  out = FFI::MemoryPointer.new(:pointer)
880
- code = @hegel_string_generator_regex_fn.call(ctx, pattern, fullmatch, alphabet, out)
956
+ bytes = pattern.encode(Encoding::UTF_8)
957
+ code = @hegel_string_generator_regex_fn.call(
958
+ ctx, bytes_to_pointer(bytes), bytes.bytesize, fullmatch, alphabet, out
959
+ )
881
960
  LibHegel.check!(self, ctx, code)
882
961
  out.read_pointer
883
962
  end
@@ -888,7 +967,9 @@ module Hegel
888
967
  def string_generator_email(ctx)
889
968
  out = FFI::MemoryPointer.new(:pointer)
890
969
  code = @hegel_string_generator_email_fn.call(ctx, out)
891
- LibHegel.check!(self, ctx, code)
970
+ # This call takes only ctx and this method's own out argument; neither
971
+ # makes the real engine return anything but HEGEL_OK.
972
+ LibHegel.check!(self, ctx, code) # mutineer:disable-line statement_removal
892
973
  out.read_pointer
893
974
  end
894
975
 
@@ -897,7 +978,9 @@ module Hegel
897
978
  def string_generator_url(ctx)
898
979
  out = FFI::MemoryPointer.new(:pointer)
899
980
  code = @hegel_string_generator_url_fn.call(ctx, out)
900
- LibHegel.check!(self, ctx, code)
981
+ # This call takes only ctx and this method's own out argument; neither
982
+ # makes the real engine return anything but HEGEL_OK.
983
+ LibHegel.check!(self, ctx, code) # mutineer:disable-line statement_removal
901
984
  out.read_pointer
902
985
  end
903
986
 
@@ -947,7 +1030,7 @@ module Hegel
947
1030
  # 8-4-4-4-12 hex String is left to Hegel::Generators::UuidsGenerator,
948
1031
  # the same division of labor #generate_ipv4/#generate_ipv6 already
949
1032
  # follow for their own byte-to-address conversion. An out-of-range
950
- # +version+ is not checked here: measured against libhegel 0.32.5, the
1033
+ # +version+ is not checked here: measured against libhegel 0.45.0, the
951
1034
  # engine itself returns HEGEL_E_INVALID_ARG for one, which
952
1035
  # LibHegel.check! already translates.
953
1036
  def generate_uuid(ctx, tc, version, has_version)
@@ -965,7 +1048,7 @@ module Hegel
965
1048
  # the same division of labor #generate_ipv4/#generate_uuid already
966
1049
  # follow, returning raw values for a generator one layer up to turn
967
1050
  # into the caller-facing type. This layer does not validate
968
- # year/month/day itself: measured against libhegel 0.32.5, an
1051
+ # year/month/day itself: measured against libhegel 0.45.0, an
969
1052
  # invalid date (month 13, say) already comes back
970
1053
  # HEGEL_E_INVALID_ARG, translated by LibHegel.check! below, even
971
1054
  # though the header's Returns line for this call names only
@@ -979,7 +1062,7 @@ module Hegel
979
1062
  end
980
1063
 
981
1064
  # hegel_generate_time. +min_value+/+max_value+ are each an
982
- # [hour, minute, second, microsecond] Array; same struct-passing,
1065
+ # [hour, minute, second, nanosecond] Array; same struct-passing,
983
1066
  # return shape, and validation division of labor as #generate_date
984
1067
  # above, for Hegel::Generators::TimesGenerator.
985
1068
  def generate_time(ctx, tc, min_value, max_value)
@@ -991,10 +1074,10 @@ module Hegel
991
1074
 
992
1075
  # hegel_generate_datetime. +min_date+/+max_date+ are each a
993
1076
  # [year, month, day] Array; +min_time+/+max_time+ are each an
994
- # [hour, minute, second, microsecond] Array -- hegel_datetime_t is a
1077
+ # [hour, minute, second, nanosecond] Array -- hegel_datetime_t is a
995
1078
  # hegel_date_t followed by a hegel_time_t (see DatetimeStruct's own
996
1079
  # layout, above #initialize). Returns a
997
- # [[year, month, day], [hour, minute, second, microsecond]] pair, for
1080
+ # [[year, month, day], [hour, minute, second, nanosecond]] pair, for
998
1081
  # Hegel::Generators::DatetimesGenerator to build its own Time from.
999
1082
  def generate_datetime(ctx, tc, min_date, min_time, max_date, max_time)
1000
1083
  out = DatetimeStruct.new
@@ -1011,7 +1094,12 @@ module Hegel
1011
1094
  # FFI::Function, the direct form #initialize's own comment explains
1012
1095
  # the choice of.
1013
1096
  def bind(symbol, arg_types, ret_type)
1014
- FFI::Function.new(ret_type, arg_types, @handle.find_function(symbol))
1097
+ # find_function answers nil for a symbol the library lacks, and
1098
+ # FFI::Function.new then fails with a TypeError that names no
1099
+ # symbol. An engine built from another release is the usual cause.
1100
+ function = @handle.find_function(symbol) or
1101
+ raise Hegel::Error, "libhegel has no #{symbol}; these bindings need libhegel #{Hegel::LIBHEGEL_VERSION}"
1102
+ FFI::Function.new(ret_type, arg_types, function)
1015
1103
  end
1016
1104
 
1017
1105
  # Copies +bytes+ into a freshly allocated buffer, for
@@ -1050,7 +1138,9 @@ module Hegel
1050
1138
  # factory method, not in a method whose only job is packing an
1051
1139
  # Array into a buffer.
1052
1140
  def pack_name_array(names)
1053
- return [nil, []] if names.empty?
1141
+ # An empty array acts the same as NULL with length 0:
1142
+ # hegel_new_state_machine reads zero names either way.
1143
+ return [nil, []] if names.empty? # mutineer:disable-line statement_removal
1054
1144
 
1055
1145
  pointers = names.map { |name| FFI::MemoryPointer.from_string(name) }
1056
1146
  array = FFI::MemoryPointer.new(:pointer, names.size)
@@ -1058,6 +1148,15 @@ module Hegel
1058
1148
  [array, pointers]
1059
1149
  end
1060
1150
 
1151
+ # Packs +flags+ (an Array of true/false) into a const bool * buffer,
1152
+ # one byte each. An empty Array packs to a zero-length buffer, which the
1153
+ # engine reads zero flags from.
1154
+ def pack_flags(flags)
1155
+ buffer = FFI::MemoryPointer.new(:uint8, flags.size)
1156
+ buffer.write_array_of_uint8(flags.map { |flag| flag ? 1 : 0 })
1157
+ buffer
1158
+ end
1159
+
1061
1160
  # Writes +value+ (a [year, month, day] Array) into +struct+'s own
1062
1161
  # :year/:month/:day fields. Shared by #date_struct, which builds a
1063
1162
  # standalone DateStruct, and #datetime_struct, which writes the same
@@ -1072,13 +1171,13 @@ module Hegel
1072
1171
  end
1073
1172
 
1074
1173
  # The #write_date/#date_struct counterpart for a [hour, minute,
1075
- # second, microsecond] Array.
1174
+ # second, nanosecond] Array.
1076
1175
  def write_time(struct, value)
1077
- hour, minute, second, microsecond = value
1176
+ hour, minute, second, nanosecond = value
1078
1177
  struct[:hour] = hour
1079
1178
  struct[:minute] = minute
1080
1179
  struct[:second] = second
1081
- struct[:microsecond] = microsecond
1180
+ struct[:nanosecond] = nanosecond
1082
1181
  end
1083
1182
 
1084
1183
  # A standalone DateStruct built from +value+, for #generate_date's
@@ -1118,16 +1217,16 @@ module Hegel
1118
1217
  end
1119
1218
 
1120
1219
  # The #read_date counterpart for a struct's :hour/:minute/:second/
1121
- # :microsecond fields.
1220
+ # :nanosecond fields.
1122
1221
  def read_time(struct)
1123
- [struct[:hour], struct[:minute], struct[:second], struct[:microsecond]]
1222
+ [struct[:hour], struct[:minute], struct[:second], struct[:nanosecond]]
1124
1223
  end
1125
1224
 
1126
1225
  # Reads +out+'s const char* out-parameter into a Ruby String, or
1127
- # returns nil if libhegel left it NULL. Shared by #run_result_error
1128
- # and #failure_reproduction_blob, the two out-parameters the header
1129
- # documents as nullable, so both branches only need to be exercised
1130
- # once between the two call sites rather than at each one.
1226
+ # returns nil if libhegel left it NULL. Shared by #run_result_error,
1227
+ # #failure_reproduction_blob, and #failure_caveat, the out-parameters
1228
+ # the header documents as nullable, so both branches only need to be
1229
+ # exercised once between the call sites rather than at each one.
1131
1230
  def nullable_out_string(out)
1132
1231
  ptr = out.read_pointer
1133
1232
  ptr.null? ? nil : utf8(ptr)