Module: Hegel::LibHegel

Defined in:
lib/hegel/lib_hegel.rb,
lib/hegel/lib_hegel/real.rb,
sig/hegel.rbs

Overview

The libhegel binding boundary: the small set of methods a connection to the native engine must answer to, plus the result-code translation and context lifecycle shared by every implementation.

Ruby has no interface construct, so this module carries the contract two ways: METHODS names every method an implementation must answer to, and a conformance test (test/hegel/test_lib_hegel.rb) asserts that every implementation responds to all of them. LibHegel::Real drives the C ABI; test/support/fake_lib_hegel.rb (not shipped in the gem) is a second, configurable implementation, so the logic built on top of this boundary is testable without the native engine. hegel-java makes the same split, between its Libhegel interface, RealLibhegel, and FakeLibhegel.

Defined Under Namespace

Classes: Real

Constant Summary collapse

HEGEL_OK =

hegel_result_t, named from hegel-c/include/hegel.h's enum of the same name. HEGEL_OK is success; every other value is a negative error code.

Returns:

  • (Integer)
0
HEGEL_E_STOP_TEST =

Returns:

  • (Integer)
-1
HEGEL_E_ASSUME =

Returns:

  • (Integer)
-2
HEGEL_E_BACKEND =

Returns:

  • (Integer)
-3
HEGEL_E_INVALID_HANDLE =

Returns:

  • (Integer)
-4
HEGEL_E_INVALID_ARG =

Returns:

  • (Integer)
-5
HEGEL_E_ALREADY_COMPLETE =

Returns:

  • (Integer)
-6
HEGEL_E_NOT_COMPLETE =

Returns:

  • (Integer)
-7
HEGEL_E_INTERNAL =

Returns:

  • (Integer)
-8
HEGEL_E_CONCURRENT_USE =

Returns:

  • (Integer)
-9
CODE_NAMES =

The hegel.h name for each code above, so a translated error message names the code instead of leaving the reader to cross-reference the header by number alone.

Returns:

  • (Hash[Integer, String])
{
  HEGEL_OK => "HEGEL_OK",
  HEGEL_E_STOP_TEST => "HEGEL_E_STOP_TEST",
  HEGEL_E_ASSUME => "HEGEL_E_ASSUME",
  HEGEL_E_BACKEND => "HEGEL_E_BACKEND",
  HEGEL_E_INVALID_HANDLE => "HEGEL_E_INVALID_HANDLE",
  HEGEL_E_INVALID_ARG => "HEGEL_E_INVALID_ARG",
  HEGEL_E_ALREADY_COMPLETE => "HEGEL_E_ALREADY_COMPLETE",
  HEGEL_E_NOT_COMPLETE => "HEGEL_E_NOT_COMPLETE",
  HEGEL_E_INTERNAL => "HEGEL_E_INTERNAL",
  HEGEL_E_CONCURRENT_USE => "HEGEL_E_CONCURRENT_USE"
}.freeze
HEGEL_STATUS_VALID =

hegel_status_t, named from hegel.h's enum of the same name. Passed to hegel_mark_complete to describe how a test case ended.

Returns:

  • (Integer)
0
HEGEL_STATUS_INVALID =

Returns:

  • (Integer)
1
HEGEL_STATUS_OVERRUN =

Returns:

  • (Integer)
2
HEGEL_STATUS_INTERESTING =

Returns:

  • (Integer)
3
HEGEL_RUN_STATUS_PASSED =

hegel_run_status_t, named from hegel.h's enum of the same name. Read via hegel_run_result_status once a run has finished.

Returns:

  • (Integer)
0
HEGEL_RUN_STATUS_FAILED =

Returns:

  • (Integer)
1
HEGEL_RUN_STATUS_ERROR =

Returns:

  • (Integer)
2
HEGEL_VERBOSITY_QUIET =

hegel_verbosity_t, named from hegel.h's enum of the same name. Passed to hegel_settings_set_verbosity.

Returns:

  • (Integer)
0
HEGEL_VERBOSITY_NORMAL =

