hegeltest 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +182 -0
  3. data/README.ja.md +408 -0
  4. data/README.md +208 -102
  5. data/Rakefile +6 -3
  6. data/docs/README.md +9 -0
  7. data/docs/adr/0027-report-each-runs-test-location-from-the-callers-frame.md +63 -0
  8. data/docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md +91 -0
  9. data/docs/adr/0029-keep-notes-local-and-defer-run-error-kinds.md +72 -0
  10. data/docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md +48 -0
  11. data/docs/adr/0031-take-the-alphabet-options-from-hegel-rust.md +52 -0
  12. data/docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md +147 -0
  13. data/docs/adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md +80 -0
  14. data/docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md +120 -0
  15. data/docs/adr/0035-defer-concurrent-state-machines-and-the-engine-printer.md +136 -0
  16. data/docs/architecture.md +16 -1
  17. data/lib/hegel/errors.rb +24 -0
  18. data/lib/hegel/generator.rb +52 -25
  19. data/lib/hegel/generators.rb +715 -45
  20. data/lib/hegel/grammar_cache.rb +36 -0
  21. data/lib/hegel/lib_hegel/real.rb +320 -17
  22. data/lib/hegel/lib_hegel.rb +31 -6
  23. data/lib/hegel/libhegel_version.rb +1 -1
  24. data/lib/hegel/report.rb +13 -10
  25. data/lib/hegel/runner.rb +61 -27
  26. data/lib/hegel/settings.rb +92 -16
  27. data/lib/hegel/state_machine.rb +17 -4
  28. data/lib/hegel/stateful/pool.rb +52 -0
  29. data/lib/hegel/stateful.rb +2 -2
  30. data/lib/hegel/syntax/methods.rb +99 -15
  31. data/lib/hegel/test_case.rb +256 -35
  32. data/lib/hegel/test_location.rb +49 -0
  33. data/lib/hegel/version.rb +1 -1
  34. data/lib/hegel.rb +21 -5
  35. data/sig/hegel.rbs +295 -42
  36. data/skills/hegel-ruby/references/ruby/reference.md +463 -41
  37. metadata +13 -1
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hegel
4
+ # The grammars compiled during one run, keyed by everything the
5
+ # compilation read. Hegel::Runner.run owns one and frees it after the run
6
+ # and before the context; see docs/adr/0033.
7
+ class GrammarCache
8
+ # A compiled grammar handle and the marker standing for each explicit
9
+ # terminal, in the order the terminals were named.
10
+ Compiled = Data.define(:grammar, :markers)
11
+
12
+ def initialize
13
+ @entries = {}
14
+ end
15
+
16
+ # The entry under +key+, or the block's value, stored only once the
17
+ # block returns, so a compilation that raises leaves nothing behind.
18
+ def fetch(key)
19
+ @entries.fetch(key) { @entries[key] = yield }
20
+ end
21
+
22
+ # Frees every grammar, carrying on past a free that raises so that the
23
+ # rest are still freed before the context, and raises the first error
24
+ # once all have been tried.
25
+ def free_all(impl, ctx)
26
+ error = nil
27
+ @entries.each_value do |compiled|
28
+ impl.grammar_free(ctx, compiled.grammar)
29
+ rescue => e
30
+ error ||= e
31
+ end
32
+ @entries.clear
33
+ raise error if error
34
+ end
35
+ end
36
+ end
@@ -82,6 +82,13 @@ module Hegel
82
82
 
83
83
  @hegel_settings_new_fn = bind("hegel_settings_new", [:pointer, :pointer], :int32)
84
84
  @hegel_settings_free_fn = bind("hegel_settings_free", [:pointer, :pointer], :int32)
85
+ @hegel_settings_new_for_profile_fn = bind(
86
+ "hegel_settings_new_for_profile", [:pointer, StringStruct.by_value, :pointer], :int32
87
+ )
88
+ @hegel_settings_register_profile_fn = bind(
89
+ "hegel_settings_register_profile", [:pointer, StringStruct.by_value, :pointer], :int32
90
+ )
91
+ @hegel_set_default_profile_fn = bind("hegel_set_default_profile", [:pointer, StringStruct.by_value], :int32)
85
92
  @hegel_settings_set_test_cases_fn = bind(
86
93
  "hegel_settings_set_test_cases", [:pointer, :pointer, :uint64], :int32
87
94
  )
@@ -109,6 +116,24 @@ module Hegel
109
116
  @hegel_settings_set_suppress_health_check_fn = bind(
110
117
  "hegel_settings_set_suppress_health_check", [:pointer, :pointer, :uint32], :int32
111
118
  )
