Skip to main content

TestCase

Struct TestCase 

Source
pub struct TestCase { /* private fields */ }
Expand description

A handle to the current test case.

This is passed to #[hegel::test] functions and provides methods for drawing values, making assumptions, and recording notes.

§Example

use hegel::generators as gs;

#[hegel::test]
fn my_test(tc: hegel::TestCase) {
    let x: i32 = tc.draw(gs::integers());
    tc.assume(x > 0);
    tc.note(&format!("x = {}", x));
}

§Threading

TestCase is Send but not Sync. To drive generation from another thread, clone the test case and move the clone. Each clone generates from its own independent stream of choices: draws on one clone never perturb the values any other clone (or the original) produces, so several threads can generate concurrently and the test stays fully deterministic — the same seed replays the same values on every stream, failures shrink normally, and the shrunk counterexample replays exactly.

use hegel::generators as gs;

#[hegel::test]
fn my_test(tc: hegel::TestCase) {
    let tc_worker = tc.clone();
    let handle = std::thread::spawn(move || {
        tc_worker.draw(gs::integers::<i32>())
    });
    let _b: bool = tc.draw(gs::booleans());
    let n = handle.join().unwrap();
    let _ = n;
}

§What is guaranteed

Each clone owns its own stream, so a clone may be moved to and driven from another thread freely, concurrently with every other clone. A single clone may only be driven by one thread at a time — the backend rejects concurrent use of one handle outright — which is why you clone to hand work to a thread rather than sharing one TestCase across threads (the type is !Sync, so the compiler enforces this too).

The clones share the test case’s outcome: the whole family passes, fails, or is rejected as one test case, and the choice budget is shared across all streams. Everything else about generation is per-stream.

§What is not guaranteed

Determinism extends exactly as far as your own code’s determinism. If threads race on your state — for example, which of two clones first consumes a value from a shared queue — Hegel replays each stream’s values faithfully, but your test may still behave differently run to run, and such failures may not reproduce or shrink well.

Variable pools and engine-managed collections are shared across clones (an id from one clone works on any other). Using one such object from two threads at the same time makes the affected draws depend on scheduling order, which brings back the same replay caveat.

§Panics inside spawned threads

If a worker thread panics with an assumption failure or a backend StopTest, that panic stays inside the thread’s JoinHandle until the main thread joins it. The main thread is responsible for propagating (or suppressing) the panic — typically by calling handle.join().unwrap(), which resumes the panic on the main thread so Hegel’s runner can observe it.

Implementations§

Source§

impl TestCase

Source

pub fn draw<T: Debug>(&self, generator: impl Generator<T>) -> T

Draw a value from a generator.

§Example
use hegel::generators as gs;

#[hegel::test]
fn my_test(tc: hegel::TestCase) {
    let x: i32 = tc.draw(gs::integers());
    let s: String = tc.draw(gs::text());
}

Note: when run inside a #[hegel::test], draw() will typically be rewritten to __draw_named() with an appropriate variable name in order to give better test output.

Examples found in repository?
examples/min_stack.rs (line 64)
63    fn push(&mut self, tc: TestCase) {
64        let element = tc.draw(gs::integers::<i32>());
65        self.stack.push(element);
66    }
More examples
Hide additional examples
examples/stack.rs (line 15)
13    fn push(&mut self, tc: TestCase) {
14        let integers = gs::integers::<i32>;
15        let element = tc.draw(integers());
16        self.stack.push(element);
17    }
18
19    #[rule]
20    fn pop(&mut self, _: TestCase) {
21        self.stack.pop();
22    }
23
24    #[rule]
25    fn pop_push(&mut self, tc: TestCase) {
26        let integers = gs::integers::<i32>;
27        let element = tc.draw(integers());
28        let initial = self.stack.clone();
29        self.stack.push(element);
30        let popped = self.stack.pop().unwrap();
31        assert_eq!(popped, element);
32        assert_eq!(self.stack, initial);
33    }
examples/ledger.rs (line 50)
49    fn create_account(&mut self, tc: TestCase) {
50        let account = tc.draw(gs::text().min_size(1));
51        tc.note(&format!("create account '{}'", account.clone()));
52        self.accounts.add(account);
53    }
54
55    #[rule]
56    fn credit(&mut self, tc: TestCase) {
57        let account = tc.draw(self.accounts.values_reusable()).clone();
58        let amount = tc.draw(gs::integers::<i64>().min_value(0).max_value(LIMIT));
59        tc.note(&format!("credit '{}' with {}", account.clone(), amount));
60        self.ledger.credit(account, amount);
61    }
62
63    #[rule]
64    fn transfer(&mut self, tc: TestCase) {
65        let from = tc.draw(self.accounts.values_reusable()).clone();
66        let to = tc.draw(self.accounts.values_reusable()).clone();
67        let amount = tc.draw(gs::integers::<i64>().min_value(0).max_value(LIMIT));
68        tc.note(&format!(
69            "transfer '{}' from {} to {}",
70            amount,
71            from.clone(),
72            to.clone()
73        ));
74        self.ledger.transfer(from, to, amount);
75    }
Source

pub fn __draw_named<T: Debug>( &self, generator: impl Generator<T>, name: &str, repeatable: bool, ) -> T

Draw a value from a generator with a specific name for output.

When repeatable is true, a counter suffix is appended (e.g. x_1, x_2). When repeatable is false, reusing the same name panics.

Using the same name with different values of repeatable is an error.