Returns:

  • (Integer)
1
HEGEL_VERBOSITY_VERBOSE =

Returns:

  • (Integer)
2
HEGEL_VERBOSITY_DEBUG =

Returns:

  • (Integer)
3
HEGEL_PHASE_EXPLICIT =

hegel_phase_t, named from hegel.h's enum of the same name. A bitwise OR of these is passed to hegel_settings_set_phases; the default is HEGEL_PHASE_ALL.

Returns:

  • (Integer)
1
HEGEL_PHASE_REUSE =

Returns:

  • (Integer)
2
HEGEL_PHASE_GENERATE =

Returns:

  • (Integer)
4
HEGEL_PHASE_TARGET =

Returns:

  • (Integer)
8
HEGEL_PHASE_SHRINK =

Returns:

  • (Integer)
16
HEGEL_PHASE_ALL =

Returns:

  • (Integer)
31
HEGEL_HC_FILTER_TOO_MUCH =

hegel_health_check_t, named from hegel.h's enum of the same name. A bitwise OR of these is passed to hegel_settings_set_suppress_health_check; the default is all enabled.

Returns:

  • (Integer)
1
HEGEL_HC_TOO_SLOW =

Returns:

  • (Integer)
2
HEGEL_HC_TEST_CASES_TOO_LARGE =

Returns:

  • (Integer)
4
HEGEL_HC_LARGE_INITIAL_TEST_CASE =

Returns:

  • (Integer)
8
HEGEL_LABEL_LIST =

hegel_label_t, named from hegel.h's enum of the same name. Passed to hegel_start_span to identify what kind of structure a span groups. Copied through HEGEL_LABEL_SET_CHOICE (value 33). The header describes the last two, HEGEL_LABEL_FRESH_ID and HEGEL_LABEL_SET_CHOICE, as spans the engine opens itself around a hegel_pool_add / hegel_pool_generate call; a caller never passes either to hegel_start_span.

The header documents that "Libraries may use any stable u64 to define their own spans." A caller building its own compound generator on top of this boundary can pick any u64 that does not collide with the reserved values below.

Returns:

  • (Integer)
1
HEGEL_LABEL_LIST_ELEMENT =

Returns:

  • (Integer)
2
HEGEL_LABEL_SET =

Returns:

  • (Integer)
3
HEGEL_LABEL_SET_ELEMENT =

Returns:

  • (Integer)
4
HEGEL_LABEL_MAP =

Returns:

  • (Integer)
5
HEGEL_LABEL_MAP_ENTRY =

Returns:

  • (Integer)
6
HEGEL_LABEL_TUPLE =

Returns:

  • (Integer)
7
HEGEL_LABEL_ONE_OF =

Returns:

  • (Integer)
8
HEGEL_LABEL_OPTIONAL =

Returns:

  • (Integer)
9
HEGEL_LABEL_FIXED_DICT =

Returns:

  • (Integer)
10
HEGEL_LABEL_FLAT_MAP =

Returns:

  • (Integer)
11
HEGEL_LABEL_FILTER =

Returns:

  • (Integer)
12
HEGEL_LABEL_MAPPED =

Returns:

  • (Integer)
13
HEGEL_LABEL_SAMPLED_FROM =

Returns:

  • (Integer)
14
HEGEL_LABEL_ENUM_VARIANT =

Returns:

  • (Integer)
15
HEGEL_LABEL_FEATURE_FLAG =

Returns:

  • (Integer)
16
HEGEL_LABEL_REGEX =

Returns:

  • (Integer)
17
HEGEL_LABEL_EMAIL =

Returns:

  • (Integer)
18
HEGEL_LABEL_URL =

Returns:

  • (Integer)
19
HEGEL_LABEL_DOMAIN =

Returns:

  • (Integer)
20
HEGEL_LABEL_DATE =

Returns:

  • (Integer)
21
HEGEL_LABEL_TIME =

Returns:

  • (Integer)
22
HEGEL_LABEL_DATETIME =

Returns:

  • (Integer)
23
HEGEL_LABEL_UUID =

