Skip to main content

hegel/
runner.rs

1use crate::test_case::TestCase;
2
3/// Health checks that can be suppressed during test execution.
4///
5/// Health checks detect common issues with test configuration that would
6/// otherwise cause tests to run inefficiently or not at all.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8#[non_exhaustive]
9pub enum HealthCheck {
10    /// Too many test cases are being filtered out via `assume()`.
11    FilterTooMuch,
12    /// Test execution is too slow.
13    TooSlow,
14    /// Generated test cases are too large: they routinely reach the limit
15    /// on the number of choices a single test case may make. Suppressing
16    /// this check also removes that limit, so a test case can run
17    /// indefinitely; see [`Settings::suppress_health_check`].
18    TestCasesTooLarge,
19    /// The smallest natural input is very large.
20    LargeInitialTestCase,
21}
22
23impl HealthCheck {
24    /// Returns all health check variants.
25    ///
26    /// Useful for suppressing all health checks at once:
27    ///
28    /// ```no_run
29    /// use hegel::HealthCheck;
30    ///
31    /// #[hegel::test(suppress_health_check = HealthCheck::all())]
32    /// fn my_test(tc: hegel::TestCase) {
33    ///     // ...
34    /// }
35    /// ```
36    pub const fn all() -> [HealthCheck; 4] {
37        [
38            HealthCheck::FilterTooMuch,
39            HealthCheck::TooSlow,
40            HealthCheck::TestCasesTooLarge,
41            HealthCheck::LargeInitialTestCase,
42        ]
43    }
44}
45
46/// Controls which phases of the test lifecycle are executed.
47///
48/// By default, all phases run. Use [`Settings::phases`] to restrict which
49/// phases execute — for example, passing only `[Phase::Generate]` disables
50/// shrinking, which is useful when you only need to find a counterexample
51/// quickly and don't need the minimal one.
52///
53/// Corresponds to a subset of `hypothesis.Phase` (the `explain` phase is not
54/// yet supported in hegel-rust).
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56#[non_exhaustive]
57pub enum Phase {
58    /// Run explicit test cases added via `#[hegel::explicit_test_case]`.
59    Explicit,
60    /// Replay examples from the failure database.
61    Reuse,
62    /// Generate new random examples.
63    Generate,
64    /// Use targeting to guide generation toward interesting areas.
65    Target,
66    /// Shrink failing examples to a minimal counterexample.
67    Shrink,
68}
69
70/// Selects the source of randomness the engine draws from.
71///
72/// Mirrors Hypothesis's `backend` setting (specifically `backend="hypothesis"`
73/// vs `backend="hypothesis-urandom"`).
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75#[non_exhaustive]
76pub enum Backend {
77    /// The default: generate from a seeded pseudo-random generator. Runs are
78    /// reproducible from [`Settings::seed`] and shrinking/replay work as usual.
79    Default,
80    /// Take fresh entropy from outside the process on every draw, instead
81    /// of expanding a single PRNG seed.
82    ///
83    /// This exists for running under [Antithesis](https://antithesis.com/).
84    /// Sourcing every choice from its fuzzer hands it control over the
85    /// entire test case (rather than just the PRNG seed), so it can steer
86    /// and reproduce generation directly. Inside Antithesis (detected from
87    /// `ANTITHESIS_OUTPUT_DIR`) each draw asks the fuzzer through its
88    /// instrumentation library, `fuzz_get_random` from
89    /// `/usr/lib/libvoidstar.so`, the same function Antithesis's SDKs draw
90    /// from; when that library cannot be loaded, and everywhere outside
91    /// Antithesis, each draw reads `/dev/urandom` instead. The shipped
92    /// `workload` settings profile selects this backend.
93    ///
94    /// The generation algorithm is otherwise unchanged — only the random
95    /// source differs. On platforms without `/dev/urandom` (Windows) it falls
96    /// back to an OS-seeded PRNG. You almost certainly don't want this backend
97    /// unless you are running under Antithesis.
98    Urandom,
99}
100
101/// Controls how much output Hegel produces during test runs.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103#[non_exhaustive]
104pub enum Verbosity {
105    /// Suppress all output.
106    Quiet,
107    /// Default output level.
108    Normal,
109    /// Show more detail about the test run.
110    Verbose,
111    /// Show protocol-level debug information.
112    Debug,
113}
114
115/// How a run reacts when it detects nondeterministic test behavior — a test
116/// whose structure or outcome changes when the same choices are replayed.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118#[non_exhaustive]
119pub enum NondeterminismStrictness {
120    /// Switch to nondeterministic handling silently: failures are confirmed
121    /// by repeated replay before they are shrunk or persisted, and every
122    /// report carries a caveat quoting the run's replay evidence. An
123    /// unconfirmed failure still fails the run. The default.
124    Quiet,
125    /// Switch as under quiet, printing a one-line notice once per run.
126    Warn,
127    /// Abort the run with a flaky-test error, for suites that use determinism
128    /// as a lint.
129    Error,
130}
131
132/// Configuration for a Hegel test run.
133///
134/// Use builder methods to customize, then pass to [`Hegel::settings`] or
135/// the `settings` parameter of `#[hegel::test]`.
136///
137/// # Profiles
138///
139/// The values a `Settings` starts from come from a named *profile*,
140/// resolved by the engine. Two names are reserved: `base` is the immutable
141/// base settings, and `default` is the default profile — the one in effect
142/// when nothing names a profile, chosen by [`Settings::set_default_profile`],
143/// `HEGEL_DEFAULT_PROFILE`, or the `default` entry in `hegel.toml`, else by
144/// the environment (`workload` inside Antithesis, `ci` on a CI server,
145/// `development` locally). [`Settings::new`] resolves `default`;
146/// [`Settings::from_profile`] resolves a profile by name. Profiles are
147/// modified and defined in a `hegel.toml` at the package or workspace root,
148/// or registered with [`Settings::register_profile`].
149///
150/// The [`docs::settings`](crate::docs::settings) page covers the whole
151/// system: every setting and the layers it can be set in, the shipped
152/// profiles, inheritance, the `hegel.toml` format, and the programmatic
153/// API.
154#[derive(Debug, Clone)]
155pub struct Settings {
156    pub(crate) test_cases: u64,
157    pub(crate) verbosity: Verbosity,
158    pub(crate) nondeterminism_strictness: NondeterminismStrictness,
159    pub(crate) seed: Option<u64>,
160    pub(crate) derandomize: bool,
161    pub(crate) database: Database,
162    pub(crate) suppress_health_check: Vec<HealthCheck>,
163    pub(crate) phases: Vec<Phase>,
164    pub(crate) report_multiple_failures: bool,
165    pub(crate) stop_on_first_failure: bool,
166    pub(crate) show_statistics: bool,
167    pub(crate) print_blob: bool,
168    pub(crate) backend: Backend,
169}
170
171impl Settings {
172    /// Create settings from the `default` profile described in the
173    /// [profiles](Settings#profiles) section, with the settings environment
174    /// variables (`HEGEL_TEST_CASES`, `HEGEL_SEED`, …; see
175    /// [`docs::settings`](crate::docs::settings)) applied over it. Panics
176    /// when profile resolution fails: a default-profile setting names an
177    /// unknown profile, a `hegel.toml` is malformed, or one of those
178    /// variables holds a malformed value.
179    pub fn new() -> Self {
180        Self::from_resolution(crate::ffi::settings_from_profile(None))
181    }
182
183    /// Create settings from the named profile: reserved (`base`,
184    /// `default`), shipped (`development`, `ci`, `workload`), defined in
185    /// `hegel.toml`, or registered with [`Settings::register_profile`].
186    /// Selecting a profile does not change what the default profile is, and
187    /// the named profile still implicitly extends `default`, so it layers
188    /// over the environment's profile — except `base`, which is always the
189    /// plain base settings. The settings environment variables apply over
190    /// the result as for [`Settings::new`]. Panics when the profile is
191    /// unknown, a `hegel.toml` is malformed, or a variable holds a
192    /// malformed value; [`Settings::try_from_profile`] reports the failure
193    /// as an `Err` instead.
194    pub fn from_profile(name: &str) -> Self {
195        Self::from_resolution(Self::try_from_profile(name).map_err(|e| e.message))
196    }
197
198    /// [`Settings::from_profile`], reporting resolution failure as an `Err`
199    /// carrying the engine's diagnostic instead of panicking.
200    pub fn try_from_profile(name: &str) -> Result<Self, ProfileError> {
201        crate::ffi::settings_from_profile(Some(name)).map_err(|message| ProfileError { message })
202    }
203
204    fn from_resolution(resolution: Result<Self, String>) -> Self {
205        resolution.unwrap_or_else(|message| crate::test_case::invalid_argument!("{message}"))
206    }
207
208    /// Set the default profile for the whole process: the profile
209    /// [`Settings::new`] resolves and profiles without `extends` layer
210    /// over. Takes precedence over `HEGEL_DEFAULT_PROFILE` and the
211    /// `default` entry in `hegel.toml`; the `--profile` flag of a
212    /// `#[hegel::main]` binary calls this. The name is not required to
213    /// exist yet. An empty name clears the process override.
214    ///
215    /// Like [`Settings::register_profile`] this is not retroactive, so it
216    /// must run before the tests that should see it; under `cargo test`
217    /// prefer the `default` entry in `hegel.toml`.
218    ///
219    /// Panics when `name` is not a valid profile name.
220    pub fn set_default_profile(name: &str) {
221        if let Err(message) = crate::ffi::set_default_profile(Some(name)) {
222            crate::test_case::invalid_argument!("{message}");
223        }
224    }
225
226    /// Register a complete snapshot of `settings` as the profile `name`,
227    /// process-wide, replacing any earlier registration of the same name.
228    /// Registering a shipped profile's name replaces that profile; a
229    /// `hegel.toml` section for `name` still merges on top of the snapshot.
230    ///
231    /// Registration is not retroactive (settings values already created
232    /// keep their fields), so it must run before the tests that use the
233    /// profile. A `#[hegel::main]` binary or an embedding controls that
234    /// ordering. Under `cargo test` there is no reliable pre-test hook, so
235    /// prefer `hegel.toml` there.
236    ///
237    /// Panics when `name` is not a valid profile name (ASCII letters,
238    /// digits, `-` and `_`) or is one of the reserved names `base` and
239    /// `default`.
240    ///
241    /// # Example
242    ///
243    /// ```no_run
244    /// use hegel::Settings;
245    ///
246    /// Settings::register_profile("nightly", Settings::from_profile("ci").test_cases(10_000));
247    /// ```
248    pub fn register_profile(name: &str, settings: Settings) {
249        if let Err(message) = crate::ffi::register_profile(name, &settings) {
250            crate::test_case::invalid_argument!("{message}");
251        }
252    }
253
254    /// Select the randomness backend (base value: [`Backend::Default`]; the
255    /// shipped `workload` profile selects [`Backend::Urandom`]).
256    pub fn backend(mut self, backend: Backend) -> Self {
257        self.backend = backend;
258        self
259    }
260
261    /// Set the number of test cases to run (default: 100).
262    ///
263    /// The `HEGEL_TEST_CASES` environment variable, when set and non-empty,
264    /// changes the value a `Settings` starts from, so a whole suite can be
265    /// scaled up (a nightly deep run) or down (a quick smoke pass) without
266    /// editing source. A value set here, or via
267    /// `#[hegel::test(test_cases = ...)]`, is compiled in and takes
268    /// precedence over the variable.
269    pub fn test_cases(mut self, n: u64) -> Self {
270        self.test_cases = n;
271        self
272    }
273
274    /// Set the verbosity level.
275    pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
276        self.verbosity = verbosity;
277        self
278    }
279
280    /// Set how the run reacts when it detects nondeterministic test behavior
281    /// (default: [`NondeterminismStrictness::Quiet`]).
282    ///
283    /// The `HEGEL_NONDETERMINISM_STRICTNESS` environment variable, when set
284    /// and non-empty, changes the value a `Settings` starts from, whatever
285    /// the profile says: `quiet`, `warn` or `error`. A value set here takes
286    /// precedence over it.
287    pub fn nondeterminism_strictness(mut self, strictness: NondeterminismStrictness) -> Self {
288        self.nondeterminism_strictness = strictness;
289        self
290    }
291
292    /// Set a fixed seed for reproducibility, or `None` for random.
293    ///
294    /// A fixed seed takes precedence over [`derandomize`](Self::derandomize).
295    ///
296    /// The `HEGEL_SEED` environment variable, when set and non-empty,
297    /// changes the seed a `Settings` starts from: an integer is the seed to
298    /// use, and the literal value `none` clears one a profile set (matching
299    /// the `--seed` CLI flag's vocabulary). A seed set here, or via
300    /// `#[hegel::test(seed = ...)]`, takes precedence over the variable.
301    pub fn seed(mut self, seed: Option<u64>) -> Self {
302        self.seed = seed;
303        self
304    }
305
306    /// When true, use a fixed seed derived from the test name. Enabled by
307    /// the shipped `ci` profile. Has no effect when a fixed
308    /// [`seed`](Self::seed) is set.
309    ///
310    /// The `HEGEL_DERANDOMIZE` environment variable, when set and
311    /// non-empty, changes the value a `Settings` starts from, whatever the
312    /// profile says: `true`, `1` or `yes` turns it on and `false`, `0` or
313    /// `no` turns it off. A value set here takes precedence over it.
314    pub fn derandomize(mut self, derandomize: bool) -> Self {
315        self.derandomize = derandomize;
316        self
317    }
318
319    /// Set the database path for storing failing examples, or `None` to disable.
320    ///
321    /// The `HEGEL_DATABASE` environment variable, when set and non-empty,
322    /// changes the database a `Settings` starts from: the literal value
323    /// `disabled` turns the database off (matching the `--database` CLI
324    /// flag's keyword), and any other value is used as the database path.
325    /// A value set here takes precedence over it.
326    pub fn database(mut self, database: Option<String>) -> Self {
327        self.database = match database {
328            None => Database::Disabled,
329            Some(path) => Database::Path(path),
330        };
331        self
332    }
333
334    /// Set which test lifecycle phases to run.
335    ///
336    /// Defaults to all phases: `[Phase::Explicit, Phase::Reuse, Phase::Generate, Phase::Target, Phase::Shrink]`.
337    ///
338    /// Example — skip shrinking (useful when you only need a witness, not a
339    /// minimal counterexample):
340    ///
341    /// ```no_run
342    /// use hegel::{Phase, Settings};
343    ///
344    /// let s = Settings::new().phases([Phase::Reuse, Phase::Generate]);
345    /// ```
346    pub fn phases(mut self, phases: impl IntoIterator<Item = Phase>) -> Self {
347        self.phases = phases.into_iter().collect();
348        self
349    }
350
351    /// Print a copy-pasteable `#[hegel::reproduce_failure("…")]` line for the
352    /// counterexample when a test fails. Defaults to `true`: on CI, with the
353    /// database disabled, the blob is the way to reproduce a failure locally.
354    ///
355    /// The reproduce blob is always *attached* to a failure that has one, and
356    /// this setting only controls whether it is printed to the failure
357    /// output. A failure without a blob — an unconfirmed nondeterministic
358    /// failure reported caveat-only, or one reproduced from a blob replay —
359    /// prints no reproducer line. Has effect only on the native backend.
360    ///
361    /// The `HEGEL_PRINT_BLOB` environment variable, when set and non-empty,
362    /// changes the value a `Settings` starts from: `true`, `1` or `yes`
363    /// turns it on and `false`, `0` or `no` turns it off, so the reproducer
364    /// line can be turned on or off for one run without editing the test.
365    /// A value set here takes precedence over it.
366    pub fn print_blob(mut self, print_blob: bool) -> Self {
367        self.print_blob = print_blob;
368        self
369    }
370
371    /// Suppress one or more health checks so they do not cause test failure.
372    ///
373    /// Health checks detect common issues like excessive filtering or slow
374    /// tests. Use this to suppress specific checks when they are expected.
375    /// Replaces any previously configured suppressions, like [`Settings::phases`].
376    ///
377    /// Suppressing [`HealthCheck::TestCasesTooLarge`] also removes the limit
378    /// on the number of choices a single test case may make, which is
379    /// otherwise 2^20 (1,048,576). Every draw counts as at least one choice,
380    /// as does each element of a collection, each step of a state machine,
381    /// and each cloned [`TestCase`](crate::TestCase); a test case that
382    /// reaches the limit is stopped and discarded. Suppress the check for a
383    /// test case that is meant to run for a long time, such as a concurrent
384    /// state machine driven for hours:
385    ///
386    /// ```no_run
387    /// use hegel::{HealthCheck, TestCase};
388    /// use hegel::generators as gs;
389    ///
390    /// #[hegel::test(test_cases = 1, suppress_health_check = [HealthCheck::TestCasesTooLarge])]
391    /// fn soak(tc: TestCase) {
392    ///     for _ in 0..5_000_000 {
393    ///         let _: u8 = tc.draw(gs::integers());
394    ///     }
395    /// }
396    /// ```
397    ///
398    /// The engine records every choice a test case makes, so an unlimited
399    /// test case's memory grows with its length.
400    ///
401    /// # Example
402    ///
403    /// ```no_run
404    /// use hegel::{HealthCheck, Verbosity};
405    /// use hegel::generators as gs;
406    ///
407    /// #[hegel::test(suppress_health_check = [HealthCheck::FilterTooMuch, HealthCheck::TooSlow])]
408    /// fn my_test(tc: hegel::TestCase) {
409    ///     let n: i32 = tc.draw(gs::integers());
410    ///     tc.assume(n > 0);
411    /// }
412    /// ```
413    pub fn suppress_health_check(mut self, checks: impl IntoIterator<Item = HealthCheck>) -> Self {
414        self.suppress_health_check = checks.into_iter().collect();
415        self
416    }
417
418    /// Returns `true` if the given phase is enabled in these settings.
419    pub fn has_phase(&self, phase: Phase) -> bool {
420        self.phases.contains(&phase)
421    }
422
423    /// Print event statistics at the end of the run (default: off): for
424    /// each label recorded with [`TestCase::event`](crate::TestCase::event),
425    /// the fraction of generation-phase test cases it occurred in, and for
426    /// each label recorded with
427    /// [`TestCase::event_value`](crate::TestCase::event_value), a summary of
428    /// the observed distribution.
429    ///
430    /// The `HEGEL_STATISTICS` environment variable, when set to anything
431    /// but `"0"` or the empty string, turns this on in the settings a
432    /// `Settings` starts from, without editing source. A value set here
433    /// takes precedence over it.
434    pub fn show_statistics(mut self, show_statistics: bool) -> Self {
435        self.show_statistics = show_statistics;
436        self
437    }
438
439    /// The settings a `#[hegel::main]` binary runs with: one test case, with
440    /// the `TooSlow` and `TestCasesTooLarge` health checks suppressed, since
441    /// both measure how valid test cases accumulate over a run and a run of
442    /// one has nothing to measure. Suppressing `TestCasesTooLarge` also
443    /// removes the choice limit, which suits a binary's one test case: it is
444    /// typically meant to run for a long time.
445    pub(crate) fn for_single_test_case(mut self) -> Self {
446        self.test_cases = 1;
447        for check in [HealthCheck::TooSlow, HealthCheck::TestCasesTooLarge] {
448            if !self.suppress_health_check.contains(&check) {
449                self.suppress_health_check.push(check);
450            }
451        }
452        self
453    }
454
455    /// Control whether multi-bug runs report every distinct failing example
456    /// or collapse to just the first one.
457    ///
458    /// When `true`, each distinct origin Hegel finds is surfaced as its own
459    /// diagnostic, and the final panic message reports the count of distinct
460    /// failures.  When `false` (the default), Hegel collapses a multi-bug run
461    /// to one example — several superficially-distinct failures often share a
462    /// root cause, and the extra reports are just noise.
463    ///
464    /// Maps to Hypothesis's `report_multiple_bugs` setting.
465    pub fn report_multiple_failures(mut self, report_multiple_failures: bool) -> Self {
466        self.report_multiple_failures = report_multiple_failures;
467        self
468    }
469
470    /// Control whether finding a failure ends the generation phase.
471    ///
472    /// When `true` (the default), Hegel stops generating once it has found
473    /// a failure (after a short window to surface other distinct failures
474    /// when [`Settings::report_multiple_failures`] is on) and moves on to
475    /// shrinking and reporting it. When `false`, the run goes on to use its
476    /// whole `test_cases` budget before reporting, whatever it finds along
477    /// the way: a database replay that reproduces a failure no longer skips
478    /// generation, and every stored failure is replayed. Failing test cases
479    /// then count against the budget like valid ones, so a test that fails
480    /// on every input still ends.
481    ///
482    /// Every distinct failure found is persisted to the database either
483    /// way. What is shrunk and reported follows
484    /// [`Settings::report_multiple_failures`]: when that is `false` only
485    /// the smallest failure found is shrunk and reported, and the rest are
486    /// persisted as found.
487    ///
488    /// The `HEGEL_STOP_ON_FIRST_FAILURE` environment variable, when set and
489    /// non-empty, changes the value a `Settings` starts from, with the same
490    /// vocabulary as `HEGEL_DERANDOMIZE`. A value set here takes precedence
491    /// over it.
492    pub fn stop_on_first_failure(mut self, stop_on_first_failure: bool) -> Self {
493        self.stop_on_first_failure = stop_on_first_failure;
494        self
495    }
496}
497
498impl Default for Settings {
499    fn default() -> Self {
500        Self::new()
501    }
502}
503
504#[derive(Debug, Clone, PartialEq, Eq)]
505pub(crate) enum Database {
506    Unset,
507    Disabled,
508    Path(String),
509}
510
511/// Why [`Settings::try_from_profile`] could not resolve a profile: the name
512/// is unknown, or a `hegel.toml` is malformed. The `Display` impl carries
513/// the engine's diagnostic.
514#[derive(Debug, Clone)]
515pub struct ProfileError {
516    pub(crate) message: String,
517}
518
519impl std::fmt::Display for ProfileError {
520    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521        f.write_str(&self.message)
522    }
523}
524
525impl std::error::Error for ProfileError {}
526
527#[doc(hidden)]
528pub fn hegel<F>(test_fn: F)
529where
530    F: FnMut(TestCase),
531{
532    Hegel::new(test_fn).run();
533}
534
535/// The boolean vocabulary of the `--derandomize` flag, shared with the
536/// engine's `HEGEL_DERANDOMIZE` and `HEGEL_PRINT_BLOB` variables.
537pub(crate) fn parse_bool(s: &str) -> Option<bool> {
538    match s {
539        "true" | "1" | "yes" => Some(true),
540        "false" | "0" | "no" => Some(false),
541        _ => None,
542    }
543}
544
545/// Where a test is defined, as the `#[hegel::test]` and `#[hegel::main]`
546/// macros record it: passed to the engine, which reports the test's verdict
547/// against it when running under [Antithesis](https://antithesis.com/).
548#[doc(hidden)]
549pub struct TestLocation {
550    pub function: String,
551    pub file: String,
552    pub class: String,
553    pub begin_line: u32,
554}
555
556#[doc(hidden)]
557pub struct Hegel<F> {
558    test_fn: F,
559    database_key: Option<String>,
560    test_location: Option<TestLocation>,
561    settings: Settings,
562    reproduce_failure: Option<String>,
563    single_test_case: bool,
564}
565
566impl<F> Hegel<F>
567where
568    F: FnMut(TestCase),
569{
570    /// Create a new test builder with default settings.
571    pub fn new(test_fn: F) -> Self {
572        Self {
573            test_fn,
574            database_key: None,
575            settings: Settings::new(),
576            test_location: None,
577            reproduce_failure: None,
578            single_test_case: false,
579        }
580    }
581
582    /// Override the default settings.
583    pub fn settings(mut self, settings: Settings) -> Self {
584        self.settings = settings;
585        self
586    }
587
588    #[doc(hidden)]
589    pub fn __database_key(mut self, key: String) -> Self {
590        self.database_key = Some(key);
591        self
592    }
593
594    /// Run exactly one test case, the behavior of `#[hegel::main]` binaries.
595    /// Applied in [`run`](Self::run) over whatever the settings say, so
596    /// neither `test_cases` nor `HEGEL_TEST_CASES` (which only sets the
597    /// value a [`Settings`] starts from) can undo it. Also suppresses
598    /// [`HealthCheck::TooSlow`] and [`HealthCheck::TestCasesTooLarge`]: both
599    /// judge how a run accumulates valid test cases, which is meaningless
600    /// for a run of one.
601    #[doc(hidden)]
602    pub fn __single_test_case(mut self) -> Self {
603        self.single_test_case = true;
604        self
605    }
606
607    #[doc(hidden)]
608    pub fn test_location(mut self, location: TestLocation) -> Self {
609        self.test_location = Some(location);
610        self
611    }
612
613    /// Replay a failing example from a base64 failure blob instead of
614    /// generating fresh test cases.
615    ///
616    /// A failure blob encodes a counterexample: the choice sequence of a
617    /// deterministic failure, or the counterexample graph of a
618    /// nondeterministic one. A native failure prints one while
619    /// [`print_blob`](Settings::print_blob) is true. When set,
620    /// [`run`](Self::run) replays the blob — a deterministic blob up to four
621    /// times, each attempt free to draw fresh values past the recorded
622    /// choices, stopping at the first failure; a nondeterministic blob's
623    /// graph until a replay fails, under a replay budget — bypassing
624    /// generation and shrinking, so you can reproduce a CI failure locally.
625    ///
626    /// First-wins: if a blob is already set, further calls are ignored.
627    /// Stacked `#[hegel::reproduce_failure]` attributes lower to repeated
628    /// calls here, so only the first attribute replays; the rest are
629    /// bookkeeping to be deleted one by one as the failures are fixed.
630    pub fn reproduce_failure(mut self, blob: impl Into<String>) -> Self {
631        if self.reproduce_failure.is_none() {
632            self.reproduce_failure = Some(blob.into());
633        }
634        self
635    }
636
637    /// Run the property-based tests.
638    ///
639    /// Panics if any test case fails.
640    pub fn run(self) {
641        let mut settings = self.settings;
642        if self.single_test_case {
643            settings = settings.for_single_test_case();
644        }
645        if let Some(blob) = self.reproduce_failure {
646            crate::run_lifecycle::drive_blob_replay(
647                self.test_fn,
648                &settings,
649                self.database_key.as_deref(),
650                &blob,
651                self.test_location.as_ref(),
652            );
653            return;
654        }
655
656        crate::run_lifecycle::drive(
657            self.test_fn,
658            &settings,
659            self.database_key.as_deref(),
660            self.test_location.as_ref(),
661        );
662    }
663}
664
665#[cfg(test)]
666#[path = "../tests/embedded/runner_tests.rs"]
667mod tests;