On the final replay of a failing test case, this prints:

  • let name = value; (when not repeatable)
  • let name_N = value; (when repeatable)

Not intended for direct use. This is the target that #[hegel::test] rewrites draw() calls to where appropriate.

Source

pub fn draw_silent<T>(&self, generator: impl Generator<T>) -> T

Draw a value from a generator without recording it in the output.

Unlike draw, this does not require T: Debug and will not print the value in the failing-test summary.

Source

pub fn assume(&self, condition: bool)

Assume a condition is true. If false, reject the current test input.

§Example
use hegel::generators as gs;

#[hegel::test]
fn my_test(tc: hegel::TestCase) {
    let age: u32 = tc.draw(gs::integers());
    tc.assume(age >= 18);
}
Examples found in repository?
examples/stack.rs (line 39)
36    fn push_pop(&mut self, tc: TestCase) {
37        let initial = self.stack.clone();
38        let element = self.stack.pop();
39        tc.assume(element.is_some());
40        let element = element.unwrap();
41        self.stack.push(element);
42        assert_eq!(self.stack, initial);
43    }
Source

pub fn reject(&self) -> !

Reject the current test input unconditionally.

Equivalent to assume(false), but with a ! return type so that code following the call is statically known to be unreachable.

§Example
use hegel::generators as gs;

#[hegel::test]
fn my_test(tc: hegel::TestCase) {
    let n: i32 = tc.draw(gs::integers());
    let positive: u32 = match u32::try_from(n) {
        Ok(v) => v,
        Err(_) => tc.reject(),
    };
    let _ = positive;
}
Source

pub fn note(&self, message: &str)

Note a message which will be displayed with the reported failing test case.

At the default verbosity, only prints during the final replay of a failing test case. At Verbose or higher, prints on every test case.

§Example
use hegel::generators as gs;

#[hegel::test]
fn my_test(tc: hegel::TestCase) {
    let x: i32 = tc.draw(gs::integers());
    tc.note(&format!("Generated x = {}", x));
}
Examples found in repository?
examples/die_hard.rs (line 55)
54    fn die_hard_problem_not_solved(&mut self, tc: TestCase) {
55        tc.note(&format!("small / big = {0} / {1}", self.small, self.big));
56        assert!(self.big != 4);
57    }
More examples
Hide additional examples
examples/ledger.rs (line 51)
49    fn create_account(&mut self, tc: TestCase) {
50        let account = tc.draw(gs::text().min_size(1));
51        tc.note(&format!("create account '{}'", account.clone()));
52        self.accounts.add(account);
53    }
54
55    #[rule]
56    fn credit(&mut self, tc: TestCase) {
57        let account = tc.draw(self.accounts.values_reusable()).clone();
58        let amount = tc.draw(gs::integers::<i64>().min_value(0).max_value(LIMIT));
59        tc.note(&format!("credit '{}' with {}", account.clone(), amount));
60        self.ledger.credit(account, amount);
61    }
62
63    #[rule]
64    fn transfer(&mut self, tc: TestCase) {
65        let from = tc.draw(self.accounts.values_reusable()).clone();
66        let to = tc.draw(self.accounts.values_reusable()).clone();
67        let amount = tc.draw(gs::integers::<i64>().min_value(0).max_value(LIMIT));
68        tc.note(&format!(
69            "transfer '{}' from {} to {}",
70            amount,
71            from.clone(),
72            to.clone()
73        ));
74        self.ledger.transfer(from, to, amount);
75    }
examples/min_stack.rs (line 73)
69    fn pop(&mut self, tc: TestCase) {
70        let element = self.stack.pop();
71        match element {
72            Some(element) => {
73                tc.note(&format!("pop {}", element));
74            }
75            _ => {
76                tc.note("pop nothing");
77            }
78        }
79    }
Source

pub fn target(&self, score: f64)

Record a targeting observation to help the engine find extreme inputs.

Call this inside a test body to guide generation toward inputs that maximise score. Inside a #[hegel::test], #[hegel::main], or #[hegel::standalone_function] body, tc.target(expr) is rewritten to call target_labelled with the source text of expr as the label, so different targeting expressions are tracked separately by default. Outside that rewrite, tc.target(score) uses the empty label.

Has no effect during replays or if the test case has been aborted.

§Example
use hegel::generators as gs;

#[hegel::test]
fn my_test(tc: hegel::TestCase) {
    let n: u32 = tc.draw(gs::integers::<u32>());
    tc.target(n as f64);
}
Source

pub fn target_labelled(&self, score: f64, label: impl Into<String>)

Record a targeting observation under an explicit label.

The label distinguishes multiple simultaneous targeting goals. Use this directly when you want a specific label string; target is the usual entry point and will be rewritten to call this with the source expression as the label inside a #[hegel::test] body.

Has no effect during replays or if the test case has been aborted.

Source

pub fn repeat<F: FnMut()>(&self, body: F) -> !

Run body in a loop that should runs “logically infinitely” or until error. Roughly equivalent to a loop but with better interaction with the test runner: This loop will never exit until the test case completes.

At the start of each iteration a // Loop iteration N note is emitted into the failing-test replay output.

§Example
use hegel::generators as gs;

#[hegel::test]
fn my_test(tc: hegel::TestCase) {
    let mut total: i32 = 0;
    tc.repeat(|| {
        let n: i32 = tc.draw(gs::integers().min_value(0).max_value(10));
        total += n;
        assert!(total >= 0);
    });
}

Trait Implementations§

Source§

impl Clone for TestCase

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for TestCase

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.