hegeltest 0.1.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +264 -0
  6. data/Rakefile +19 -0
  7. data/docs/README.md +25 -0
  8. data/docs/adr/0001-bind-libhegel-through-fiddle.md +54 -0
  9. data/docs/adr/0002-ship-one-prebuilt-engine-per-platform-specific-gem.md +48 -0
  10. data/docs/adr/0003-publish-as-hegeltest-require-as-hegel.md +39 -0
  11. data/docs/adr/0004-expose-generators-through-a-mixin-with-keyword-options.md +42 -0
  12. data/docs/adr/0005-name-drawn-values-from-the-callers-source-with-prism.md +40 -0
  13. data/docs/adr/0006-verify-the-binding-in-seven-layers-with-full-coverage.md +51 -0
  14. data/docs/adr/0007-ship-a-thin-ruby-skill-shaped-for-donation.md +56 -0
  15. data/docs/adr/0008-revisit-the-binding-after-milestone-c-on-measurement.md +81 -0
  16. data/docs/adr/0009-turn-the-example-database-on-with-a-key.md +89 -0
  17. data/docs/adr/0010-declare-stateful-rules-with-a-class-macro.md +113 -0
  18. data/docs/adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md +83 -0
  19. data/docs/adr/0012-build-a-failure-origin-from-the-callers-own-frame.md +72 -0
  20. data/docs/adr/0013-bind-libhegel-through-the-ffi-gem.md +102 -0
  21. data/docs/architecture.md +182 -0
  22. data/lib/hegel/draw_name.rb +109 -0
  23. data/lib/hegel/errors.rb +47 -0
  24. data/lib/hegel/generator.rb +98 -0
  25. data/lib/hegel/generators.rb +865 -0
  26. data/lib/hegel/lib_hegel/real.rb +1149 -0
  27. data/lib/hegel/lib_hegel.rb +269 -0
  28. data/lib/hegel/libhegel_version.rb +9 -0
  29. data/lib/hegel/locate.rb +188 -0
  30. data/lib/hegel/report.rb +87 -0
  31. data/lib/hegel/runner.rb +464 -0
  32. data/lib/hegel/settings.rb +164 -0
  33. data/lib/hegel/state_machine.rb +89 -0
  34. data/lib/hegel/stateful/pool.rb +111 -0
  35. data/lib/hegel/stateful.rb +120 -0
  36. data/lib/hegel/syntax/methods.rb +173 -0
  37. data/lib/hegel/test_case.rb +523 -0
  38. data/lib/hegel/version.rb +5 -0
  39. data/lib/hegel.rb +92 -0
  40. data/lib/hegeltest.rb +7 -0
  41. data/lib/tasks/libhegel.rake +112 -0
  42. data/lib/tasks/platform_gems.rake +111 -0
  43. data/sig/hegel.rbs +563 -0
  44. data/skills/hegel-ruby/SKILL.md +30 -0
  45. data/skills/hegel-ruby/references/ruby/reference.md +1210 -0
  46. metadata +113 -0