Returns:

  • (Integer)
24
HEGEL_LABEL_IP_ADDRESS =

Returns:

  • (Integer)
25
HEGEL_LABEL_INTEGER =

Returns:

  • (Integer)
26
HEGEL_LABEL_FLOAT =

Returns:

  • (Integer)
27
HEGEL_LABEL_BOOLEAN =

Returns:

  • (Integer)
28
HEGEL_LABEL_BYTES =

Returns:

  • (Integer)
29
HEGEL_LABEL_STRING =

Returns:

  • (Integer)
30
HEGEL_LABEL_STATEFUL_RULE =

Returns:

  • (Integer)
31
HEGEL_LABEL_FRESH_ID =

Returns:

  • (Integer)
32
HEGEL_LABEL_SET_CHOICE =

Returns:

  • (Integer)
33
HEGEL_STATE_MACHINE_DONE =

HEGEL_STATE_MACHINE_DONE, named from hegel.h's #define of the same name. hegel_state_machine_next_rule writes this to its out_rule_index parameter once the current test case's step budget is exhausted; see LibHegel::Real#state_machine_next_rule for why that raw sentinel is returned rather than translated to nil.

Returns:

  • (Integer)
-1
HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED =

hegel_new_collection's max_size accepts UINT64_MAX to mean "no upper bound", in the header's own words. Ruby has no fixed-width integer type to read that constant off, so it is spelled out here as the value a 64-bit unsigned integer maxes out at.

Returns:

  • (Integer)
(2**64) - 1
HEGEL_FLOAT64_SMALLEST_NONZERO_MAGNITUDE_UNRESTRICTED =

hegel_generate_float's smallest_nonzero_magnitude must be positive and finite. The header names 5e-324 as the width-64 value that places no restriction on which nonzero magnitudes get drawn.

Returns:

  • (Float)
5e-324
METHODS =

The methods every implementation of this boundary (Real, Fake) must answer to. Held as data, not a Ruby interface/protocol, because Ruby has none; test/hegel/test_lib_hegel.rb asserts every implementation responds to each name here.

Returns:

  • (Array[Symbol])
%i[
  context_new context_free context_last_error version
  settings_new settings_free settings_set_test_cases settings_set_verbosity
  settings_set_seed settings_set_derandomize settings_set_database
  run_start next_test_case run_free test_case_free mark_complete
  generate_boolean generate_integer generate_integer_big
  run_result run_result_free run_result_status run_result_error
  run_result_failure_count run_result_failure failure_free failure_origin
  failure_reproduction_blob test_case_from_blob
  start_span stop_span
  new_collection collection_more collection_reject collection_free
  generate_float
  string_generator_text string_generator_free generate_string generate_string_result_free
  generate_bytes generate_bytes_result_free
  string_generator_regex string_generator_email string_generator_url string_generator_domain
  generate_ipv4 generate_ipv6 generate_uuid
  generate_date generate_time generate_datetime
  settings_set_phases settings_set_suppress_health_check settings_set_report_multiple_failures
  settings_set_database_key settings_set_stateful_step_count
  target
  new_pool pool_add pool_generate pool_free
  new_state_machine state_machine_next_rule state_machine_rule_rejected state_machine_free
].freeze

Class Method Summary collapse

Class Method Details

.check!(impl, ctx, code) ⇒ void

This method returns an undefined value.

Raises the exception code translates to, or returns without effect for HEGEL_OK. The message is read from impl.context_last_error(ctx) immediately, since libhegel's own buffer for it is invalidated by the next call taking the same context. By the time a caller further up the stack could read it, the buffer might already describe a different call.

Parameters:

  • impl (Object)
  • ctx (Object)
  • code (Integer)


205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/hegel/lib_hegel.rb', line 205