119
+ @hegel_settings_set_stop_on_first_failure_fn = bind(
120
+ "hegel_settings_set_stop_on_first_failure", [:pointer, :pointer, :bool], :int32
121
+ )
122
+ @hegel_settings_set_print_blob_fn = bind("hegel_settings_set_print_blob", [:pointer, :pointer, :bool], :int32)
123
+ @hegel_settings_get_print_blob_fn = bind(
124
+ "hegel_settings_get_print_blob", [:pointer, :pointer, :pointer], :int32
125
+ )
126
+ @hegel_settings_set_show_statistics_fn = bind(
127
+ "hegel_settings_set_show_statistics", [:pointer, :pointer, :bool], :int32
128
+ )
129
+ @hegel_settings_set_nondeterminism_strictness_fn = bind(
130
+ "hegel_settings_set_nondeterminism_strictness", [:pointer, :pointer, :uint32], :int32
131
+ )
132
+ @hegel_settings_set_backend_fn = bind("hegel_settings_set_backend", [:pointer, :pointer, :uint32], :int32)
133
+ @hegel_settings_set_test_location_fn = bind(
134
+ "hegel_settings_set_test_location",
135
+ [:pointer, :pointer, StringStruct.by_value, :uint32, StringStruct.by_value, StringStruct.by_value], :int32
136
+ )
112
137
 
