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;