package ppx_hegel_test

  1. Overview
  2. Docs
PPX expander for Hegel test definitions with Antithesis support

Install

dune-project
 Dependency

Authors

Maintainers

Sources

hegel-0.13.1-opam.tar.gz
md5=5ddc4a754e7568d615f2dd9095157d7c
sha512=567141c13aad52d82d918615abcc490480dc69ea62113c16e298897f58dc2a1e74a906ecec352bcb8879312cb06158a5c46f0bd26350e209b94c1498be5b0c4e

doc/CHANGELOG.html

Changelog

0.13.1 - 2026-08-04

This patch bumps our pinned libhegel (hegel-rust) from 0.29.0 to 0.30.5.

0.13.0 - 2026-08-03

This release removes hegel_test_runtime. Users are are now expected to run Hegel tests using a test framework like Alcotest.

0.12.2 - 2026-07-21

This patch fixes redundant output from generated functions on a failing replay. When a drawn function was applied more than once to the same argument, every application printed its own name arg = result line, so a loop calling f 10 three times produced three identical lines. Now, under the default verbosity only the first application with a given argument prints. Applications with that same argument after do not print. Under Verbose or Debug verbosity every application prints.

0.12.1 - 2026-07-15

This patch adds support for driving generation from more than one thread within a single test. A test-case handle may only be drawn from by one thread at a time, so to generate concurrently you clone it and give each thread its own clone. Each clone draws from its own independent stream while the clones share the test case's outcome and budget, so a failure still shrinks and its blob replays deterministically (as long as your own code is deterministic).

Hegel.clone tc returns a fresh clone. Hegel.spawn clones tc and runs the worker on a new thread, and Hegel.join waits for it and re-raises any exception the worker raised.

0.12.0 - 2026-07-14

Removed format_time, format_date, and format_datetime as they duplicated functionality from Core.

0.11.0 - 2026-07-14

This release cleans up several generator APIs where documentation and behavior disagreed.

Breaking: dates, times, and datetimes now generate typed values instead of ISO 8601 strings: dates produces Core.Date.t, times produces Core.Time_ns.Ofday.t (microsecond precision), and datetimes produces Core.Date.t * Core.Time_ns.Ofday.t pairs. Previously the engine's structured date/time values were formatted away into strings at the draw. The format_date / format_time / format_datetime helpers are now documented public API and render the new typed values in the old ISO 8601 forms, so string-oriented tests migrate with a single function call:

(* before *)
let s = draw tc (dates ()) in

(* after *)
let d = draw tc (dates ()) in
let s = format_date d in

[@@deriving hegel_generator]'s t list fields now generate through the same engine-driven collection protocol as Generators.lists. Previously the deriver drew a length in a hidden, unconfigurable [0, 20] range and looped, so derived lists could never exceed 20 elements and the engine could not shrink them by deleting individual elements. Derived t option fields likewise now go through the same machinery as Generators.optional.

[@@deriving hegel_generator] no longer clamps derived int fields to ±2³⁰−1. Derived ints now use the same full native-int default range as a hand-written integers (). The clamp deliberately weakened generation so user arithmetic could not overflow — hiding exactly the bugs a property test should find. Properties that relied on products of derived ints never overflowing must handle overflow themselves (or bound their generators explicitly).

Breaking: ip_addresses now takes ?version:[V4 | V6] instead of ?version:int, and generates typed Ipaddr.t values instead of strings (ipaddr was already a dependency). Previously ~version:5 type-checked and then failed at draw time, and the typed address the implementation built was thrown away via to_string. Render a drawn address with Ipaddr.to_string:

(* before *)
let s = draw tc (ip_addresses ~version:4 ()) in

