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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +182 -0
- data/README.ja.md +408 -0
- data/README.md +208 -102
- data/Rakefile +6 -3
- data/docs/README.md +9 -0
- data/docs/adr/0027-report-each-runs-test-location-from-the-callers-frame.md +63 -0
- data/docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md +91 -0
- data/docs/adr/0029-keep-notes-local-and-defer-run-error-kinds.md +72 -0
- data/docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md +48 -0
- data/docs/adr/0031-take-the-alphabet-options-from-hegel-rust.md +52 -0
- data/docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md +147 -0
- data/docs/adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md +80 -0
- data/docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md +120 -0
- data/docs/adr/0035-defer-concurrent-state-machines-and-the-engine-printer.md +136 -0
- data/docs/architecture.md +16 -1
- data/lib/hegel/errors.rb +24 -0
- data/lib/hegel/generator.rb +52 -25
- data/lib/hegel/generators.rb +715 -45
- data/lib/hegel/grammar_cache.rb +36 -0
- data/lib/hegel/lib_hegel/real.rb +320 -17
- data/lib/hegel/lib_hegel.rb +31 -6
- data/lib/hegel/libhegel_version.rb +1 -1
- data/lib/hegel/report.rb +13 -10
- data/lib/hegel/runner.rb +61 -27
- data/lib/hegel/settings.rb +92 -16
- data/lib/hegel/state_machine.rb +17 -4
- data/lib/hegel/stateful/pool.rb +52 -0
- data/lib/hegel/stateful.rb +2 -2
- data/lib/hegel/syntax/methods.rb +99 -15
- data/lib/hegel/test_case.rb +256 -35
- data/lib/hegel/test_location.rb +49 -0
- data/lib/hegel/version.rb +1 -1
- data/lib/hegel.rb +21 -5
- data/sig/hegel.rbs +295 -42
- data/skills/hegel-ruby/references/ruby/reference.md +463 -41
- 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
|
data/lib/hegel/lib_hegel/real.rb
CHANGED
|
@@ -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
|
-
#
|
|
745
|
-
#
|
|
746
|
-
#
|
|
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,
|
|
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
|
|
845
|
-
#
|
|
846
|
-
#
|
|
847
|
-
#
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
|
|
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.
|
data/lib/hegel/lib_hegel.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
129
|
-
|
|
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.
|
|
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
|
|
17
|
-
# Hegel::TestCase recorded on the stamped
|
|
18
|
-
# failure, in call order, still
|
|
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
|