Skip to content

v0.28.0

Choose a tag to compare

@hegel-release hegel-release released this 08 Jul 11:21
Immutable release. Only release title and notes can be modified.

This release fixes a number of bugs found in a full review of the frontend, generators, and proc macros, alongside the engine fixes in the corresponding hegeltest-c release (whose regex, shrinking, and replay fixes all surface here — see its changelog).

Breaking changes:

  • Settings::suppress_health_check now replaces any previously configured suppressions, like Settings::phases, instead of accumulating across calls. Callers that chained multiple calls to build up a set should pass all the checks in one call.
  • Several generator configurations that previously misbehaved silently are now clean usage errors: gs::hashsets(...)/gs::hashmaps(...) with a min_size larger than the element generator's distinct-value pool (previously returned a too-small collection, violating the documented contract), gs::durations().min_value(...) beyond u64::MAX nanoseconds (previously generated values below the requested minimum), gs::uuids().version(n) outside 1–5 (previously generated non-RFC-4122 output), and chrono time/datetime bounds using a mid-day leap-second representation (previously could generate values outside the bounds).
  • The write-only panic_message and reproduce_blob fields have been removed from the doc-hidden backend::Failure type.

Bug fixes:

  • Builder methods called on a string-shaped generator (text, characters, from_regex, domains) after it had already been drawn from were silently ignored; they now take effect.
  • Running out of data in the middle of a #[rule] now unwinds through hegel::stateful::run() as an overrun instead of returning normally with a half-applied rule, so code after run() can no longer observe torn state. Engine errors from Pool operations are classified properly instead of all being treated as out-of-data.
  • A test body that caught its own panic no longer donates that panic's location to a later failure on the same thread, which could group failures under the wrong origin.
  • In Mode::SingleTestCase, a failed assume() inside tc.repeat(...) skips that iteration and continues, matching normal mode, instead of silently ending the supposedly endless loop.
  • hegel::with_output_override restores the previous sink even if the wrapped closure panics, and explicit-test-case replay output goes through the output sink like every other replay line.
  • #[derive(DefaultGenerator)] now supports generic types: the generated generator and DefaultGenerator impl carry the type's parameters with DefaultGenerator bounds, so #[derive(DefaultGenerator)] struct Point<T> { x: T } makes gs::default::<Point<i32>>() work for any T: DefaultGenerator. Const parameters are supported too; lifetime parameters get a clean compile error (previously any generic type produced a confusing error pointing into generated code). The derive also reports clean errors for zero-variant enums and fields named new or boxed (previously a proc-macro panic or a builder-collision error), and an enum with variants Foo(...) and FooWith { ... } now compiles. Generated code is fully qualified, so a local mod hegel or a shadowed Vec no longer breaks expansion, and a doc comment on a #[rule] no longer emits an unused_doc_comments warning into your crate.
  • #[hegel::composite] and #[hegel::state_machine] now reject arguments instead of silently ignoring them, and #[hegel::test] rejects a declared return type with a targeted message.

Improvements:

  • gs::hashmaps with an enumerable key generator (e.g. sampled_from) draws keys without replacement, so maps that must contain most of a small key alphabet generate efficiently instead of tripping the TooSlow health check.
  • .filter(...) on an enumerable generator computes the filtered value set once instead of re-cloning the source's elements on every draw, and one_of generators of enumerable children are themselves enumerable.
  • hegel::extras::serde_json::values() bounds its recursive arrays and objects so generated JSON trees terminate naturally instead of routinely exhausting the choice buffer.
  • HashSet<T> now has a DefaultGenerator impl, matching Vec and HashMap, and hegel::extras::chrono::naive_weeks() keeps its default range clear of NaiveDate::MIN/MAX, where chrono's own NaiveWeek accessors panic.
  • text() and binary() with min_size > 100 and no max_size generate lengths in [min_size, min_size + 100] instead of collapsing to a fixed length.

libhegel C ABI

This release fixes a number of correctness bugs found in a full review of the engine, hardens the C ABI against misuse, and improves generation and shrinking performance.

Breaking C ABI changes:

  • hegel_settings_set_mode, hegel_settings_set_backend, hegel_settings_set_verbosity, and hegel_mark_complete now take their enum-valued parameter as a validated uint32_t instead of the enum type itself. Passing an out-of-range value is now a reportable HEGEL_E_INVALID_ARG instead of undefined behavior in the library. C callers passing the enum constants are source-compatible and just need a recompile against the new header.
  • hegel_settings_set_suppress_health_check now replaces the set of suppressed checks on each call, like hegel_settings_set_phases, instead of accumulating across calls (which made it impossible to clear a suppression). Callers that relied on accumulation should OR their bits together into a single call.
  • hegel_next_test_case, hegel_run_result, hegel_test_case_from_blob, and hegel_test_case_clone now check the handle before the out parameter, so passing both as NULL returns HEGEL_E_INVALID_HANDLE rather than HEGEL_E_INVALID_ARG, consistent with every other function.

Generation fixes:

  • Strings generated from regex patterns now actually match patterns using \b, \B, or $/\Z in non-final positions (previously the anchors were ignored, so e.g. most strings generated for \bfoo\b contained no match), and fullmatch generation no longer emits lookaround assertion bodies into the output. Atomic groups and possessive repeats re-validate their output against the pattern, and (?i) negated character classes exclude the full case-folding closure of their members.
  • A string generator whose alphabet is empty with max_size = 0 — a legal configuration whose only value is the empty string — no longer crashes the engine on its first test case.
  • Times and datetimes drawn near a bound expressed with chrono's leap-second representation could exceed the bound; such bounds are now rejected up front (except the end-of-day leap second, which remains fully supported).

Shrinking and replay fixes:

  • Fixed an engine panic when a shrink pass revisited an integer node whose kind had changed under it mid-pass.
  • The pre-shrink verification run now requires the failure to reproduce with the same origin. Previously a test that panicked at a different location on replay could be reported under the wrong origin with a reproduction blob that did not reproduce it; it is now correctly reported as a flaky test.
  • Several shrink passes are substantially more effective per invocation: the length-redistribution passes can move more than one element at a time, the adaptive deletion pass's leftward walk accumulates across accepted steps, and string truncation binary-searches instead of trying every length.
  • The targeting phase no longer corrupts its hill-climbing steps for byte values wider than 128 bits.
  • Database replay no longer runs an example twice when it is stored under both the primary and secondary keys, and a stored counterexample that replays with different values no longer skips the shrink phase just because it realised the same length.

Performance: regex . and negated-literal draws, string-constant injection, and codepoint lookups no longer rescan their alphabets on every drawn character, and the per-draw choice-configuration clone in the draw hot path is gone.

Diagnostics: test-case handle errors (HEGEL_E_INVALID_HANDLE, HEGEL_E_ALREADY_COMPLETE, HEGEL_E_CONCURRENT_USE) now record a message on the context like every other handle family, and the header documentation has been corrected in several places (the hegel_pool_generate empty-pool result is HEGEL_E_ASSUME and callers may recover from it like any failed assumption, hegel_settings_new defaults are CI-dependent, run handles are single-threaded while settings handles document their share-after-configuring contract, and hegel_date_t spans the proleptic year range its draws actually use).