@@ -0,0 +1,1149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+ require_relative "../lib_hegel"
5
+
6
+ module Hegel
7
+ module LibHegel
8
+ # Drives libhegel's C ABI through the ffi gem. Every other file works
9
+ # against the plain Ruby values and method calls this class exposes, so
10
+ # a future change to how the native call happens has exactly one file to
11
+ # change.
12
+ #
13
+ # Opens the library and binds every function once, in #initialize, each
14
+ # as its own FFI::Function held in an instance variable (see
15
+ # #initialize's own comment for why this binds one callable per function
16
+ # rather than attach_function's usual class-body DSL). Each call below
17
+ # reuses that already-bound function rather than re-resolving the
18
+ # symbol.
19
+ class Real
20
+ # hegel_date_t / hegel_time_t / hegel_datetime_t, transcribed field
21
+ # for field from hegel-c/include/hegel.h. FFI::Struct computes each
22
+ # field's offset and the struct's own alignment padding from this
23
+ # layout -- the same C-ABI rules the compiled library was built
24
+ # against -- so there is no hand-computed byte offset here to drift
25
+ # from the header. Declared with .by_value in #initialize's own
26
+ # #bind calls, so a call marshals the whole struct rather than a
27
+ # pointer to it, including on an ABI that passes a struct this size
28
+ # by hidden reference instead of in registers: classifying that is
29
+ # libffi's own job once .by_value asks for it. See docs/adr/0013 for
30
+ # the degenerate-draw check this layout is measured against.
31
+ class DateStruct < FFI::Struct
32
+ layout :year, :int32, :month, :uint8, :day, :uint8
33
+ end
34
+
35
+ class TimeStruct < FFI::Struct
36
+ layout :hour, :uint8, :minute, :uint8, :second, :uint8, :microsecond, :uint32
37
+ end
38
+
39
+ class DatetimeStruct < FFI::Struct
40
+ layout :date, DateStruct, :time, TimeStruct
41
+ end
42
+
43
+ # hegel_generate_string_result_t and hegel_generate_bytes_result_t are
44
+ # both `{ char *data; size_t len; }` in hegel-c/include/hegel.h --
45
+ # byte-for-byte the same layout under two names, one per element type
46
+ # the two calls draw. One Ruby struct mirrors both; #generate_string
47
+ # and #generate_bytes each pass a fresh instance as the out-parameter
48
+ # and read it back through their own method below.
49
+ class RawResultStruct < FFI::Struct
50
+ layout :data, :pointer, :len, :size_t
51
+ end
52
+
53
+ private_constant :DateStruct, :TimeStruct, :DatetimeStruct, :RawResultStruct
54
+
55
+ # Opens +path+ (default: Hegel::Locate.resolve) and binds the
56
+ # functions this boundary calls. Immediately after, opens a context
57
+ # of its own to compare the loaded engine's version against
58
+ # Hegel::LIBHEGEL_VERSION, warning on +io+ (default $stderr) on a
59
+ # mismatch; see LibHegel.warn_on_version_mismatch. +io+ exists so a
60
+ # test can capture the warning instead of writing to the real stderr.
61
+ #
62
+ # attach_function's usual DSL binds a fixed library, named at
63
+ # class-definition time, to methods it defines on a class or module
64
+ # body. This class instead takes +path+ as a constructor argument,
65
+ # resolved fresh per instance, so attach_function's own per-instance
66
+ # form is an anonymous module built fresh in #initialize. Measured on
67
+ # ffi 1.17.4, arm64-darwin, over a failing property that shrinks: 777
68
+ # ms for that form against 750 ms for binding each function directly
69
+ # off a resolved symbol, which is what #bind below does: FFI::DynamicLibrary.open gives a handle to resolve
70
+ # symbols against, and each call to #bind wraps one resolved symbol
71
+ # as a callable FFI::Function, stored in its own instance variable
72
+ # and invoked with #call by the method below it.
73
+ def initialize(path = Hegel::Locate.resolve, io: $stderr)
74
+ @handle = FFI::DynamicLibrary.open(path, FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_GLOBAL)
75
+
76
+ @hegel_context_new_fn = bind("hegel_context_new", [], :pointer)
77
+ @hegel_context_free_fn = bind("hegel_context_free", [:pointer], :int32)
78
+ @hegel_context_last_error_fn = bind("hegel_context_last_error", [:pointer], :pointer)
79
+ @hegel_version_fn = bind("hegel_version", [:pointer, :pointer], :int32)
80
+
81
+ @hegel_settings_new_fn = bind("hegel_settings_new", [:pointer, :pointer], :int32)
82
+ @hegel_settings_free_fn = bind("hegel_settings_free", [:pointer, :pointer], :int32)
83
+ @hegel_settings_set_test_cases_fn = bind(
84
+ "hegel_settings_set_test_cases", [:pointer, :pointer, :uint64], :int32
85
+ )
86
+ @hegel_settings_set_verbosity_fn = bind(
87
+ "hegel_settings_set_verbosity", [:pointer, :pointer, :uint32], :int32
88
+ )
89
+ @hegel_settings_set_seed_fn = bind(
90
+ "hegel_settings_set_seed", [:pointer, :pointer, :uint64, :bool], :int32
91
+ )
92
+ @hegel_settings_set_derandomize_fn = bind(
93
+ "hegel_settings_set_derandomize", [:pointer, :pointer, :bool], :int32
94
+ )
95
+ @hegel_settings_set_database_fn = bind(
96
+ "hegel_settings_set_database", [:pointer, :pointer, :string], :int32
97
+ )
98
+ @hegel_settings_set_stateful_step_count_fn = bind(
99
+ "hegel_settings_set_stateful_step_count", [:pointer, :pointer, :int64], :int32
100
+ )
101
+ @hegel_settings_set_report_multiple_failures_fn = bind(
102
+ "hegel_settings_set_report_multiple_failures", [:pointer, :pointer, :bool], :int32
103
+ )
104
+ @hegel_settings_set_database_key_fn = bind(
105
+ "hegel_settings_set_database_key", [:pointer, :pointer, :string], :int32
106
+ )
107
+ @hegel_settings_set_phases_fn = bind(
108
+ "hegel_settings_set_phases", [:pointer, :pointer, :uint32], :int32
109
+ )
110
+ @hegel_settings_set_suppress_health_check_fn = bind(
111
+ "hegel_settings_set_suppress_health_check", [:pointer, :pointer, :uint32], :int32
112
+ )
113
+
114
+ @hegel_run_start_fn = bind(
115
+ "hegel_run_start", [:pointer, :pointer, :pointer, :pointer, :pointer], :int32
116
+ )
117
+ @hegel_next_test_case_fn = bind("hegel_next_test_case", [:pointer, :pointer, :pointer], :int32)
118
+ @hegel_run_free_fn = bind("hegel_run_free", [:pointer, :pointer], :int32)
119
+ @hegel_test_case_free_fn = bind("hegel_test_case_free", [:pointer, :pointer], :int32)
120
+ @hegel_mark_complete_fn = bind("hegel_mark_complete", [:pointer, :pointer, :uint32, :string], :int32)
121
+ @hegel_target_fn = bind("hegel_target", [:pointer, :pointer, :double, :string], :int32)
122
+
123
+ @hegel_run_result_fn = bind("hegel_run_result", [:pointer, :pointer, :pointer], :int32)
124
+ @hegel_run_result_free_fn = bind("hegel_run_result_free", [:pointer, :pointer], :int32)
125
+ @hegel_run_result_status_fn = bind("hegel_run_result_status", [:pointer, :pointer, :pointer], :int32)
126
+ @hegel_run_result_error_fn = bind("hegel_run_result_error", [:pointer, :pointer, :pointer], :int32)
127
+ @hegel_run_result_failure_count_fn = bind(
128
+ "hegel_run_result_failure_count", [:pointer, :pointer, :pointer], :int32
129
+ )
130
+ @hegel_run_result_failure_fn = bind(
131
+ "hegel_run_result_failure", [:pointer, :pointer, :size_t, :pointer], :int32
132
+ )
133
+ @hegel_failure_free_fn = bind("hegel_failure_free", [:pointer, :pointer], :int32)
134
+ @hegel_failure_origin_fn = bind("hegel_failure_origin", [:pointer, :pointer, :pointer], :int32)
135
+ @hegel_failure_reproduction_blob_fn = bind(
136
+ "hegel_failure_reproduction_blob", [:pointer, :pointer, :pointer], :int32
137
+ )
138
+ @hegel_test_case_from_blob_fn = bind(
139
+ "hegel_test_case_from_blob", [:pointer, :pointer, :string, :pointer, :pointer, :pointer], :int32
140
+ )
141
+
142
+ @hegel_generate_boolean_fn = bind(
143
+ "hegel_generate_boolean", [:pointer, :pointer, :double, :bool, :bool, :pointer], :int32
144
+ )
145
+ @hegel_generate_integer_fn = bind(
146
+ "hegel_generate_integer", [:pointer, :pointer, :int64, :int64, :pointer], :int32
147
+ )
148
+ @hegel_generate_integer_big_fn = bind(
149
+ "hegel_generate_integer_big",
150
+ [:pointer, :pointer, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer], :int32
151
+ )
152
+
153
+ @hegel_start_span_fn = bind("hegel_start_span", [:pointer, :pointer, :uint64], :int32)
154
+ @hegel_stop_span_fn = bind("hegel_stop_span", [:pointer, :pointer, :bool], :int32)
155
+
156
+ @hegel_new_collection_fn = bind(
157
+ "hegel_new_collection", [:pointer, :pointer, :uint64, :uint64, :pointer], :int32
158
+ )
159
+ @hegel_collection_more_fn = bind("hegel_collection_more", [:pointer, :pointer, :pointer, :pointer], :int32)
160
+ @hegel_collection_reject_fn = bind(
161
+ "hegel_collection_reject", [:pointer, :pointer, :pointer, :string], :int32
162
+ )
163
+ @hegel_collection_free_fn = bind("hegel_collection_free", [:pointer, :pointer], :int32)
164
+
165
+ @hegel_new_pool_fn = bind("hegel_new_pool", [:pointer, :pointer, :pointer], :int32)
166
+ @hegel_pool_add_fn = bind("hegel_pool_add", [:pointer, :pointer, :pointer, :pointer], :int32)
167
+ @hegel_pool_generate_fn = bind(
168
+ "hegel_pool_generate", [:pointer, :pointer, :pointer, :bool, :pointer], :int32
169
+ )
170
+ @hegel_pool_free_fn = bind("hegel_pool_free", [:pointer, :pointer], :int32)
171
+
172
+ @hegel_new_state_machine_fn = bind(
173
+ "hegel_new_state_machine",
174
+ [:pointer, :pointer, :pointer, :size_t, :pointer, :size_t, :pointer], :int32
175
+ )
176
+ @hegel_state_machine_next_rule_fn = bind(
177
+ "hegel_state_machine_next_rule", [:pointer, :pointer, :pointer, :pointer], :int32
178
+ )
179
+ @hegel_state_machine_rule_rejected_fn = bind(
180
+ "hegel_state_machine_rule_rejected", [:pointer, :pointer, :pointer], :int32
181
+ )
182
+ @hegel_state_machine_free_fn = bind("hegel_state_machine_free", [:pointer, :pointer], :int32)
183
+
184
+ @hegel_generate_float_fn = bind(
185
+ "hegel_generate_float",
186
+ [:pointer, :pointer, :uint32, :double, :double, :bool, :bool, :bool, :bool, :double, :pointer], :int32
187
+ )
188
+
189
+ # hegel_string_generator_text takes 14 arguments past ctx; the
190
+ # last 8 (categories_len through exclude_characters_len) are the
191
+ # category and explicit-character filter parameters this task
192
+ # does not wire up (see #string_generator_text).
193
+ @hegel_string_generator_text_fn = bind(
194
+ "hegel_string_generator_text",
195
+ [:pointer, :uint64, :uint64, :string, :uint32, :uint32,
196
+ :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t,
197
+ :pointer],
198
+ :int32
199
+ )
200
+ @hegel_string_generator_free_fn = bind("hegel_string_generator_free", [:pointer, :pointer], :int32)
201
+ @hegel_generate_string_fn = bind(
202
+ "hegel_generate_string", [:pointer, :pointer, :pointer, :pointer], :int32
203
+ )
204
+ @hegel_generate_string_result_free_fn = bind(
205
+ "hegel_generate_string_result_free", [:pointer, :pointer], :int32
206
+ )
207
+
208
+ @hegel_generate_bytes_fn = bind(
209
+ "hegel_generate_bytes", [:pointer, :pointer, :uint64, :uint64, :pointer], :int32
210
+ )
211
+ @hegel_generate_bytes_result_free_fn = bind(
212
+ "hegel_generate_bytes_result_free", [:pointer, :pointer], :int32
213
+ )
214
+
215
+ @hegel_string_generator_regex_fn = bind(
216
+ "hegel_string_generator_regex", [:pointer, :string, :bool, :pointer, :pointer], :int32
217
+ )
218
+ @hegel_string_generator_email_fn = bind("hegel_string_generator_email", [:pointer, :pointer], :int32)
219
+ @hegel_string_generator_url_fn = bind("hegel_string_generator_url", [:pointer, :pointer], :int32)
220
+ @hegel_string_generator_domain_fn = bind(
221
+ "hegel_string_generator_domain", [:pointer, :uint64, :pointer], :int32
222
+ )
223
+
224
+ @hegel_generate_ipv4_fn = bind("hegel_generate_ipv4", [:pointer, :pointer, :pointer], :int32)
225
+ @hegel_generate_ipv6_fn = bind("hegel_generate_ipv6", [:pointer, :pointer, :pointer], :int32)
226
+ @hegel_generate_uuid_fn = bind(
227
+ "hegel_generate_uuid", [:pointer, :pointer, :uint8, :bool, :pointer], :int32
228
+ )
229
+
230
+ @hegel_generate_date_fn = bind(
231
+ "hegel_generate_date", [:pointer, :pointer, DateStruct.by_value, DateStruct.by_value, :pointer], :int32
232
+ )
233
+ @hegel_generate_time_fn = bind(
234
+ "hegel_generate_time", [:pointer, :pointer, TimeStruct.by_value, TimeStruct.by_value, :pointer], :int32
235
+ )
236
+ @hegel_generate_datetime_fn = bind(
237
+ "hegel_generate_datetime",
238
+ [:pointer, :pointer, DatetimeStruct.by_value, DatetimeStruct.by_value, :pointer], :int32
239
+ )
240
+
241
+ LibHegel.with_context(self) { |ctx| LibHegel.warn_on_version_mismatch(self, ctx, io: io) }
242
+ end
243
+
244
+ # hegel_context_new never returns NULL (guaranteed by the header), so
245
+ # the handle returned here is always live.
246
+ def context_new
247
+ @hegel_context_new_fn.call
248
+ end
249
+
250
+ # No-op when +ctx+ is nil: libhegel documents hegel_context_free as a
251
+ # no-op on NULL, and a Ruby nil marshals to a NULL pointer for a
252
+ # :pointer argument here, so no separate nil check is needed on this
253
+ # side. The result code is not translated: the header documents this
254
+ # call as always returning HEGEL_OK, so there is nothing to raise.
255
+ def context_free(ctx)
256
+ @hegel_context_free_fn.call(ctx)
257
+ nil
258
+ end
259
+
260
+ # Copies the message out of libhegel's own buffer into a Ruby String
261
+ # before returning, since the header documents that buffer as
262
+ # borrowed and invalidated by the next call taking the same context.
263
+ def context_last_error(ctx)
264
+ utf8(@hegel_context_last_error_fn.call(ctx))
265
+ end
266
+
267
+ # Returns the loaded engine's version string, or raises the
268
+ # exception LibHegel.check! translates this call's result code to.
269
+ def version(ctx)
270
+ out = FFI::MemoryPointer.new(:pointer)
271
+ code = @hegel_version_fn.call(ctx, out)
272
+ LibHegel.check!(self, ctx, code)
273
+ utf8(out.read_pointer)
274
+ end
275
+
276
+ # Returns a settings handle initialized with libhegel's defaults, or
277
+ # raises the exception LibHegel.check! translates this call's result
278
+ # code to.
279
+ def settings_new(ctx)
280
+ out = FFI::MemoryPointer.new(:pointer)
281
+ code = @hegel_settings_new_fn.call(ctx, out)
282
+ LibHegel.check!(self, ctx, code)
283
+ out.read_pointer
284
+ end
285
+
286
+ # No-op when +s+ is nil, matching hegel_settings_free's documented
287
+ # no-op-on-NULL contract; not translated, for the same reason as
288
+ # #context_free: the header documents this call as always returning
289
+ # HEGEL_OK.
290
+ def settings_free(ctx, s)
291
+ @hegel_settings_free_fn.call(ctx, s)
292
+ nil
293
+ end
294
+
295
+ def settings_set_test_cases(ctx, s, n)
296
+ code = @hegel_settings_set_test_cases_fn.call(ctx, s, n)
297
+ LibHegel.check!(self, ctx, code)
298
+ nil
299
+ end
300
+
301
+ def settings_set_verbosity(ctx, s, v)
302
+ code = @hegel_settings_set_verbosity_fn.call(ctx, s, v)
303
+ LibHegel.check!(self, ctx, code)
304
+ nil
305
+ end
306
+
307
+ def settings_set_seed(ctx, s, seed, has_seed)
308
+ code = @hegel_settings_set_seed_fn.call(ctx, s, seed, has_seed)
309
+ LibHegel.check!(self, ctx, code)
310
+ nil
311
+ end
312
+
313
+ def settings_set_derandomize(ctx, s, derandomize)
314
+ code = @hegel_settings_set_derandomize_fn.call(ctx, s, derandomize)
315
+ LibHegel.check!(self, ctx, code)
316
+ nil
317
+ end
318
+
319
+ # +database+ may be nil (libhegel's own default path) or a String,
320
+ # including "" to disable the database. Declared :string below, so a
321
+ # Ruby String marshals as a const char* to its bytes and nil marshals
322
+ # to NULL, both directly -- no separate pointer to build here.
323
+ def settings_set_database(ctx, s, database)
324
+ code = @hegel_settings_set_database_fn.call(ctx, s, database)
325
+ LibHegel.check!(self, ctx, code)
326
+ nil
327
+ end
328
+
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
+ def settings_set_report_multiple_failures(ctx, s, yes)
336
+ code = @hegel_settings_set_report_multiple_failures_fn.call(ctx, s, yes)
337
+ LibHegel.check!(self, ctx, code)
338
+ nil
339
+ end
340
+
341
+ # +key+ may be nil, which the header documents as clearing the key
342
+ # (the default); nil marshals to NULL for this :string argument, the
343
+ # same as #settings_set_database's own nilable database argument.
344
+ def settings_set_database_key(ctx, s, key)
345
+ code = @hegel_settings_set_database_key_fn.call(ctx, s, key)
346
+ LibHegel.check!(self, ctx, code)
347
+ nil
348
+ end
349
+
350
+ # +phases+ is a bitwise OR of the HEGEL_PHASE_* constants.
351
+ def settings_set_phases(ctx, s, phases)
352
+ code = @hegel_settings_set_phases_fn.call(ctx, s, phases)
353
+ LibHegel.check!(self, ctx, code)
354
+ nil
355
+ end
356
+
357
+ # +checks+ is a bitwise OR of the HEGEL_HC_* constants. Each call
358
+ # overwrites the previous suppressions, per the header.
359
+ def settings_set_suppress_health_check(ctx, s, checks)
360
+ code = @hegel_settings_set_suppress_health_check_fn.call(ctx, s, checks)
361
+ LibHegel.check!(self, ctx, code)
362
+ nil
363
+ end
364
+
365
+ # +settings+ can be freed by the caller as soon as this call returns:
366
+ # the header documents that hegel_run_start copies the settings it is
367
+ # given rather than borrowing them. callback and user_data are always
368
+ # NULL here, which the header documents as leaving libhegel's output
369
+ # on stderr; wiring a Ruby-backed callback is left to a later task.
370
+ def run_start(ctx, settings)
371
+ out = FFI::MemoryPointer.new(:pointer)
372
+ code = @hegel_run_start_fn.call(ctx, settings, nil, nil, out)
373
+ LibHegel.check!(self, ctx, code)
374
+ out.read_pointer
375
+ end
376
+
377
+ # Returns the next test case, or nil once the run has finished (the
378
+ # header documents *out_test_case as NULL at that point, with a
379
+ # HEGEL_OK result rather than an error).
380
+ def next_test_case(ctx, run)
381
+ out = FFI::MemoryPointer.new(:pointer)
382
+ code = @hegel_next_test_case_fn.call(ctx, run, out)
383
+ LibHegel.check!(self, ctx, code)
384
+ ptr = out.read_pointer
385
+ ptr.null? ? nil : ptr
386
+ end
387
+
388
+ # No-op when +run+ is nil, matching hegel_run_free's documented
389
+ # no-op-on-NULL contract; not translated, for the same reason as
390
+ # #context_free.
391
+ def run_free(ctx, run)
392
+ @hegel_run_free_fn.call(ctx, run)
393
+ nil
394
+ end
395
+
396
+ # No-op when +tc+ is nil, matching hegel_test_case_free's documented
397
+ # no-op-on-NULL contract; not translated, for the same reason as
398
+ # #context_free.
399
+ def test_case_free(ctx, tc)
400
+ @hegel_test_case_free_fn.call(ctx, tc)
401
+ nil
402
+ end
403
+
404
+ # +origin+ must be non-nil only when +status+ is
405
+ # HEGEL_STATUS_INTERESTING, per the header; this layer neither builds
406
+ # nor validates that string, only passes through what the caller
407
+ # supplies. nil marshals to NULL for this :string argument.
408
+ def mark_complete(ctx, tc, status, origin)
409
+ code = @hegel_mark_complete_fn.call(ctx, tc, status, origin)
410
+ LibHegel.check!(self, ctx, code)
411
+ nil
412
+ end
413
+
414
+ # Records a numeric observation under +label+ for libhegel's own
415
+ # hill-climbing between generation rounds. The header documents this
416
+ # as a no-op unless HEGEL_PHASE_TARGET is enabled (the default), and
417
+ # a label as recordable at most once per test case; neither is
418
+ # checked here, matching how this layer leaves every other
419
+ # argument-shape rule to the engine's own HEGEL_E_INVALID_ARG.
420
+ def target(ctx, tc, value, label)
421
+ code = @hegel_target_fn.call(ctx, tc, value, label)
422
+ LibHegel.check!(self, ctx, code)
423
+ nil
424
+ end
425
+
426
+ # Returns a caller-owned copy of the finished run's result, or raises
427
+ # HEGEL_E_NOT_COMPLETE (via LibHegel.check!) if the run has not
428
+ # finished. The header documents this copy as staying valid after
429
+ # #run_free, so +run+ can be freed as soon as this call returns; it
430
+ # must be released separately, exactly once, with #run_result_free.
431
+ def run_result(ctx, run)
432
+ out = FFI::MemoryPointer.new(:pointer)
433
+ code = @hegel_run_result_fn.call(ctx, run, out)
434
+ LibHegel.check!(self, ctx, code)
435
+ out.read_pointer
436
+ end
437
+
438
+ # No-op when +r+ is nil, matching hegel_run_result_free's documented
439
+ # no-op-on-NULL contract; not translated, for the same reason as
440
+ # #context_free.
441
+ def run_result_free(ctx, r)
442
+ @hegel_run_result_free_fn.call(ctx, r)
443
+ nil
444
+ end
445
+
446
+ # Returns the raw hegel_run_status_t value (HEGEL_RUN_STATUS_PASSED /
447
+ # _FAILED / _ERROR); this layer does not interpret it, matching how
448
+ # #mark_complete passes hegel_status_t values through unexamined.
449
+ def run_result_status(ctx, r)
450
+ out = FFI::MemoryPointer.new(:int32)
451
+ code = @hegel_run_result_status_fn.call(ctx, r, out)
452
+ LibHegel.check!(self, ctx, code)
453
+ out.read_int32
454
+ end
455
+
456
+ # Returns nil when the run completed normally (PASSED or FAILED),
457
+ # matching the header's documented NULL-on-success contract for this
458
+ # out-parameter, distinct from an empty-string message. See
459
+ # #nullable_out_string for the ownership note shared with
460
+ # #failure_reproduction_blob.
461
+ def run_result_error(ctx, r)
462
+ out = FFI::MemoryPointer.new(:pointer)
463
+ code = @hegel_run_result_error_fn.call(ctx, r, out)
464
+ LibHegel.check!(self, ctx, code)
465
+ nullable_out_string(out)
466
+ end
467
+
468
+ def run_result_failure_count(ctx, r)
469
+ out = FFI::MemoryPointer.new(:size_t)
470
+ code = @hegel_run_result_failure_count_fn.call(ctx, r, out)
471
+ LibHegel.check!(self, ctx, code)
472
+ out.read_uint64
473
+ end
474
+
475
+ # +index+ must be less than #run_result_failure_count's value, per the
476
+ # header. Returns a caller-owned failure handle, released separately
477
+ # with #failure_free. Measured against libhegel 0.32.5: an
478
+ # out-of-range +index+ comes back HEGEL_E_INVALID_ARG, even though
479
+ # the header's Returns line for this call names only HEGEL_OK.
480
+ def run_result_failure(ctx, r, index)
481
+ out = FFI::MemoryPointer.new(:pointer)
482
+ code = @hegel_run_result_failure_fn.call(ctx, r, index, out)
483
+ LibHegel.check!(self, ctx, code)
484
+ out.read_pointer
485
+ end
486
+
487
+ # No-op when +f+ is nil, matching hegel_failure_free's documented
488
+ # no-op-on-NULL contract; not translated, for the same reason as
489
+ # #context_free.
490
+ def failure_free(ctx, f)
491
+ @hegel_failure_free_fn.call(ctx, f)
492
+ nil
493
+ end
494
+
495
+ # Copies the origin string out of libhegel's own buffer before
496
+ # returning, since it is owned by the failure and only valid until
497
+ # #failure_free.
498
+ def failure_origin(ctx, f)
499
+ out = FFI::MemoryPointer.new(:pointer)
500
+ code = @hegel_failure_origin_fn.call(ctx, f, out)
501
+ LibHegel.check!(self, ctx, code)
502
+ utf8(out.read_pointer)
503
+ end
504
+
505
+ # Returns nil when libhegel produced no reproduction blob for this
506
+ # failure, matching the header's documented NULL-on-that-case
507
+ # contract. See #nullable_out_string for the shared ownership note.
508
+ def failure_reproduction_blob(ctx, f)
509
+ out = FFI::MemoryPointer.new(:pointer)
510
+ code = @hegel_failure_reproduction_blob_fn.call(ctx, f, out)
511
+ LibHegel.check!(self, ctx, code)
512
+ nullable_out_string(out)
513
+ end
514
+
515
+ # Replays +blob+ (from #failure_reproduction_blob) against +settings+
516
+ # with no run handle and no run loop involved, per the header.
517
+ # callback and user_data are always NULL here, for the same reason as
518
+ # #run_start. +blob+ is declared :string, the same as
519
+ # #settings_set_database's own const char* argument. Raises
520
+ # HEGEL_E_INVALID_ARG (via LibHegel.check!) for a blob that is
521
+ # corrupt, non-UTF-8, or from an incompatible Hegel version.
522
+ #
523
+ # A blob whose choices no longer match the caller's generators is a
524
+ # different case, and the header places it elsewhere: it "returns
525
+ # HEGEL_E_STOP_TEST from the draw that overruns", so the replay is
526
+ # built here and fails later, inside the body. Measured against
527
+ # 0.32.5, replaying a two-draw blob against a five-draw body builds
528
+ # fine and overruns at a draw.
529
+ def test_case_from_blob(ctx, settings, blob)
530
+ out = FFI::MemoryPointer.new(:pointer)
531
+ code = @hegel_test_case_from_blob_fn.call(ctx, settings, blob, nil, nil, out)
532
+ LibHegel.check!(self, ctx, code)
533
+ out.read_pointer
534
+ end
535
+
536
+ # Forcing has to agree with +p+. Measured against libhegel 0.32.5:
537
+ # forcing true at p = 0.0 and forcing false at p = 1.0 both come back
538
+ # HEGEL_E_INVALID_ARG ("generate_boolean: cannot force ..."), while
539
+ # forcing either way succeeds at any p between them. The header
540
+ # describes the two ends as yielding false and true without consuming
541
+ # entropy, and says nothing about what forcing does against them, so
542
+ # this is written down where a caller constructing a forced draw will
543
+ # look for it.
544
+ def generate_boolean(ctx, tc, p, forced, has_forced)
545
+ out = FFI::MemoryPointer.new(:bool)
546
+ code = @hegel_generate_boolean_fn.call(ctx, tc, p, forced, has_forced, out)
547
+ LibHegel.check!(self, ctx, code)
548
+ out.read_uint8 != 0
549
+ end
550
+
551
+ def generate_integer(ctx, tc, min_value, max_value)
552
+ out = FFI::MemoryPointer.new(:int64)
553
+ code = @hegel_generate_integer_fn.call(ctx, tc, min_value, max_value, out)
554
+ LibHegel.check!(self, ctx, code)
555
+ out.read_int64
556
+ end
557
+
558
+ # hegel_generate_integer_big, for bounds that do not fit int64_t (see
559
+ # Hegel::Generators::IntegerGenerator#do_draw, which dispatches here
560
+ # instead of #generate_integer only when a bound is outside that
561
+ # range). +min_value+/+max_value+ are encoded and the result decoded
562
+ # via LibHegel.encode_integer_le/.decode_integer_le, which own the
563
+ # two's-complement little-endian convention itself; this method only
564
+ # owns the buffer marshalling around it. min_value_ptr/max_value_ptr
565
+ # are declared :pointer, not :string: the encoded bytes routinely
566
+ # contain interior zero bytes (256 encodes as "\x00\x01"), which a
567
+ # NUL-terminated const char* argument would truncate. out_value's
568
+ # capacity is the larger of the two encoded bounds, per the header's
569
+ # "out_value_cap >= max(min_value_len, max_value_len) always
570
+ # succeeds"; the result is read back at its own reported
571
+ # out_value_len, not the buffer's full capacity, since
572
+ # decode_integer_le needs only that many bytes.
573
+ def generate_integer_big(ctx, tc, min_value, max_value)
574
+ min_bytes = LibHegel.encode_integer_le(min_value)
575
+ max_bytes = LibHegel.encode_integer_le(max_value)
576
+ min_value_ptr = bytes_to_pointer(min_bytes)
577
+ max_value_ptr = bytes_to_pointer(max_bytes)
578
+
579
+ cap = [min_bytes.bytesize, max_bytes.bytesize].max
580
+ out_value = FFI::MemoryPointer.new(cap)
581
+ out_value_len = FFI::MemoryPointer.new(:size_t)
582
+
583
+ code = @hegel_generate_integer_big_fn.call(
584
+ ctx, tc, min_value_ptr, min_bytes.bytesize, max_value_ptr, max_bytes.bytesize, out_value, cap,
585
+ out_value_len
586
+ )
587
+ LibHegel.check!(self, ctx, code)
588
+ len = out_value_len.read_uint64
589
+ LibHegel.decode_integer_le(out_value.read_bytes(len))
590
+ end
591
+
592
+ # Opens a span labelled +label+ (one of the HEGEL_LABEL_* constants,
593
+ # or a caller-defined value that avoids them). Must be paired with
594
+ # exactly one #stop_span call, per the header.
595
+ def start_span(ctx, tc, label)
596
+ code = @hegel_start_span_fn.call(ctx, tc, label)
597
+ LibHegel.check!(self, ctx, code)
598
+ nil
599
+ end
600
+
601
+ # Closes the most recently opened span. +discard+ true marks it
602
+ # rejected, so libhegel retries from before the span opened.
603
+ def stop_span(ctx, tc, discard)
604
+ code = @hegel_stop_span_fn.call(ctx, tc, discard)
605
+ LibHegel.check!(self, ctx, code)
606
+ nil
607
+ end
608
+
609
+ # Returns a caller-owned collection handle, released separately
610
+ # with #collection_free. Pass HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED
611
+ # as +max_size+ for no upper bound, per the header.
612
+ def new_collection(ctx, tc, min_size, max_size)
613
+ out = FFI::MemoryPointer.new(:pointer)
614
+ code = @hegel_new_collection_fn.call(ctx, tc, min_size, max_size, out)
615
+ LibHegel.check!(self, ctx, code)
616
+ out.read_pointer
617
+ end
618
+
619
+ # Returns whether libhegel wants another element; call in a loop,
620
+ # drawing the next element each time this is true, until it is
621
+ # false.
622
+ def collection_more(ctx, tc, collection)
623
+ out = FFI::MemoryPointer.new(:bool)
624
+ code = @hegel_collection_more_fn.call(ctx, tc, collection, out)
625
+ LibHegel.check!(self, ctx, code)
626
+ out.read_uint8 != 0
627
+ end
628
+
629
+ # Tells libhegel the last element +collection+ produced is invalid.
630
+ # +why+ is an optional human-readable reason (nil marshals to NULL
631
+ # for this :string argument, which the header allows); the header
632
+ # documents it as validated but reserved for future rejection
633
+ # diagnostics, unused today.
634
+ def collection_reject(ctx, tc, collection, why = nil)
635
+ code = @hegel_collection_reject_fn.call(ctx, tc, collection, why)
636
+ LibHegel.check!(self, ctx, code)
637
+ nil
638
+ end
639
+
640
+ # No-op when +collection+ is nil, matching hegel_collection_free's
641
+ # documented no-op-on-NULL contract; not translated, for the same
642
+ # reason as #context_free. Unlike every other *_free above, this
643
+ # call takes no test-case handle: the header documents a collection
644
+ # as independent of the test case and run it was created under.
645
+ def collection_free(ctx, collection)
646
+ @hegel_collection_free_fn.call(ctx, collection)
647
+ nil
648
+ end
649
+
650
+ # Returns a caller-owned pool handle, released separately with
651
+ # #pool_free. A pool tracks a set of variable ids libhegel can draw
652
+ # from and shrink over -- mostly used for stateful testing, where a
653
+ # rule acts on a value a previous rule generated; the caller keeps
654
+ # its own mapping from variable id to that value, per the header.
655
+ def new_pool(ctx, tc)
656
+ out = FFI::MemoryPointer.new(:pointer)
657
+ code = @hegel_new_pool_fn.call(ctx, tc, out)
658
+ LibHegel.check!(self, ctx, code)
659
+ out.read_pointer
660
+ end
661
+
662
+ # Returns a fresh variable id for the caller to associate with the
663
+ # value it just generated. The header documents the id as drawn
664
+ # from +tc+'s stream and recorded by value, not by pool position, so
665
+ # it stays stable across shrinking: deleting an earlier addition
666
+ # never renumbers the survivors.
667
+ def pool_add(ctx, tc, pool)
668
+ out = FFI::MemoryPointer.new(:int64)
669
+ code = @hegel_pool_add_fn.call(ctx, tc, pool, out)
670
+ LibHegel.check!(self, ctx, code)
671
+ out.read_int64
672
+ end
673
+
674
+ # Returns a variable id libhegel chose from +pool+ (and can shrink
675
+ # which one it chose). +consume+ true removes the drawn variable
676
+ # from the pool; false leaves it. LibHegel.check! already translates
677
+ # HEGEL_E_ASSUME -- what the header documents this call returning
678
+ # when +pool+ holds no variables -- to Hegel::AssumeFailed, the same
679
+ # translation every other assumption failure gets, so no extra code
680
+ # is needed here for that case; pinned by
681
+ # test_real_pool_generate_on_an_empty_pool_raises_assume_failed in
682
+ # test/hegel/test_lib_hegel.rb.
683
+ def pool_generate(ctx, tc, pool, consume)
684
+ out = FFI::MemoryPointer.new(:int64)
685
+ code = @hegel_pool_generate_fn.call(ctx, tc, pool, consume, out)
686
+ LibHegel.check!(self, ctx, code)
687
+ out.read_int64
688
+ end
689
+
690
+ # No-op when +pool+ is nil, matching hegel_pool_free's documented
691
+ # no-op-on-NULL contract; not translated, for the same reason as
692
+ # #context_free.
693
+ def pool_free(ctx, pool)
694
+ @hegel_pool_free_fn.call(ctx, pool)
695
+ nil
696
+ end
697
+
698
+ # Returns a caller-owned state-machine handle, released separately
699
+ # with #state_machine_free. +rule_names+ and +invariant_names+ are
700
+ # each an Array of Ruby Strings, packed into the const char *const *
701
+ # arguments hegel_new_state_machine expects by #pack_name_array; see
702
+ # that method's own comment for how and why. Validating
703
+ # +rule_names+ as non-empty (the header's own requirement) is left
704
+ # to the caller, the same division of labor #new_collection leaves
705
+ # to the caller for its own min_size/max_size ordering.
706
+ def new_state_machine(ctx, tc, rule_names, invariant_names)
707
+ out = FFI::MemoryPointer.new(:pointer)
708
+ # _rule_pointers / _invariant_pointers are unread past the call
709
+ # below, the same shape #generate_integer_big's own
710
+ # min_value_ptr/max_value_ptr already have; keeping them as local
711
+ # variables here, not discarded inside #pack_name_array, is what
712
+ # keeps the native buffers each one owns live through the call.
713
+ # The leading underscore tells the linter that on purpose, the
714
+ # same way it would for a block argument the block never reads.
715
+ rule_names_ptr, _rule_pointers = pack_name_array(rule_names)
716
+ invariant_names_ptr, _invariant_pointers = pack_name_array(invariant_names)
717
+
718
+ code = @hegel_new_state_machine_fn.call(
719
+ ctx, tc, rule_names_ptr, rule_names.size, invariant_names_ptr, invariant_names.size, out
720
+ )
721
+ LibHegel.check!(self, ctx, code)
722
+ out.read_pointer
723
+ end
724
+
725
+ # Returns the index (in 0...num_rules) of the next stateful-testing
726
+ # rule to run, or HEGEL_STATE_MACHINE_DONE (-1) once +state_machine+'s
727
+ # step budget is exhausted -- returned as the raw sentinel value, not
728
+ # translated to nil. Unlike #next_test_case's out-parameter, which is
729
+ # NULL (no value) at the equivalent boundary, the header documents
730
+ # this out-parameter as holding a real value, -1, at that point; a
731
+ # caller comparing against HEGEL_STATE_MACHINE_DONE is the layer that
732
+ # should decide what that value means, the same way #run_result_status
733
+ # hands back its raw HEGEL_RUN_STATUS_* value unexamined.
734
+ def state_machine_next_rule(ctx, tc, state_machine)
735
+ out = FFI::MemoryPointer.new(:int64)
736
+ code = @hegel_state_machine_next_rule_fn.call(ctx, tc, state_machine, out)
737
+ LibHegel.check!(self, ctx, code)
738
+ out.read_int64
739
+ end
740
+
741
+ # Reports the rule most recently returned by #state_machine_next_rule
742
+ # as rejected (an assumption failed before it completed), so it does
743
+ # not count toward the step budget. Raises HEGEL_E_INVALID_ARG (via
744
+ # LibHegel.check!, translated to Hegel::Error) when no rule is
745
+ # outstanding, per the header.
746
+ def state_machine_rule_rejected(ctx, tc, state_machine)
747
+ code = @hegel_state_machine_rule_rejected_fn.call(ctx, tc, state_machine)
748
+ LibHegel.check!(self, ctx, code)
749
+ nil
750
+ end
751
+
752
+ # No-op when +state_machine+ is nil, matching
753
+ # hegel_state_machine_free's documented no-op-on-NULL contract; not
754
+ # translated, for the same reason as #context_free.
755
+ def state_machine_free(ctx, state_machine)
756
+ @hegel_state_machine_free_fn.call(ctx, state_machine)
757
+ nil
758
+ end
759
+
760
+ # Returns a drawn double. +smallest_nonzero_magnitude+ must be
761
+ # positive and finite; pass
762
+ # HEGEL_FLOAT64_SMALLEST_NONZERO_MAGNITUDE_UNRESTRICTED for width
763
+ # 64 with no restriction, per the header.
764
+ def generate_float(ctx, tc, width, min_value, max_value, allow_nan, allow_infinity, exclude_min, exclude_max,
765
+ smallest_nonzero_magnitude)
766
+ out = FFI::MemoryPointer.new(:double)
767
+ code = @hegel_generate_float_fn.call(ctx, tc, width, min_value, max_value, allow_nan, allow_infinity,
768
+ exclude_min, exclude_max, smallest_nonzero_magnitude, out)
769
+ LibHegel.check!(self, ctx, code)
770
+ out.read_double
771
+ end
772
+
773
+ # Returns a caller-owned string generator handle, released
774
+ # separately with #string_generator_free (or scoped with
775
+ # Hegel::TestCase#with_text_generator). +categories+,
776
+ # +exclude_categories+, +include_characters+, and
777
+ # +exclude_characters+ are always passed as NULL/0 below: this call
778
+ # only wires up the codepoint-range constraints of the 14-argument
779
+ # bind; the category and explicit-character filter arguments are a
780
+ # later generator's scope, layered on top of this same bind.
781
+ def string_generator_text(ctx, min_size:, max_size:, codec: nil, min_codepoint: 0, max_codepoint: 0xFFFFFFFF)
782
+ out = FFI::MemoryPointer.new(:pointer)
783
+ code = @hegel_string_generator_text_fn.call(
784
+ ctx, min_size, max_size, codec, min_codepoint, max_codepoint,
785
+ nil, 0, nil, 0, nil, 0, nil, 0,
786
+ out
787
+ )
788
+ LibHegel.check!(self, ctx, code)
789
+ out.read_pointer
790
+ end
791
+
792
+ # No-op when +generator+ is nil, matching
793
+ # hegel_string_generator_free's documented no-op-on-NULL contract;
794
+ # not translated, for the same reason as #context_free.
795
+ def string_generator_free(ctx, generator)
796
+ @hegel_string_generator_free_fn.call(ctx, generator)
797
+ nil
798
+ end
799
+
800
+ # Returns a drawn String, force-encoded as UTF-8 (this codec's
801
+ # alphabet), copying exactly the returned +len+ bytes rather than
802
+ # reading a NUL-terminated buffer: the header documents it as not
803
+ # NUL-terminated and possibly containing interior NUL bytes, since
804
+ # the drawn alphabet can include U+0000.
805
+ #
806
+ # The native buffer is released in +ensure+ via
807
+ # #generate_string_result_free, called directly from here rather
808
+ # than left to the caller: nothing outside this file may hold or
809
+ # read the raw struct (ffi is confined to this file), so unlike
810
+ # #new_collection or #string_generator_text, the freeable handle
811
+ # here never leaves this method. Freeing is safe even when the draw
812
+ # above raised: +out+ starts zero-filled (FFI::Struct.new allocates
813
+ # cleared memory) and libhegel only writes into it on success, so
814
+ # the struct #generate_string_result_free sees here is always
815
+ # either a completed draw or the all-zero state the header
816
+ # documents as already safe to free.
817
+ def generate_string(ctx, tc, generator)
818
+ out = RawResultStruct.new
819
+ code = @hegel_generate_string_fn.call(ctx, tc, generator, out)
820
+ LibHegel.check!(self, ctx, code)
821
+ out[:data].read_bytes(out[:len]).force_encoding(Encoding::UTF_8)
822
+ ensure
823
+ generate_string_result_free(ctx, out)
824
+ end
825
+
826
+ # No-op when +result+ is nil, matching
827
+ # hegel_generate_string_result_free's documented no-op-on-NULL
828
+ # contract (also safe on an already-freed, zeroed struct); not
829
+ # translated, for the same reason as #context_free.
830
+ def generate_string_result_free(ctx, result)
831
+ @hegel_generate_string_result_free_fn.call(ctx, result)
832
+ nil
833
+ end
834
+
835
+ # Returns drawn bytes as a String, read via +len+ the same way
836
+ # #generate_string reads its own out-parameter: the header gives no
837
+ # NUL-termination guarantee for this buffer either, so the length is
838
+ # what makes the copy exact.
839
+ #
840
+ # Unlike #generate_string, the result is never force-encoded.
841
+ # hegel_generate_bytes_result_t is documented as a byte buffer, not
842
+ # text, and FFI::Pointer#read_bytes already returns ASCII-8BIT
843
+ # (Encoding::BINARY), which is the encoding a byte string belongs in.
844
+ #
845
+ # The native buffer is released in +ensure+ via
846
+ # #generate_bytes_result_free, for the same reason #generate_string
847
+ # frees its own result from inside this file: ffi is confined to
848
+ # this file, so the freeable handle never leaves this method. Freeing
849
+ # is safe even when the draw above raised, for the same
850
+ # zero-filled-allocation reason documented on #generate_string: the
851
+ # header documents hegel_generate_bytes_result_free as safe on an
852
+ # already-freed (zeroed) struct too.
853
+ def generate_bytes(ctx, tc, min_size, max_size)
854
+ out = RawResultStruct.new
855
+ code = @hegel_generate_bytes_fn.call(ctx, tc, min_size, max_size, out)
856
+ LibHegel.check!(self, ctx, code)
857
+ out[:data].read_bytes(out[:len])
858
+ ensure
859
+ generate_bytes_result_free(ctx, out)
860
+ end
861
+
862
+ # No-op when +result+ is nil, matching
863
+ # hegel_generate_bytes_result_free's documented no-op-on-NULL
864
+ # contract (also safe on an already-freed, zeroed struct); not
865
+ # translated, for the same reason as #context_free.
866
+ def generate_bytes_result_free(ctx, result)
867
+ @hegel_generate_bytes_result_free_fn.call(ctx, result)
868
+ nil
869
+ end
870
+
871
+ # Returns a caller-owned string generator handle matching +pattern+
872
+ # (Python re syntax), released the same way as #string_generator_text:
873
+ # with #string_generator_free. +alphabet+ is an optional string
874
+ # generator handle (built via #string_generator_text, scoped with
875
+ # Hegel::TestCase#with_text_generator) whose character set constrains the
876
+ # padding and wildcard characters; nil (the default) marshals to NULL,
877
+ # the header's documented "no particular alphabet" case.
878
+ def string_generator_regex(ctx, pattern, fullmatch, alphabet = nil)
879
+ out = FFI::MemoryPointer.new(:pointer)
880
+ code = @hegel_string_generator_regex_fn.call(ctx, pattern, fullmatch, alphabet, out)
881
+ LibHegel.check!(self, ctx, code)
882
+ out.read_pointer
883
+ end
884
+
885
+ # Returns a caller-owned string generator handle producing RFC
886
+ # 5321/5322 email addresses, released the same way as
887
+ # #string_generator_text.
888
+ def string_generator_email(ctx)
889
+ out = FFI::MemoryPointer.new(:pointer)
890
+ code = @hegel_string_generator_email_fn.call(ctx, out)
891
+ LibHegel.check!(self, ctx, code)
892
+ out.read_pointer
893
+ end
894
+
895
+ # Returns a caller-owned string generator handle producing RFC 3986
896
+ # http/https URLs, released the same way as #string_generator_text.
897
+ def string_generator_url(ctx)
898
+ out = FFI::MemoryPointer.new(:pointer)
899
+ code = @hegel_string_generator_url_fn.call(ctx, out)
900
+ LibHegel.check!(self, ctx, code)
901
+ out.read_pointer
902
+ end
903
+
904
+ # Returns a caller-owned string generator handle producing
905
+ # fully-qualified domain names, released the same way as
906
+ # #string_generator_text. +max_length+ is the total FQDN length; the
907
+ # header documents it as valid in 4..=255. This layer does not
908
+ # validate that range itself, only translates the
909
+ # HEGEL_E_INVALID_ARG the engine returns outside it, the same
910
+ # division of labor #settings_set_database and every other setter
911
+ # above already follows.
912
+ def string_generator_domain(ctx, max_length)
913
+ out = FFI::MemoryPointer.new(:pointer)
914
+ code = @hegel_string_generator_domain_fn.call(ctx, max_length, out)
915
+ LibHegel.check!(self, ctx, code)
916
+ out.read_pointer
917
+ end
918
+
919
+ # hegel_generate_ipv4 writes into a caller-supplied fixed-length
920
+ # buffer instead of handing back a pointer through an out-parameter,
921
+ # unlike every generate_* call above: the header documents out_bytes
922
+ # as the address's 4 network-order bytes with no separate length to
923
+ # read, so the buffer's own size is the contract instead of a
924
+ # trailing len field. IPAddr conversion is left to the generator
925
+ # built on top of this call; this layer returns the raw bytes.
926
+ def generate_ipv4(ctx, tc)
927
+ out = FFI::MemoryPointer.new(4)
928
+ code = @hegel_generate_ipv4_fn.call(ctx, tc, out)
929
+ LibHegel.check!(self, ctx, code)
930
+ out.read_bytes(4)
931
+ end
932
+
933
+ # Same fixed-buffer shape as #generate_ipv4, sized for the header's
934
+ # documented 16 network-order bytes.
935
+ def generate_ipv6(ctx, tc)
936
+ out = FFI::MemoryPointer.new(16)
937
+ code = @hegel_generate_ipv6_fn.call(ctx, tc, out)
938
+ LibHegel.check!(self, ctx, code)
939
+ out.read_bytes(16)
940
+ end
941
+
942
+ # hegel_generate_uuid, returning the drawn UUID's 16 raw bytes.
943
+ # +has_version+ true forces the RFC 4122 version nibble to +version+
944
+ # (0..15) and the variant nibble to the RFC 4122 variant, per the
945
+ # header; +version+ is ignored (but still marshalled; pass 0) when
946
+ # +has_version+ is false. Converting the raw bytes to the standard
947
+ # 8-4-4-4-12 hex String is left to Hegel::Generators::UuidsGenerator,
948
+ # the same division of labor #generate_ipv4/#generate_ipv6 already
949
+ # follow for their own byte-to-address conversion. An out-of-range
950
+ # +version+ is not checked here: measured against libhegel 0.32.5, the
951
+ # engine itself returns HEGEL_E_INVALID_ARG for one, which
952
+ # LibHegel.check! already translates.
953
+ def generate_uuid(ctx, tc, version, has_version)
954
+ out = FFI::MemoryPointer.new(16)
955
+ code = @hegel_generate_uuid_fn.call(ctx, tc, version, has_version, out)
956
+ LibHegel.check!(self, ctx, code)
957
+ out.read_bytes(16)
958
+ end
959
+
960
+ # hegel_generate_date. +min_value+/+max_value+ are each a
961
+ # [year, month, day] Array, written by #date_struct into a
962
+ # DateStruct passed .by_value (see the comment on that class, above
963
+ # #initialize). Returns a [year, month, day] Array, the field-level
964
+ # shape Hegel::Generators::DatesGenerator builds its own Date from --
965
+ # the same division of labor #generate_ipv4/#generate_uuid already
966
+ # follow, returning raw values for a generator one layer up to turn
967
+ # into the caller-facing type. This layer does not validate
968
+ # year/month/day itself: measured against libhegel 0.32.5, an
969
+ # invalid date (month 13, say) already comes back
970
+ # HEGEL_E_INVALID_ARG, translated by LibHegel.check! below, even
971
+ # though the header's Returns line for this call names only
972
+ # HEGEL_OK/HEGEL_E_STOP_TEST. #run_result_failure's own comment
973
+ # records the same measured behaviour for a different call.
974
+ def generate_date(ctx, tc, min_value, max_value)
975
+ out = DateStruct.new
976
+ code = @hegel_generate_date_fn.call(ctx, tc, date_struct(min_value), date_struct(max_value), out)
977
+ LibHegel.check!(self, ctx, code)
978
+ read_date(out)
979
+ end
980
+
981
+ # hegel_generate_time. +min_value+/+max_value+ are each an
982
+ # [hour, minute, second, microsecond] Array; same struct-passing,
983
+ # return shape, and validation division of labor as #generate_date
984
+ # above, for Hegel::Generators::TimesGenerator.
985
+ def generate_time(ctx, tc, min_value, max_value)
986
+ out = TimeStruct.new
987
+ code = @hegel_generate_time_fn.call(ctx, tc, time_struct(min_value), time_struct(max_value), out)
988
+ LibHegel.check!(self, ctx, code)
989
+ read_time(out)
990
+ end
991
+
992
+ # hegel_generate_datetime. +min_date+/+max_date+ are each a
993
+ # [year, month, day] Array; +min_time+/+max_time+ are each an
994
+ # [hour, minute, second, microsecond] Array -- hegel_datetime_t is a
995
+ # hegel_date_t followed by a hegel_time_t (see DatetimeStruct's own
996
+ # layout, above #initialize). Returns a
997
+ # [[year, month, day], [hour, minute, second, microsecond]] pair, for
998
+ # Hegel::Generators::DatetimesGenerator to build its own Time from.
999
+ def generate_datetime(ctx, tc, min_date, min_time, max_date, max_time)
1000
+ out = DatetimeStruct.new
1001
+ code = @hegel_generate_datetime_fn.call(
1002
+ ctx, tc, datetime_struct(min_date, min_time), datetime_struct(max_date, max_time), out
1003
+ )
1004
+ LibHegel.check!(self, ctx, code)
1005
+ [read_date(out[:date]), read_time(out[:time])]
1006
+ end
1007
+
1008
+ private
1009
+
1010
+ # Resolves +symbol+ against @handle and wraps it as a callable
1011
+ # FFI::Function, the direct form #initialize's own comment explains
1012
+ # the choice of.
1013
+ def bind(symbol, arg_types, ret_type)
1014
+ FFI::Function.new(ret_type, arg_types, @handle.find_function(symbol))
1015
+ end
1016
+
1017
+ # Copies +bytes+ into a freshly allocated buffer, for
1018
+ # #generate_integer_big's min_value/max_value: a const uint8_t*
1019
+ # argument libhegel reads from, the opposite direction of an
1020
+ # out-parameter buffer (which libhegel writes into, and the caller
1021
+ # then reads).
1022
+ def bytes_to_pointer(bytes)
1023
+ ptr = FFI::MemoryPointer.new(bytes.bytesize)
1024
+ ptr.put_bytes(0, bytes)
1025
+ ptr
1026
+ end
1027
+
1028
+ # Packs +names+ (an Array of Ruby Strings) into a const char *const *
1029
+ # for #new_state_machine's rule_names/invariant_names arguments: one
1030
+ # address per name, written into a single buffer via
1031
+ # write_array_of_pointer.
1032
+ #
1033
+ # Returns [pointer, kept_alive]. FFI::MemoryPointer.from_string
1034
+ # copies each name into a fresh native buffer that Ruby object owns
1035
+ # -- unlike a pointer built straight off a Ruby String's own bytes,
1036
+ # nothing here depends on the original +names+ Strings staying live
1037
+ # -- so what must stay live until the native call reads them is the
1038
+ # per-name FFI::MemoryPointer array itself; write_array_of_pointer
1039
+ # only copies the addresses those objects report, not a reference to
1040
+ # the objects, so the caller keeping +kept_alive+ as a live local for
1041
+ # the length of the call is what keeps the buffers each one owns
1042
+ # from being garbage-collected first.
1043
+ #
1044
+ # Empty +names+ maps to [nil, []] -- NULL and (by the caller passing
1045
+ # names.size, 0) the header's own contract for invariant_names' "no
1046
+ # invariants" case. #new_state_machine reuses this same packing for
1047
+ # rule_names too, whose own documented non-empty requirement this
1048
+ # method does not enforce; that validation belongs one layer up, in
1049
+ # a caller that can raise Hegel::Error with a message naming the
1050
+ # factory method, not in a method whose only job is packing an
1051
+ # Array into a buffer.
1052
+ def pack_name_array(names)
1053
+ return [nil, []] if names.empty?
1054
+
1055
+ pointers = names.map { |name| FFI::MemoryPointer.from_string(name) }
1056
+ array = FFI::MemoryPointer.new(:pointer, names.size)
1057
+ array.write_array_of_pointer(pointers)
1058
+ [array, pointers]
1059
+ end
1060
+
1061
+ # Writes +value+ (a [year, month, day] Array) into +struct+'s own
1062
+ # :year/:month/:day fields. Shared by #date_struct, which builds a
1063
+ # standalone DateStruct, and #datetime_struct, which writes the same
1064
+ # three fields into a DatetimeStruct's embedded :date view instead
1065
+ # -- both are plain FFI::Struct field assignment, so the same method
1066
+ # writes either one.
1067
+ def write_date(struct, value)
1068
+ year, month, day = value
1069
+ struct[:year] = year
1070
+ struct[:month] = month
1071
+ struct[:day] = day
1072
+ end
1073
+
1074
+ # The #write_date/#date_struct counterpart for a [hour, minute,
1075
+ # second, microsecond] Array.
1076
+ def write_time(struct, value)
1077
+ hour, minute, second, microsecond = value
1078
+ struct[:hour] = hour
1079
+ struct[:minute] = minute
1080
+ struct[:second] = second
1081
+ struct[:microsecond] = microsecond
1082
+ end
1083
+
1084
+ # A standalone DateStruct built from +value+, for #generate_date's
1085
+ # min_value/max_value arguments.
1086
+ def date_struct(value)
1087
+ struct = DateStruct.new
1088
+ write_date(struct, value)
1089
+ struct
1090
+ end
1091
+
1092
+ # The #date_struct counterpart for #generate_time's own arguments.
1093
+ def time_struct(value)
1094
+ struct = TimeStruct.new
1095
+ write_time(struct, value)
1096
+ struct
1097
+ end
1098
+
1099
+ # A DatetimeStruct built from +date+ and +time+, for
1100
+ # #generate_datetime's min/max arguments. Writes directly into the
1101
+ # struct's embedded :date/:time fields rather than building two
1102
+ # standalone structs and copying, since an embedded field is already
1103
+ # a view onto the same memory (see #write_date/#write_time).
1104
+ def datetime_struct(date, time)
1105
+ struct = DatetimeStruct.new
1106
+ write_date(struct[:date], date)
1107
+ write_time(struct[:time], time)
1108
+ struct
1109
+ end
1110
+
1111
+ # Reads +struct+'s :year/:month/:day fields back into a
1112
+ # [year, month, day] Array, the inverse of #write_date. Shared by
1113
+ # #generate_date (a standalone DateStruct out-parameter) and
1114
+ # #generate_datetime (the embedded :date view of a DatetimeStruct
1115
+ # out-parameter).
1116
+ def read_date(struct)
1117
+ [struct[:year], struct[:month], struct[:day]]
1118
+ end
1119
+
1120
+ # The #read_date counterpart for a struct's :hour/:minute/:second/
1121
+ # :microsecond fields.
1122
+ def read_time(struct)
1123
+ [struct[:hour], struct[:minute], struct[:second], struct[:microsecond]]
1124
+ end
1125
+
1126
+ # Reads +out+'s const char* out-parameter into a Ruby String, or
1127
+ # returns nil if libhegel left it NULL. Shared by #run_result_error
1128
+ # and #failure_reproduction_blob, the two out-parameters the header
1129
+ # documents as nullable, so both branches only need to be exercised
1130
+ # once between the two call sites rather than at each one.
1131
+ def nullable_out_string(out)
1132
+ ptr = out.read_pointer
1133
+ ptr.null? ? nil : utf8(ptr)
1134
+ end
1135
+
1136
+ # Reads the NUL-terminated string at +pointer+ and labels it UTF-8.
1137
+ # The header calls every string libhegel hands back valid UTF-8, but
1138
+ # FFI::Pointer#read_string labels what it reads ASCII-8BIT. A caller
1139
+ # who matches an error message against a pattern of their own then
1140
+ # gets Encoding::CompatibilityError rather than an answer, and the
1141
+ # engine's own messages do carry characters outside ASCII: the
1142
+ # FilterTooMuch health check writes an em dash into the sentence a
1143
+ # caller is most likely to read.
1144
+ def utf8(pointer)
1145
+ pointer.read_string.force_encoding(Encoding::UTF_8)
1146
+ end
1147
+ end
1148
+ end
1149
+ end