113
138
  @hegel_run_start_fn = bind(
114
139
  "hegel_run_start", [:pointer, :pointer, :pointer, :pointer, :pointer], :int32
@@ -124,6 +149,10 @@ module Hegel
124
149
  @hegel_test_case_free_fn = bind("hegel_test_case_free", [:pointer, :pointer], :int32)
125
150
  @hegel_mark_complete_fn = bind("hegel_mark_complete", [:pointer, :pointer, :uint32, :pointer], :int32)
126
151
  @hegel_target_fn = bind("hegel_target", [:pointer, :pointer, :double, StringStruct.by_value], :int32)
152
+ @hegel_event_fn = bind("hegel_event", [:pointer, :pointer, StringStruct.by_value], :int32)
153
+ @hegel_event_value_fn = bind(
154
+ "hegel_event_value", [:pointer, :pointer, :double, StringStruct.by_value], :int32
155
+ )
127
156
 
128
157
  @hegel_run_result_fn = bind("hegel_run_result", [:pointer, :pointer, :pointer], :int32)
129
158
  @hegel_run_result_free_fn = bind("hegel_run_result_free", [:pointer, :pointer], :int32)
@@ -170,8 +199,20 @@ module Hegel
170
199
  @hegel_pool_generate_fn = bind(
171
200
  "hegel_pool_generate", [:pointer, :pointer, :pointer, :bool, :pointer], :int32
172
201
  )
202
+ @hegel_pool_restore_fn = bind("hegel_pool_restore", [:pointer, :pointer, :pointer, :int64], :int32)
173
203
  @hegel_pool_free_fn = bind("hegel_pool_free", [:pointer, :pointer], :int32)
174
204
 
205
+ @hegel_new_recursion_fn = bind(
206
+ "hegel_new_recursion", [:pointer, :pointer, :uint64, :uint64, :pointer], :int32
207
+ )
208
+ @hegel_recursion_branch_fn = bind(
209
+ "hegel_recursion_branch", [:pointer, :pointer, :pointer, :uint64, :pointer], :int32
210
+ )
211
+ @hegel_recursion_leaf_fn = bind("hegel_recursion_leaf", [:pointer, :pointer, :pointer], :int32)
212
+ @hegel_recursion_retry_fn = bind("hegel_recursion_retry", [:pointer, :pointer, :pointer], :int32)
213
+ @hegel_recursion_finish_fn = bind("hegel_recursion_finish", [:pointer, :pointer, :pointer], :int32)
214
+ @hegel_recursion_free_fn = bind("hegel_recursion_free", [:pointer, :pointer], :int32)
215
+
175
216
  @hegel_new_state_machine_fn = bind(
176
217
  "hegel_new_state_machine",
177
218
  [
@@ -198,10 +239,6 @@ module Hegel
198
239
  [:pointer, :pointer, :uint32, :double, :double, :bool, :bool, :bool, :bool, :double, :pointer], :int32
199
240
  )
200
241
 
201
- # hegel_string_generator_text takes 12 arguments past ctx; the
202
- # last 6 (categories through exclude_characters) are the category
203
- # and explicit-character filter parameters this binding does not
204
- # wire up (see #string_generator_text).
205
242
  @hegel_string_generator_text_fn = bind(
206
243
  "hegel_string_generator_text",
207
244
  [:pointer, :uint64, :uint64, :pointer, :uint32, :uint32,
@@ -248,6 +285,28 @@ module Hegel
248
285
  [:pointer, :pointer, DatetimeStruct.by_value, DatetimeStruct.by_value, :pointer], :int32
249
286
  )
250
287
 
288
+ @hegel_grammar_builder_new_fn = bind(
289
+ "hegel_grammar_builder_new", [:pointer, StringStruct.by_value, :pointer], :int32
290
+ )
291
+ @hegel_grammar_builder_file_fn = bind(
292
+ "hegel_grammar_builder_file", [:pointer, :pointer, :size_t, :pointer], :int32
293
+ )
294
+ @hegel_grammar_builder_provide_fn = bind(
295
+ "hegel_grammar_builder_provide", [:pointer, :pointer, :size_t, :pointer], :int32
296
+ )
297
+ @hegel_grammar_builder_free_fn = bind("hegel_grammar_builder_free", [:pointer, :pointer], :int32)
298
+ @hegel_grammar_new_fn = bind(
299
+ "hegel_grammar_new",
300
+ [:pointer, :pointer, :pointer, :uint64, :uint32, :pointer, :pointer, :size_t, :pointer], :int32
301
+ )
302
+ @hegel_grammar_placeholder_fn = bind(
303
+ "hegel_grammar_placeholder", [:pointer, :pointer, StringStruct.by_value, :pointer], :int32
304
+ )
305
+ @hegel_grammar_free_fn = bind("hegel_grammar_free", [:pointer, :pointer], :int32)
306
+ @hegel_generate_grammar_fn = bind(
307
+ "hegel_generate_grammar", [:pointer, :pointer, :pointer, :uint64, :pointer], :int32
308
+ )
309
+
251
310
  LibHegel.with_context(self) { |ctx| LibHegel.warn_on_version_mismatch(self, ctx, io: io) }
252
311
  end
253
312
 
@@ -298,6 +357,31 @@ module Hegel
298
357
  out.read_pointer
299
358
  end
300
359
 
360
+ # Like #settings_new, but starts from the profile +name+, which still
361
+ # extends the default profile (except "base"). An unknown name returns
362
+ # HEGEL_E_INVALID_ARG.
363
+ def settings_new_for_profile(ctx, name)
364
+ out = FFI::MemoryPointer.new(:pointer)
365
+ code = @hegel_settings_new_for_profile_fn.call(ctx, string_view(name), out)
366
+ LibHegel.check!(self, ctx, code)
367
+ out.read_pointer
368
+ end
369
+
370
+ # Registers a snapshot of +s+ as the profile +name+ for the whole
371
+ # process. The caller keeps ownership of +s+.
372
+ def settings_register_profile(ctx, name, s)
373
+ code = @hegel_settings_register_profile_fn.call(ctx, string_view(name), s)
374
+ LibHegel.check!(self, ctx, code)
375
+ nil
376
+ end
377
+
378
+ # "" clears an earlier call.
379
+ def set_default_profile(ctx, name)
380
+ code = @hegel_set_default_profile_fn.call(ctx, string_view(name))
381
+ LibHegel.check!(self, ctx, code)
382
+ nil
383
+ end
384
+
301
385
  # No-op when +s+ is nil, matching hegel_settings_free's documented
302
386
  # no-op-on-NULL contract; not translated, for the same reason as
303
387
  # #context_free: the header documents this call as always returning
@@ -358,6 +442,14 @@ module Hegel
358
442
  nil
359
443
  end
360
444
 
445
+ def settings_set_test_location(ctx, s, file, begin_line, class_name, function)
446
+ code = @hegel_settings_set_test_location_fn.call(
447
+ ctx, s, string_view(file), begin_line, string_view(class_name), string_view(function)
448
+ )
449
+ LibHegel.check!(self, ctx, code)
450
+ nil
451
+ end
452
+
361
453
  # +phases+ is a bitwise OR of the HEGEL_PHASE_* constants.
362
454
  def settings_set_phases(ctx, s, phases)
363
455
  code = @hegel_settings_set_phases_fn.call(ctx, s, phases)
@@ -373,6 +465,44 @@ module Hegel
373
465
  nil
374
466
  end
375
467
 
468
+ def settings_set_stop_on_first_failure(ctx, s, yes)
469
+ code = @hegel_settings_set_stop_on_first_failure_fn.call(ctx, s, yes)
470
+ LibHegel.check!(self, ctx, code)
471
+ nil
472
+ end
473
+
474
+ def settings_set_print_blob(ctx, s, yes)
475
+ code = @hegel_settings_set_print_blob_fn.call(ctx, s, yes)
476
+ LibHegel.check!(self, ctx, code)
477
+ nil
478
+ end
479
+
480
+ # libhegel never prints the blob; the caller decides from the resolved value.
481
+ def settings_get_print_blob(ctx, s)
482
+ out = FFI::MemoryPointer.new(:bool)
483
+ code = @hegel_settings_get_print_blob_fn.call(ctx, s, out)
484
+ LibHegel.check!(self, ctx, code)
485
+ out.read_uint8 != 0
486
+ end
487
+
488
+ def settings_set_show_statistics(ctx, s, yes)
489
+ code = @hegel_settings_set_show_statistics_fn.call(ctx, s, yes)
490
+ LibHegel.check!(self, ctx, code)
491
+ nil
492
+ end
493
+
494
+ def settings_set_nondeterminism_strictness(ctx, s, strictness)
495
+ code = @hegel_settings_set_nondeterminism_strictness_fn.call(ctx, s, strictness)
496
+ LibHegel.check!(self, ctx, code)
497
+ nil
498
+ end
499
+
500
+ def settings_set_backend(ctx, s, backend)
501
+ code = @hegel_settings_set_backend_fn.call(ctx, s, backend)
502
+ LibHegel.check!(self, ctx, code)
503
+ nil
504
+ end
505
+
376
506
  # +settings+ can be freed by the caller as soon as this call returns:
377
507
  # the header documents that hegel_run_start copies the settings it is
378
508
  # given rather than borrowing them. callback and user_data are always
@@ -461,6 +591,18 @@ module Hegel
461
591
  nil
462
592
  end
463
593
 
594
+ def event(ctx, tc, label)
595
+ code = @hegel_event_fn.call(ctx, tc, string_view(label))
596
+ LibHegel.check!(self, ctx, code)
597
+ nil
598
+ end
599
+
600
+ def event_value(ctx, tc, label, value)
601
+ code = @hegel_event_value_fn.call(ctx, tc, value, string_view(label))
602
+ LibHegel.check!(self, ctx, code)
603
+ nil
604
+ end
605
+
464
606
  # Returns a caller-owned copy of the finished run's result, or raises
465
607
  # HEGEL_E_NOT_COMPLETE (via LibHegel.check!) if the run has not
466
608
  # finished. The header documents this copy as staying valid after
@@ -721,6 +863,16 @@ module Hegel
721
863
  out.read_int64
722
864
  end
723
865
 
866
+ # Puts back +variable_id+, which a consuming #pool_generate removed, so
867
+ # it can be drawn again. libhegel never does this by itself: a consumed
868
+ # id stays consumed until it is restored. An id that was not consumed
869
+ # raises Hegel::Error (HEGEL_E_INVALID_ARG).
870
+ def pool_restore(ctx, tc, pool, variable_id)
871
+ code = @hegel_pool_restore_fn.call(ctx, tc, pool, variable_id)
872
+ LibHegel.check!(self, ctx, code)
873
+ nil
874
+ end
875
+
724
876
  # No-op when +pool+ is nil, matching hegel_pool_free's documented
725
877
  # no-op-on-NULL contract; not translated, for the same reason as
726
878
  # #context_free.
@@ -731,6 +883,55 @@ module Hegel
731
883
  nil
732
884
  end
733
885
 
886
+ # Returns a caller-owned recursion handle, released separately with
887
+ # #recursion_free.
888
+ def new_recursion(ctx, tc, max_depth, max_leaves)
889
+ out = FFI::MemoryPointer.new(:pointer)
890
+ code = @hegel_new_recursion_fn.call(ctx, tc, max_depth, max_leaves, out)
891
+ LibHegel.check!(self, ctx, code)
892
+ out.read_pointer
893
+ end
894
+
895
+ # Returns true when the sub-value at +depth+ is a branch, false when
896
+ # it is a leaf.
897
+ def recursion_branch(ctx, tc, recursion, depth)
898
+ out = FFI::MemoryPointer.new(:bool)
899
+ code = @hegel_recursion_branch_fn.call(ctx, tc, recursion, depth, out)
900
+ LibHegel.check!(self, ctx, code)
901
+ out.read_uint8 != 0
902
+ end
903
+
904
+ # Returns false for HEGEL_E_RETRY: the attempt outgrew its leaf
905
+ # budget, and the caller unwinds it and calls #recursion_retry.
906
+ def recursion_leaf(ctx, tc, recursion)
907
+ code = @hegel_recursion_leaf_fn.call(ctx, tc, recursion)
908
+ retry_or_check!(ctx, code)
909
+ end
910
+
911
+ def recursion_retry(ctx, tc, recursion)
912
+ code = @hegel_recursion_retry_fn.call(ctx, tc, recursion)
913
+ LibHegel.check!(self, ctx, code)
914
+ nil
915
+ end
916
+
917
+ # Returns false for HEGEL_E_RETRY: the engine discarded the finished
918
+ # attempt, and the caller starts again from the root without calling
919
+ # #recursion_retry.
920
+ def recursion_finish(ctx, tc, recursion)
921
+ code = @hegel_recursion_finish_fn.call(ctx, tc, recursion)
922
+ retry_or_check!(ctx, code)
923
+ end
924
+
925
+ # No-op when +recursion+ is nil, matching hegel_recursion_free's
926
+ # documented no-op-on-NULL contract; not translated, for the same
927
+ # reason as #context_free.
928
+ def recursion_free(ctx, recursion)
929
+ # Removing this call only leaks native memory; Ruby has no way to
930
+ # observe the leak.
931
+ @hegel_recursion_free_fn.call(ctx, recursion) # mutineer:disable-line statement_removal
932
+ nil
933
+ end
934
+
734
935
  # Returns a caller-owned state-machine handle, released separately
735
936
  # with #state_machine_free. +rule_names+ and +invariant_names+ are
736
937
  # each an Array of Ruby Strings, packed into the hegel_string_t arrays
@@ -741,13 +942,16 @@ module Hegel
741
942
  # of labor #new_collection leaves to the caller for its own
742
943
  # min_size/max_size ordering.
743
944
  #
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
945
+ # +rule_weights+ holds one number per rule name; the engine rejects one
946
+ # that is not finite and positive.
947
+ #
948
+ # The machine is always sequential: one concurrency group and a
949
+ # concurrency of exactly 1, so the engine draws no concurrency level
950
+ # and the drawn level needs no reading. These
747
951
  # bindings drive the engine from one thread, and a concurrent machine
748
952
  # needs one worker thread per drawn level. For the same reason the
749
953
  # 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)
954
+ def new_state_machine(ctx, tc, rule_names, rule_weights, invariant_names, always_check, step_count)
751
955
  out = FFI::MemoryPointer.new(:pointer)
752
956
  out_concurrency = FFI::MemoryPointer.new(:int64)
753
957
  # _rule_pointers / _invariant_pointers are unread past the call
@@ -762,9 +966,11 @@ module Hegel
762
966
  # A fresh MemoryPointer is zero-filled, which puts every rule in
763
967
  # group 0.
764
968
  rule_groups = FFI::MemoryPointer.new(:int64, rule_names.size)
969
+ weights = FFI::MemoryPointer.new(:double, rule_weights.size)
970
+ weights.write_array_of_double(rule_weights.map(&:to_f))
765
971
 
766
972
  code = @hegel_new_state_machine_fn.call(
767
- ctx, tc, rule_names_ptr, rule_groups, nil, rule_names.size,
973
+ ctx, tc, rule_names_ptr, rule_groups, weights, rule_names.size,
768
974
  invariant_names_ptr, pack_flags(always_check), invariant_names.size,
769
975
  1, 1, step_count, out, out_concurrency
770
976
  )
@@ -841,17 +1047,19 @@ module Hegel
841
1047
  # Returns a caller-owned string generator handle, released
842
1048
  # separately with #string_generator_free (or scoped with
843
1049
  # Hegel::TestCase#with_text_generator). +categories+ and
844
- # +exclude_categories+ are always passed as NULL/0 below, and
845
- # +include_characters+ and +exclude_characters+ as empty strings:
846
- # this call only wires up the codepoint-range constraints of the
847
- # 12-argument bind; the category and explicit-character filter
848
- # arguments are a later generator's scope, layered on top of this
849
- # same bind.
850
- def string_generator_text(ctx, min_size:, max_size:, codec: nil, min_codepoint: 0, max_codepoint: 0xFFFFFFFF)
1050
+ # +exclude_categories+ are Arrays of Unicode general category names,
1051
+ # or nil for none: nil goes over as NULL, which is no restriction,
1052
+ # while an empty Array goes over as a non-NULL pointer with length
1053
+ # zero, which the header reads as an empty alphabet.
1054
+ def string_generator_text(ctx, min_size:, max_size:, codec: nil, min_codepoint: 0, max_codepoint: 0xFFFFFFFF,
1055
+ categories: nil, exclude_categories: nil, include_characters: "", exclude_characters: "")
851
1056
  out = FFI::MemoryPointer.new(:pointer)
1057
+ category_array, category_slots = optional_name_array(categories)
1058
+ exclude_array, exclude_slots = optional_name_array(exclude_categories)
852
1059
  code = @hegel_string_generator_text_fn.call(
853
1060
  ctx, min_size, max_size, optional_string(codec), min_codepoint, max_codepoint,
854
- nil, 0, nil, 0, StringStruct.new, StringStruct.new,
1061
+ category_array, category_slots.size, exclude_array, exclude_slots.size,
1062
+ string_view(include_characters), string_view(exclude_characters),
855
1063
  out
856
1064
  )
857
1065
  LibHegel.check!(self, ctx, code)
@@ -1087,8 +1295,93 @@ module Hegel
1087
1295
  [read_date(out[:date]), read_time(out[:time])]
1088
1296
  end
1089
1297
 
1298
+ # Returns a caller-owned grammar builder for +text+, a grammar in
1299
+ # Lark's syntax, released with #grammar_builder_free.
1300
+ def grammar_builder_new(ctx, text)
1301
+ out = FFI::MemoryPointer.new(:pointer)
1302
+ code = @hegel_grammar_builder_new_fn.call(ctx, string_view(text.encode(Encoding::UTF_8)), out)
1303
+ LibHegel.check!(self, ctx, code)
1304
+ out.read_pointer
1305
+ end
1306
+
1307
+ # The path of the +index+th file the grammar imports, or nil past the
1308
+ # end of the list. HEGEL_E_NOT_FOUND marks that end rather than an
1309
+ # error, so it is answered before LibHegel.check! sees it.
1310
+ def grammar_builder_file(ctx, builder, index)
1311
+ out = StringStruct.new
1312
+ code = @hegel_grammar_builder_file_fn.call(ctx, builder, index, out)
1313
+ return nil if code == HEGEL_E_NOT_FOUND
1314
+
1315
+ LibHegel.check!(self, ctx, code)
1316
+ read_string(out)
1317
+ end
1318
+
1319
+ # Supplies the +index+th file's +text+, or nil for a file that does
1320
+ # not exist, which the header keeps apart from an empty file.
1321
+ def grammar_builder_provide(ctx, builder, index, text)
1322
+ code = @hegel_grammar_builder_provide_fn.call(ctx, builder, index, optional_string(text))
1323
+ LibHegel.check!(self, ctx, code)
1324
+ nil
1325
+ end
1326
+
1327
+ def grammar_builder_free(ctx, builder)
1328
+ # Removing this call only leaks native memory; Ruby has no way to
1329
+ # observe the leak.
1330
+ @hegel_grammar_builder_free_fn.call(ctx, builder) # mutineer:disable-line statement_removal
1331
+ nil
1332
+ end
1333
+
1334
+ # Returns a caller-owned grammar compiled from +builder+, released
1335
+ # with #grammar_free. +start+ nil is the rule `start`; +alphabet+ is
1336
+ # an optional text generator handle. +names+ always goes over as a
1337
+ # non-NULL array, as hegel-rust passes it, even when it is empty.
1338
+ def grammar_new(ctx, builder, start, max_size, max_depth, alphabet, names)
1339
+ out = FFI::MemoryPointer.new(:pointer)
1340
+ names_array, names_slots = optional_name_array(names)
1341
+ code = @hegel_grammar_new_fn.call(
1342
+ ctx, builder, optional_string(start), max_size, max_depth, alphabet, names_array, names_slots.size, out
1343
+ )
1344
+ LibHegel.check!(self, ctx, code)
1345
+ out.read_pointer
1346
+ end
1347
+
1348
+ # The marker standing for the explicit terminal +name+ in the strings
1349
+ # drawn from +grammar+, copied out of the grammar's own buffer.
1350
+ def grammar_placeholder(ctx, grammar, name)
1351
+ out = StringStruct.new
1352
+ code = @hegel_grammar_placeholder_fn.call(ctx, grammar, string_view(name), out)
1353
+ LibHegel.check!(self, ctx, code)
1354
+ read_string(out)
1355
+ end
1356
+
1357
+ def grammar_free(ctx, grammar)
1358
+ # Removing this call only leaks native memory; Ruby has no way to
1359
+ # observe the leak.
1360
+ @hegel_grammar_free_fn.call(ctx, grammar) # mutineer:disable-line statement_removal
1361
+ nil
1362
+ end
1363
+
1364
+ # A drawn string of +grammar+ of at most +max_size+ characters outside
1365
+ # its markers, 0 meaning the grammar's own cap. Freed like
1366
+ # #generate_string's result.
1367
+ def generate_grammar(ctx, tc, grammar, max_size)
1368
+ out = StringStruct.new
1369
+ code = @hegel_generate_grammar_fn.call(ctx, tc, grammar, max_size, out)
1370
+ LibHegel.check!(self, ctx, code)
1371
+ read_string(out)
1372
+ ensure
1373
+ owned_string_free(ctx, out)
1374
+ end
1375
+
1090
1376
  private
1091
1377
 
1378
+ def retry_or_check!(ctx, code)
1379
+ return false if code == HEGEL_E_RETRY
1380
+
1381
+ LibHegel.check!(self, ctx, code)
1382
+ true
1383
+ end
1384
+
1092
1385
  # Resolves +symbol+ against @handle and wraps it as a callable
1093
1386
  # FFI::Function, the direct form #initialize's own comment explains
1094
1387
  # the choice of.
@@ -1177,6 +1470,16 @@ module Hegel
1177
1470
  [array, slots]
1178
1471
  end
1179
1472
 
1473
+ # #pack_name_array for an argument where nil and an empty Array mean
1474
+ # different things: nil stays NULL, and an empty Array becomes a
1475
+ # non-NULL pointer, which #pack_name_array alone would turn into NULL.
1476
+ def optional_name_array(names)
1477
+ return [nil, []] unless names
1478
+
1479
+ array, slots = pack_name_array(names)
1480
+ [array || FFI::MemoryPointer.new(StringStruct), slots]
1481
+ end
1482
+
1180
1483
  # Packs +flags+ (an Array of true/false) into a const bool * buffer,
1181
1484
  # one byte each. An empty Array packs to a zero-length buffer, which the
1182
1485
  # engine reads zero flags from.
@@ -30,6 +30,8 @@ module Hegel
30
30
  HEGEL_E_NOT_COMPLETE = -7
31
31
  HEGEL_E_INTERNAL = -8
32
32
  HEGEL_E_CONCURRENT_USE = -9
33
+ HEGEL_E_RETRY = -10
34
+ HEGEL_E_NOT_FOUND = -11
33
35
 
34
36
  # The hegel.h name for each code above, so a translated error message
35
37
  # names the code instead of leaving the reader to cross-reference the
@@ -44,7 +46,9 @@ module Hegel
44
46
  HEGEL_E_ALREADY_COMPLETE => "HEGEL_E_ALREADY_COMPLETE",
45
47
  HEGEL_E_NOT_COMPLETE => "HEGEL_E_NOT_COMPLETE",
46
48
  HEGEL_E_INTERNAL => "HEGEL_E_INTERNAL",
47
- HEGEL_E_CONCURRENT_USE => "HEGEL_E_CONCURRENT_USE"
49
+ HEGEL_E_CONCURRENT_USE => "HEGEL_E_CONCURRENT_USE",
50
+ HEGEL_E_RETRY => "HEGEL_E_RETRY",
51
+ HEGEL_E_NOT_FOUND => "HEGEL_E_NOT_FOUND"
48
52
  }.freeze
49
53
 
50
54
  # hegel_status_t, named from hegel.h's enum of the same name. Passed to
@@ -67,6 +71,15 @@ module Hegel
67
71
  HEGEL_VERBOSITY_VERBOSE = 2
68
72
  HEGEL_VERBOSITY_DEBUG = 3
69
73
 
74
+ # hegel_nondeterminism_strictness_t
75
+ HEGEL_NONDETERMINISM_QUIET = 0
76
+ HEGEL_NONDETERMINISM_WARN = 1
77
+ HEGEL_NONDETERMINISM_ERROR = 2
78
+
79
+ # hegel_backend_t
80
+ HEGEL_BACKEND_DEFAULT = 1
81
+ HEGEL_BACKEND_URANDOM = 2
82
+
70
83
  # hegel_phase_t, named from hegel.h's enum of the same name. A bitwise
71
84
  # OR of these is passed to hegel_settings_set_phases; the default is
72
85
  # HEGEL_PHASE_ALL.
@@ -108,7 +121,8 @@ module Hegel
108
121
  # responds to each name here.
109
122
  METHODS = %i[
110
123
  context_new context_free context_last_error version
111
- settings_new settings_free settings_set_test_cases settings_set_verbosity
124
+ settings_new settings_new_for_profile settings_register_profile set_default_profile
125
+ settings_free settings_set_test_cases settings_set_verbosity
112
126
  settings_set_seed settings_set_derandomize settings_set_database
113
127
  run_start next_test_case run_free test_case_free mark_complete
114
128
  generate_boolean generate_integer generate_integer_big
@@ -124,13 +138,24 @@ module Hegel
124
138
  generate_ipv4 generate_ipv6 generate_uuid
125
139
  generate_date generate_time generate_datetime
126
140
  settings_set_phases settings_set_suppress_health_check settings_set_report_multiple_failures
127
- settings_set_database_key
128
- target
129
- new_pool pool_add pool_generate pool_free
141
+ settings_set_database_key settings_set_stop_on_first_failure settings_set_print_blob
142
+ settings_get_print_blob settings_set_show_statistics settings_set_nondeterminism_strictness
143
+ settings_set_backend settings_set_test_location
144
+ target event event_value
145
+ new_pool pool_add pool_generate pool_restore pool_free
130
146
  new_state_machine state_machine_next_group state_machine_next_rule state_machine_rule_rejected
131
147
  state_machine_should_check_invariant state_machine_free
148
+ grammar_builder_new grammar_builder_file grammar_builder_provide grammar_builder_free
149
+ grammar_new grammar_placeholder grammar_free generate_grammar
150
+ new_recursion recursion_branch recursion_leaf recursion_retry recursion_finish recursion_free
132
151
  ].freeze
133
152
 
153
+ # label_combine answers from this table: a recursive generator builds a
154
+ # fresh branch generator at every node it draws, and each one combines
155
+ # the same few labels again. Measured on the 30-leaf shrink in
156
+ # test_recursive_engine.rb, hashing the labels was a third of the run.
157
+ COMBINED_LABELS = {}
158
+
134
159
  module_function
135
160
 
136
161
  # Runs the block with a context obtained from +impl.context_new+,
@@ -200,7 +225,7 @@ module Hegel
200
225
  end
201
226
 
202
227
  def label_combine(labels)
203
- fnv1a(labels.pack("Q<*").bytes)
228
+ COMBINED_LABELS[labels] ||= fnv1a(labels.pack("Q<*").bytes)
204
229
  end
205
230
 
206
231
  def fnv1a(bytes)
@@ -5,5 +5,5 @@ module Hegel
5
5
  # Hegel::VERSION (the gem's own release): hegel-go and hegel-typescript keep
6
6
  # the two separate too, so bumping the pinned engine can land as its own
7
7
  # commit without also releasing a new gem version.
8
- LIBHEGEL_VERSION = "0.47.2"
8
+ LIBHEGEL_VERSION = "0.48.0"
9
9
  end
data/lib/hegel/report.rb CHANGED
@@ -13,9 +13,10 @@ module Hegel
13
13
  # One failure's rendered ingredients. +test_cases+ and +discarded+ are
14
14
  # the generation phase's own counts up to that failure's first
15
15
  # appearance (see Hegel::Runner::GenerationStats), not the shrink
16
- # phase's. +entries+ is the [:draw, name, value] / [:note, message] list
17
- # Hegel::TestCase recorded on the stamped case that produced this
18
- # failure, in call order, still un-#inspect'd/un-#to_s'd (see
16
+ # phase's. +entries+ is the [:draw, name, value, indent] /
17
+ # [:note, message, indent] list Hegel::TestCase recorded on the stamped
18
+ # case that produced this failure, in call order, still
19
+ # un-#inspect'd/un-#to_s'd (see
19
20
  # Hegel::TestCase#record_draw and #note for why). +blob+ is the string
20
21
  # Hegel.test(reproduce_failure:) accepts to replay this same failure, or
21
22
  # nil when the engine produced none. +caveat+ is the engine's own account
@@ -41,10 +42,10 @@ module Hegel
41
42
  entries.map do |entry|
42
43
  next entry unless entry[0] == :draw
43
44
 
44
- _tag, name, value = entry
45
+ _tag, name, value, indent = entry
45
46
  if counts[name] > 1
46
47
  seen[name] += 1
47
- [:draw, "#{name}_#{seen[name]}", value]
48
+ [:draw, "#{name}_#{seen[name]}", value, indent]
48
49
  else
49
50
  entry
50
51
  end
@@ -60,17 +61,19 @@ module Hegel
60
61
  # the same. A :note shares the :draw lines' 2-space indent
61
62
  # deliberately: both belong to the same block, and a different indent
62
63
  # would read as a different kind of thing instead of the same
63
- # call-order list.
64
+ # call-order list. Each entry's own indent goes on top of those 2
65
+ # columns, which is how an iteration of Hegel::TestCase#repeat sits
66
+ # under its "// Repetition #N" line.
64
67
  def render_failure(failure)
65
68
  cases = "#{failure.test_cases} test #{(failure.test_cases == 1) ? "case" : "cases"}"
66
69
  lines = ["Falsified after #{cases} (#{failure.discarded} discarded):", ""]
67
70
  assign_names(failure.entries).each do |entry|
68
71
  if entry[0] == :draw
69
- _tag, name, value = entry
70
- lines << " #{name} = #{value.inspect}"
72
+ _tag, name, value, indent = entry
73
+ lines << " #{" " * indent}#{name} = #{value.inspect}"
71
74
  else
72
- _tag, message = entry
73
- lines << " #{message}"
75
+ _tag, message, indent = entry
76
+ lines << " #{" " * indent}#{message}"
74
77
  end
75
78
  end
76
79
  lines.push("", "note: #{failure.caveat}") if failure.caveat