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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +264 -0
- data/Rakefile +19 -0
- data/docs/README.md +25 -0
- data/docs/adr/0001-bind-libhegel-through-fiddle.md +54 -0
- data/docs/adr/0002-ship-one-prebuilt-engine-per-platform-specific-gem.md +48 -0
- data/docs/adr/0003-publish-as-hegeltest-require-as-hegel.md +39 -0
- data/docs/adr/0004-expose-generators-through-a-mixin-with-keyword-options.md +42 -0
- data/docs/adr/0005-name-drawn-values-from-the-callers-source-with-prism.md +40 -0
- data/docs/adr/0006-verify-the-binding-in-seven-layers-with-full-coverage.md +51 -0
- data/docs/adr/0007-ship-a-thin-ruby-skill-shaped-for-donation.md +56 -0
- data/docs/adr/0008-revisit-the-binding-after-milestone-c-on-measurement.md +81 -0
- data/docs/adr/0009-turn-the-example-database-on-with-a-key.md +89 -0
- data/docs/adr/0010-declare-stateful-rules-with-a-class-macro.md +113 -0
- data/docs/adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md +83 -0
- data/docs/adr/0012-build-a-failure-origin-from-the-callers-own-frame.md +72 -0
- data/docs/adr/0013-bind-libhegel-through-the-ffi-gem.md +102 -0
- data/docs/architecture.md +182 -0
- data/lib/hegel/draw_name.rb +109 -0
- data/lib/hegel/errors.rb +47 -0
- data/lib/hegel/generator.rb +98 -0
- data/lib/hegel/generators.rb +865 -0
- data/lib/hegel/lib_hegel/real.rb +1149 -0
- data/lib/hegel/lib_hegel.rb +269 -0
- data/lib/hegel/libhegel_version.rb +9 -0
- data/lib/hegel/locate.rb +188 -0
- data/lib/hegel/report.rb +87 -0
- data/lib/hegel/runner.rb +464 -0
- data/lib/hegel/settings.rb +164 -0
- data/lib/hegel/state_machine.rb +89 -0
- data/lib/hegel/stateful/pool.rb +111 -0
- data/lib/hegel/stateful.rb +120 -0
- data/lib/hegel/syntax/methods.rb +173 -0
- data/lib/hegel/test_case.rb +523 -0
- data/lib/hegel/version.rb +5 -0
- data/lib/hegel.rb +92 -0
- data/lib/hegeltest.rb +7 -0
- data/lib/tasks/libhegel.rake +112 -0
- data/lib/tasks/platform_gems.rake +111 -0
- data/sig/hegel.rbs +563 -0
- data/skills/hegel-ruby/SKILL.md +30 -0
- data/skills/hegel-ruby/references/ruby/reference.md +1210 -0
- 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
|