hegeltest 0.14.21

Property-based testing for Rust, built on Hypothesis
Documentation
# AGENTS.md

This file provides guidance to Claude Code (claude.ai/code), Codex 5.5, and other coding agents when working with code in this repository. The legacy `.claude` path is a symlink to `.agents`, and `.claude/CLAUDE.md` resolves back to this file so Claude and Codex share the same instructions.

## Overview

This is the Rust library for Hegel, a universal property-based testing framework. The library communicates with a Python server (powered by Hypothesis) via Unix sockets to generate test data.

## Build & Test Commands

```bash
just check                          # run full CI checks
just test                           # run tests
just lint                           # run clipy
just format                         # format
just docs                           # build and open docs
just check-conformance              # run python conformance tests
just check-coverage                 # check coverage (requires cargo-llvm-cov + llvm-tools-preview)
cargo test test_name                # run a single test
```

MSRV is 1.86 (enforced in CI and Cargo.toml). If you bump it, also bump `ci.yml`, `hegel-macros/Cargo.toml`, and `hegel-c/Cargo.toml`.

## Crate Structure

- `src/lib.rs` — Public API: `hegel()`, `Hegel` builder, `draw()`, `assume()`, `note()`
- `src/protocol.rs` — Binary protocol: packet encoding/decoding, stream multiplexing
- `src/cbor_helpers.rs` — Macros and helpers for `ciborium::Value` (`cbor_map!`, `cbor_array!`, `map_get`, etc.)
- `src/runner.rs` — Spawns hegel CLI, manages socket server
- `src/generators/` — All generator implementations (`mod.rs` has the `Generate` trait + `TestCaseData`)
- `hegel-macros/` — Proc macro crate for `#[derive(Generate)]` (sub-crate with its own `Cargo.toml`)
- `hegel-c/` — Workspace member that builds the `libhegel` cdylib (C-ABI bindings to the native engine, header at `hegel-c/include/hegel.h` generated by cbindgen from `hegel-c/src/lib.rs`). Smoke-tested with `just c-test`. Released as `libhegel-<goos>-<goarch>.<ext>` assets on each GitHub release; the source is published to crates.io as `hegeltest-c` mostly to reserve the name.
- `build.rs` — Locates `hegel` binary on PATH, exports `HEGEL_BINARY_PATH` env var (falls back to `"hegel"`)

### Feature Flags

- **`rand`**: Enables `extras::rand::randoms()` for generating `rand::RngCore` implementations

## Architecture

### How It Works

The library creates a Unix socket path and spawns the `hegel` CLI as a subprocess. The server binds to the socket and listens for the client to connect. A single persistent connection is maintained for the program run, supporting multiple test executions.

### Protocol

CBOR-encoded binary protocol over multiplexed streams. For each test:
1. Client sends `run_test` request on control stream (stream 0)
2. Server sends `test_case` events with stream IDs for each test case
3. Client runs the test function, sending `generate`/`start_span`/`stop_span` requests on the test stream
4. Client sends `mark_complete` with status (VALID, INVALID, or INTERESTING)
5. After all test cases, server sends `test_done` with results

### Generate Trait and BasicGenerator

Generators implement `Generate<T>`:
- `do_draw(&self, data: &TestCaseData) -> T` — Produce a value
- `as_basic()` — Returns `Option<BasicGenerator<T>>` with a CBOR schema + parse function

When `as_basic()` returns `Some`, generation uses a single socket request with the schema. When `None` (after `map`/`filter` on non-basic generators, or `flat_map`), it falls back to multiple requests wrapped in spans for shrinking.

Key insight: `map()` on a `BasicGenerator` preserves the schema by composing the transform function, rather than losing it. This is the central optimization.

### Thread-Local State

`TestCaseData` is stored in thread-local `TEST_CASE_DATA` and holds the socket connection, stream, and span depth. `IS_LAST_RUN` tracks whether this is the final replay for counterexample output.

### Span System

Spans (`start_span`/`stop_span`) group related generation calls so Hypothesis can shrink effectively. Labels in `generators::labels` identify span types (LIST, TUPLE, ONE_OF, FILTER, etc.).

### Collections

Server-managed collections use `new_collection`/`collection_more`/`collection_reject` protocol commands. The `Collection` struct in `collections.rs` handles dynamic sizing via the `more()` protocol with lazy initialization.

## Key Patterns

### Adding a New Generator

1. Create a builder struct with configuration fields
2. Implement `Generate<T>` with `do_draw()` and optionally `as_basic()`
3. Export a factory function from `generators/mod.rs`
4. If the generated type should work with `#[derive(Generate)]`, implement `DefaultGenerator` in `generators/default.rs`

### Derive Macro

`#[derive(Generate)]` (in `hegel-macros/`) creates a `<Type>Generator` struct with:
- `new()`: Uses `DefaultGenerator` for all fields
- `<field>(gen)`: Builder methods to customize field generators

For enums, it also creates `<Enum><Variant>Generator` for each data variant. Implementation is split across `struct_gen.rs`, `enum_gen.rs`, and `utils.rs`.

### Testing Conventions

- All tests go in `tests/`, never inline in source files. Tests that don't need access to private functions go directly in `tests/` as integration tests. Tests that need access to private functions go in `tests/embedded/`, mirroring the `src/` directory structure (e.g. `src/protocol/packet.rs` → `tests/embedded/protocol/packet_tests.rs`). Embedded tests are included as child modules of their source file via `#[cfg(test)] #[path = "..."] mod tests;`, which gives them access to private items through `use super::*`. This keeps test code out of source files while preserving access to internals that Rust would otherwise forbid.
- Always import generators as `use hegel::generators as gs;` (or `use hegel::generators::{self as gs, Generator};` when the `Generator` trait is needed). Use `gs::` in all generator calls, e.g. `gs::booleans()`, `gs::integers::<i32>()`. This applies to code inside string literals (e.g. `TempRustProject` snippets) as well.
- When a test needs a throwaway generator, prefer `gs::booleans()` as the simplest option (unless the test needs a larger value space).
- In test code, prefer `.unwrap()` over `.expect("static message")`. A static expect message rarely adds information beyond what the panic already provides (error type + source location). Only use `.expect()` when the message includes a formatted value that aids debugging (e.g., `.expect(&format!("failed to open {}", path))`).
- When a return value isn't used, don't bind it to `_` — just call the function as a bare statement. Only use `let _ =` when needed to suppress a `#[must_use]` warning.

### Code Coverage

This project enforces 100% line coverage for new code. You may not add `// nocov` annotations without explicit human permission. See the `coverage` skill for full details on the coverage philosophy, ratchet mechanism, and how to make code testable.

**CRITICAL: You MUST NOT increase the numbers in `.github/coverage-ratchet.json` without first asking for and then receiving explicit human permission to do so.**

### Conformance Tests

Located in `tests/conformance/`. Rust test binaries in `tests/conformance/rust/src/bin/` are invoked by a Python test runner (`tests/conformance/test_conformance.py`) that validates generators produce values matching their declared constraints.