(* after *)
let ip = draw tc (ip_addresses ~version:`V4 ()) in
let s = Ipaddr.to_string ip in

one_of now prints a drawn value through the printer of the branch it was actually drawn from. Previously every branch's values printed through the first branch's printer. The recorded branch is per generator value, so one one_of generator drawn several times before printing (e.g. as the element generator of lists) still prints every value through a single branch's printer — now the most recently drawn branch rather than the first.

The internal PPX/engine plumbing that Hegel.Generators previously exposed at its top level (Labels, group, discardable_group, new_collection, collection_more, collection_reject, pool_values, resolve_draw, max_filter_attempts, composite_with_label, and the collection type) has moved into Hegel.Generators.Ppx_internal, an explicitly internal submodule with no stability guarantees. These values were never documented API; open Hegel.Generators no longer brings them into scope.

with_suppress_health_check now sets the suppressed-health-check list like every other with_* builder, replacing any previously suppressed list. Previously it appended, so repeated calls accumulated duplicates and a suppression could never be undone. To suppress several checks, pass them in one list.

Documentation corrections: sampled_from no longer claims uniform sampling (the engine's bounded-integer draw deliberately over-weights boundary indices, favoring earlier elements); the Explicit phase is documented as reserved for future use (hegel-ocaml has no explicit-examples facility yet, so selecting it has no effect); assume documents that its test-case handle is accepted for API symmetry and the rejection is client-side; and drawing from an empty pool is documented (and pinned by a test) as raising Assume_rejected rather than Data_exhausted.

0.10.1 - 2026-07-14

This patch fixes a crash when several test binaries start at the same time with a cold libhegel cache. Every process downloaded the library to the same temporary file and renamed it into place, so the first process to finish pulled the file out from under the others, which then failed with checksum or missing-file errors (and could corrupt the cached library). Each process now downloads to its own temporary file and atomically renames it into place, so concurrent first runs all succeed. Partial downloads left behind by a killed process no longer interfere with later downloads.

0.10.0 - 2026-07-13

This release overhauls what Hegel prints when a property fails.

Failing runs now produce a framed report: a header naming the test and its source location, a Falsified after N test cases (M discarded): line, an indented body of the drawn values and notes, the exception, and rerun with: [@@failure_blobs [ "..." ]]. Failure blobs are now printed by default. A run that finds multiple failures reports each in its own numbered section, and on a terminal the report is colorized (HEGEL_COLOR=1|0 forces it on or off).

In stateful testing, the draws a rule makes are nested under its step header, and an invariant violation is attributed to the index of the invariant in the invariant list (Invariant N violated after step M).

It also adds two ways to make a failure more informative:

  • require and require_equal for assertions inside a test. require_equal renders a structural s-expression diff of the two values in the report
  • ?sexp_of_state on Stateful.run, which prints the model state after the initial state and after each step.

0.9.2 - 2026-07-13

This patch bumps our pinned libhegel (hegel-rust) from 0.28.0 to 0.29.0.

0.9.1 - 2026-07-13

This patch adds functions, functions2, and functions3 in Hegel.Generators for generating functions. functions2 and functions3 generate curried two and three argument functions. Function generators are unprintable.

When a property over a generated function fails, Hegel prints the function application(s) and its result(s). For example, a property that wrongly assumes applying a function twice returns the original value:

let%hegel_test involution tc =
  let f = draw_silent tc (functions ~sexp_of_arg:Int.sexp_of_t ~returns:(integers ()) ()) in
  let x = draw tc (integers ()) in
  assert (f (f x) = x)

fails with a concrete counterexample:

x = 0
f 0 = 1
f 1 = 1

0.9.0 - 2026-07-09

The hashmaps generator is replaced by two generators that say what they mean: assoc_lists, which produces what hashmaps actually produced — a (key * value) list in generation order with unique keys — and hash_tables, which produces a real polymorphic Hashtbl.t with the same entry generation. Existing uses of hashmaps translate directly to assoc_lists.

0.8.2 - 2026-07-09

This patch bumps our pinned libhegel (hegel-rust) from 0.27.0 to 0.28.0.

0.8.1 - 2026-07-06

This patch bumps our pinned libhegel (hegel-rust) from 0.23.1 to 0.27.0.

0.8.0 - 2026-07-02

This release reexports many functions from the Client module to the Hegel module. The Client module has been renamed to Internal. Hegel tests should only use the Hegel, Hegel.Generators, and Hegel.Stateful modules.

0.7.6 - 2026-07-01

Adds database key as an optional argument to Hegel.run_hegel_test. If ppx_hegel_test is used, the key "file_name:test_name" is automatically passed in by let%hegel_test.

0.7.5 - 2026-06-30

Add support for using ppx_hegel_test with OCaml 5.3+.

0.7.4 - 2026-06-30

This release publishes Hegel for OCaml to the opam repository, with the native Hegel engine (libhegel) bundled into the package. Installing through opam also installs the prebuilt libhegel matching your platform.

0.7.3 - 2026-06-26

This patch bumps our pinned libhegel (hegel-rust) from 0.23.0 to 0.23.1.

0.7.2 - 2026-06-24

Fixes formatting when reporting multiple failures.

0.7.1 - 2026-06-24

This patch bumps our pinned libhegel (hegel-rust) from 0.21.2 to 0.23.0.

0.7.0 - 2026-06-22

This release reworks the stateful-testing value-pool API. The Stateful.Variables module is renamed to Stateful.Pool. The draw and consume functions have been replaced by two unprintable generators values_consumed and values_reusable (which can be made printable using with_printer). This lets pool picks compose with the rest of the generator combinators (map, filter, with_printer, …) and be drawn through the normal draw / draw_silent path.

Pool.create no longer takes a ?sexp_of argument.

(* before *)
let vars = Stateful.Variables.create ~sexp_of:Int.sexp_of_t tc in
Stateful.Variables.add vars id;
let reused = Stateful.Variables.draw vars in
let taken = Stateful.Variables.consume vars in

(* after *)
let vars = Stateful.Pool.create tc in
Stateful.Pool.add vars id;
let reused = Hegel.draw_silent tc (Stateful.Pool.values_reusable vars) in
let taken = Hegel.draw_silent tc (Stateful.Pool.values_consumed vars) in
(* to print a pick on a failing replay: *)
let taken = Hegel.draw tc (with_printer Int.sexp_of_t (Stateful.Pool.values_consumed vars)) in

Updating your stateful tests requires renaming Variables to Pool, replacing draw/consume calls with a draw/draw_silent of the corresponding generator, and dropping the ~sexp_of argument to create.

0.6.3 - 2026-06-19

This patch bumps our pinned libhegel (hegel-rust) from 0.19.2 to 0.21.2.

0.6.2 - 2026-06-18

This patch fixes a bug that caused stateful test generation to terminate early.

0.6.1 - 2026-06-17

This patch bumps our pinned libhegel (hegel-rust) from 0.19.1 to 0.19.2.

0.6.0 - 2026-06-17

This release makes Hegel print the values it drew as s-expressions when a test fails. On the final replay of a failing property, each draw is reported asname = value. Inside a let%hegel_test, each draw is labeled with the variable it was bound to:

let%hegel_test sorted_after_insert tc =
  let xs = Hegel.draw tc (lists (integers ()) ()) in
  let x = Hegel.draw tc (integers ()) in
  assert (is_sorted (insert x (List.sort compare xs)))
;;

(* On failure, prints:
     xs = (3 1)
     x = 0  *)

A named draw that is shadowed or inside a loop is numbered (x_1, x_2, …). An unnamed draw or a draw outside of a let%hegel_test binding is named draw_1, draw_2, …. The label can also be passed explicitly.

Generators are now ('a, 'p) generator. The phantom 'p is printable when the generator carries a printer and unprintable otherwise.

map, flat_map, sampled_from, and just are unprintable by default. Draw from them with draw_silent (no printing).

(* before *)
let v = Hegel.draw tc (sampled_from [ 10; 20; 30 ])

(* after — not printed *)
let v = Hegel.draw_silent tc (sampled_from [ 10; 20; 30 ])

Attach a printer with the new with_printer to make a printable generator. Draw from them with draw. with_printer takes any 'a -> Sexp.t.

(* after — printed on failure *)
let sexp_of_int n = ...

let v =
  Hegel.draw tc (with_printer sexp_of_int (sampled_from [ 10; 20; 30 ]))

[@@deriving generator] is now [@@deriving hegel_generator], and emits a generator value. The deriver previously produced a test_case -> t function; it now produces a (t, unprintable) generator value named <t>_generator.

type point = { x : int; y : int } [@@deriving hegel_generator]

(* before: let p = point_generator tc *)
let p = Hegel.draw_silent tc point_generator

To print a value from a derived generator:

type point = { x : int; y : int } [@@deriving hegel_generator]

let sexp_of_point p = ...

let p = Hegel.draw tc (Hegel.with_printer sexp_of_point point_generator)

For a given type t, ppx_sexp_conv can be used to automatically derive sexp_of_<t>.

Stateful tests gain a ?sexp_of argument on Variables.create; when given, each variable that is drawn or consumed prints as v<id> = <sexp> on a failing replay.

0.5.5 - 2026-06-16

This patch fixes a bug where note ignored the verbosity setting.

0.5.4 - 2026-06-15

Adds with_stateful_step_count to allow controlling the number of steps in a stateful test and swarm testing to stateful tests. Internal changes to stateful test generation.

0.5.3 - 2026-06-15

This patch bumps our pinned libhegel (hegel-rust) from 0.18.0 to 0.19.1.

0.5.2 - 2026-06-12

This release updates the bindings to libhegel 0.18.0 and has internal control flow refactors.

0.5.1 - 2026-06-12

Make reporting multiple failures false by default.

0.5.0 - 2026-06-09

This release replaces the hegel-core Python engine with the libhegel Rust engine, called in-process via ctypes. There is no longer a subprocess, socket, or wire protocol. libhegel is located (or downloaded) at runtime and dlopened.

The breaking change is the removal of the Hegel.Protocol, Hegel.Connection, and Hegel.Session modules.

0.4.2 - 2026-05-29

Implements reproducing a test case with failure blobs. When a test fails, the following is outputted:

<some exception here>
...
[hegel] To replay the failure, add to your test: [@@failure_blobs [ "<blob string>"; ... ]]

To replay:

let%hegel_test my_test tc = body
[@@failure_blobs [ "<blob string>"; ... ]]

0.4.1 - 2026-05-27

Fixes uniqueness of hashmap keys and lists when unique=true not being preserved after transformation.

0.4.0 - 2026-05-27

This release changes the public API for running Hegel tests to a new let%hegel_test PPX extension.

(* before *)
let my_invariant =
  Hegel.run_hegel_test (fun tc -> ...)

(* after *)
let%hegel_test my_invariant tc = ...

The top-level Hegel.run_hegel_test has been removed from the public API. Existing callers should migrate to let%hegel_test. The generated function remains directly callable as an ordinary unit -> unit, so it works with any test runner.

Per-test settings can be supplied with the [@@settings ...] attribute:

let%hegel_test my_invariant tc = ...
[@@settings Hegel.settings ~test_cases:1000 ()]

This release also adds integration with Antithesis. Each let%hegel_test emits an always assertion recording whether the property test passed or failed. Outside Antithesis, the integration is a no-op.

Discovering tests with dune runtest

If you use dune, add the PPX as an inline_tests backend so dune runtest discovers every let%hegel_test automatically:

(library
 (name my_tests)
 (libraries hegel)
 (inline_tests (backend ppx_hegel_test))
 (preprocess (pps ppx_hegel_test)))

After that, dune runtest finds and runs every let%hegel_test in the library.

0.3.19 - 2026-05-19

Add Single_test_case mode setting for running exactly one test case with no shrinking or replay. This mode is mostly intended for long-running workloads. Stateful testing will keep running rules indefinitely when in this mode.

0.3.18 - 2026-05-18

This patch bumps our pinned hegel-core from 0.8.0 to 0.9.1.

0.3.17 - 2026-05-14

Internal checksum refactor.

0.3.16 - 2026-05-14

Add Hegel.Stateful module for rule based stateful testing.

0.3.15 - 2026-05-13

Reexports settings in the top level Hegel module.

0.3.14 - 2026-05-13

This release adds the phase enum and using with_phases to control which test lifecycle phases run.

0.3.13 - 2026-05-07

Bump our pinned hegel-core to 0.8.0.

0.3.12 - 2026-05-06

Raise conformance to hegel-core 0.7.0.

0.3.11 - 2026-05-06

Restore compatibility with OxCaml.

0.3.10 - 2026-05-05

Bump our pinned hegel-core from 0.5.0 to 0.7.0

0.3.9 - 2026-05-05

ip_addresses now emits the protocol schema {"type": "ip_address", "version": 4|6} instead of different types per version ({"type": "ipv4"} / {"type": "ipv6"})

0.3.8 - 2026-05-05

one_of no longer wraps each child schema in a tagged tuple. It now sends the raw children and relies on the new protocol contract in which the server emits [index, value] for one_of schemas.

This release also bumps our pinned hegel-core to 0.5.0, which ships the matching server-side protocol change:

0.3.7 - 2026-04-29

Bump our pinned hegel-core version from 0.4.1 to 0.4.14.

0.3.6 - 2026-04-17

Properly handle flaky generation failures.

0.3.5 - 2026-04-16

Add support for building and running on the OxCaml compiler (ocaml-variants.5.2.0+ox). The ppx_hegel_generator PPX deriver now works on both standard OCaml and OxCaml by abstracting over ppxlib AST differences (labeled tuples, constructor modalities) via a platform-selected compatibility module.

0.3.4 - 2026-04-14

Handle StopTest in collection_reject so that unique non-basic lists terminate cleanly when the server's rejection limit is reached, instead of raising an unhandled protocol error.

0.3.3 - 2026-04-14

Restore timeouts to stream receive operations, which were dropped when switching from polling to Condition.wait.

0.3.2 - 2026-04-14

Add characters() generator, and add more options to text()

0.3.1 - 2026-04-08

Add support for new hegel-core protocol versions.

0.3.0 - 2026-03-30

This release moves the library over to use the Jane Street standard library instead of the Inria one.

0.2.0 - 2026-03-27

This release brings the OCaml library in line with hegel-rust, including a breaking API change: test functions now receive an explicit test_case parameter.

  • Breaking: draw, assume, note, and target now take a test_case first argument. Test functions change from unit -> unit to test_case -> unit. PPX-derived generators change from unit -> 'a to test_case -> 'a.
  • Switch from Unix socket to --stdio pipe-based transport, eliminating temp directories and socket polling.
  • Add a background reader thread for packet dispatch, replacing the demand-driven reader model.
  • Add Settings API with health_check, verbosity, database, derandomize, and suppress_health_check support.
  • Auto-install hegel-core via uv when not found on PATH.
  • Add server crash detection via a monitor thread.
  • Bump supported protocol versions to 0.1–0.7.

0.1.3 - 2026-03-11

Add validation to generator arguments, and drop name from run_hegel_test.

0.1.2 - 2026-03-04

Split out generators.ml into multiple files.

0.1.1 - 2026-03-03

Remove some unnecessary code only present for testing.