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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +32 -9
- data/Rakefile +81 -0
- data/docs/README.md +9 -0
- data/docs/adr/0015-follow-the-hegeldev-interface-and-take-changes-upstream-first.md +101 -0
- data/docs/adr/0016-run-mutation-testing-with-mutineer.md +87 -0
- data/docs/adr/0017-raise-the-ruby-floor-to-3-4.md +43 -0
- data/docs/adr/0018-gate-mutation-testing-on-a-committed-baseline.md +74 -0
- data/docs/adr/0019-report-failures-from-the-cases-the-engine-stamps.md +70 -0
- data/docs/adr/0020-derive-span-labels-from-generator-names.md +64 -0
- data/docs/adr/0021-run-a-state-machine-in-rounds-with-its-own-step-count.md +59 -0
- data/docs/adr/0022-keep-microsecond-times-over-a-nanosecond-engine.md +45 -0
- data/docs/adr/0023-leave-unset-settings-to-the-engines-profile.md +50 -0
- data/docs/architecture.md +11 -6
- data/lib/hegel/draw_name.rb +3 -1
- data/lib/hegel/generator.rb +50 -9
- data/lib/hegel/generators.rb +134 -102
- data/lib/hegel/lib_hegel/real.rb +193 -94
- data/lib/hegel/lib_hegel.rb +30 -56
- data/lib/hegel/libhegel_version.rb +1 -1
- data/lib/hegel/report.rb +17 -8
- data/lib/hegel/runner.rb +153 -201
- data/lib/hegel/settings.rb +9 -14
- data/lib/hegel/state_machine.rb +15 -8
- data/lib/hegel/stateful/pool.rb +0 -2
- data/lib/hegel/stateful.rb +64 -36
- data/lib/hegel/syntax/methods.rb +3 -2
- data/lib/hegel/test_case.rb +22 -8
- data/lib/hegel/version.rb +1 -1
- data/lib/hegel.rb +11 -17
- data/lib/tasks/libhegel.rake +9 -2
- data/sig/hegel.rbs +58 -60
- data/skills/hegel-ruby/references/ruby/reference.md +96 -52
- metadata +11 -2
data/lib/hegel/lib_hegel/real.rb
CHANGED
|
@@ -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, :
|
|
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
|
-
@
|
|
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
|
-
[
|
|
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, :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
277
|
-
# raises the exception LibHegel.check! translates this
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
#
|
|
516
|
-
#
|
|
517
|
-
#
|
|
518
|
-
#
|
|
519
|
-
|
|
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 = @
|
|
563
|
+
code = @hegel_failure_caveat_fn.call(ctx, f, out)
|
|
532
564
|
LibHegel.check!(self, ctx, code)
|
|
533
|
-
out
|
|
565
|
+
nullable_out_string(out)
|
|
534
566
|
end
|
|
535
567
|
|
|
536
|
-
# Forcing has to agree with +p+. Measured against libhegel 0.
|
|
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
|
|
593
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
703
|
-
# +rule_names+ as non-empty (the
|
|
704
|
-
# to the caller, the same division
|
|
705
|
-
# to the caller for its own
|
|
706
|
-
|
|
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,
|
|
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
|
-
#
|
|
726
|
-
#
|
|
727
|
-
#
|
|
728
|
-
#
|
|
729
|
-
#
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
|
743
|
-
# not count toward the step budget. Raises HEGEL_E_INVALID_ARG
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1174
|
+
# second, nanosecond] Array.
|
|
1076
1175
|
def write_time(struct, value)
|
|
1077
|
-
hour, minute, second,
|
|
1176
|
+
hour, minute, second, nanosecond = value
|
|
1078
1177
|
struct[:hour] = hour
|
|
1079
1178
|
struct[:minute] = minute
|
|
1080
1179
|
struct[:second] = second
|
|
1081
|
-
struct[:
|
|
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
|
-
# :
|
|
1220
|
+
# :nanosecond fields.
|
|
1122
1221
|
def read_time(struct)
|
|
1123
|
-
[struct[:hour], struct[:minute], struct[:second], struct[:
|
|
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 #
|
|
1129
|
-
# documents as nullable, so both branches only need to be
|
|
1130
|
-
# once between the
|
|
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)
|