def check!(impl, ctx, code)
  return if code == HEGEL_OK

  # An engine newer than the pinned one can return a code these bindings
  # have no name for, which is the situation the version warning exists
  # to announce. Naming it plainly beats failing to look it up, because a
  # KeyError here would replace the engine's own diagnostic with a
  # message about a missing hash key.
  name = CODE_NAMES[code] || "unknown result code"
  message = "#{name} (#{code}): #{impl.context_last_error(ctx)}"
  case code
  when HEGEL_E_STOP_TEST then raise StopTest, message
  when HEGEL_E_ASSUME then raise AssumeFailed, message
  else raise Hegel::Error, message
  end
end

.decode_integer_le(bytes) ⇒ Integer

The inverse of .encode_integer_le. bytes may be longer than the value's own minimal encoding -- the header documents hegel_generate_ integer_big's out_value as sign-filled past that length, so a caller may decode the whole out_value_cap-sized buffer and still get the right answer -- so length here is read from bytes itself, not assumed minimal.

Parameters:

  • bytes (String)

Returns:

  • (Integer)


262
263
264
265
266
267
# File 'lib/hegel/lib_hegel.rb', line 262

def decode_integer_le(bytes)
  length = bytes.bytesize
  unsigned = bytes.each_byte.with_index.sum { |byte, i| byte << (8 * i) }
  negative = (bytes.getbyte(length - 1) & 0x80) != 0
  negative ? unsigned - (1 << (8 * length)) : unsigned
end

.encode_integer_le(n) ⇒ String

hegel_generate_integer_big's own documented convention for min_value/max_value/out_value: two's-complement little-endian signed byte buffers. Pure Ruby arithmetic, no native marshalling, so this is unit-testable directly (test/hegel/test_lib_hegel.rb round-trips it) and shared unchanged between LibHegel::Real's bounds-encode and result-decode, kept out of real.rb so that direct testing stays possible.

+n+'s minimal two's-complement byte length is n.bit_length / 8 + 1. Integer#bit_length reports one bit fewer at a negative power of two (-128 is 7 bits, 128 is 8), so this formula needs no separate sign-bit adjustment there; verified against a brute-force minimal-length search over both signs and the int32/int64/2**100 boundaries.

Parameters:

  • n (Integer)

Returns:

  • (String)


251
252
253
254
# File 'lib/hegel/lib_hegel.rb', line 251

def encode_integer_le(n)
  byte_length = (n.bit_length / 8) + 1
  byte_length.times.map { |i| (n >> (8 * i)) & 0xFF }.pack("C*")
end

.warn_on_version_mismatch(impl, ctx, io: $stderr) ⇒ void

This method returns an undefined value.

Warns on io when impl.version(ctx) differs from the engine version these bindings were built for (Hegel::LIBHEGEL_VERSION), and does nothing when they match. Never raises: a mismatched engine is still usable, only untested against, so this is a warning rather than a load failure. io defaults to $stderr and is overridable so a caller (and a test) can capture the warning instead.

Parameters:

  • impl (Object)
  • ctx (Object)
  • io: (Object) (defaults to: $stderr)


228
229
230
231
232
233
234
235
# File 'lib/hegel/lib_hegel.rb', line 228

def warn_on_version_mismatch(impl, ctx, io: $stderr)
  loaded = impl.version(ctx)
  return if loaded == Hegel::LIBHEGEL_VERSION

  io.puts(<<~MESSAGE.chomp)
    hegel: loaded libhegel #{loaded} but these bindings were built for #{Hegel::LIBHEGEL_VERSION}; behaviour may differ. Unset HEGEL_LIBHEGEL_PATH to use the bundled engine, or point it at a matching build.
  MESSAGE
end

.with_context(impl) ⇒ void

This method returns an undefined value.

Runs the block with a context obtained from impl.context_new, freeing it via impl.context_free whether the block returns or raises.

A block is the only construct used here: hegel_context_free requires every other handle taking this context to be freed first, so the context must outlive them all, and Ruby's GC gives finalizers no ordering guarantee to rely on instead. The block's caller is the context's owner and holds it for exactly as long as the block runs.



192
193
194
195
196
197
# File 'lib/hegel/lib_hegel.rb', line 192

def with_context(impl)
  ctx = impl.context_new
  yield ctx
ensure
  impl.context_free(ctx)
end