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. Clones share the same underlying backend connection — they are views onto one test case, not independent test cases.

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 n = handle.join().unwrap();
    let _b: bool = tc.draw(gs::booleans());
    let _ = n;
}

§What is guaranteed

Individual backend operations (a single generate, start_span, stop_span, or pool/collection call) are serialised by a shared mutex, so the bytes on the wire to the backend stay well-formed no matter how clones are used across threads.

This is enough for patterns where threads do not race on generation — for example:

  • Spawn a worker, let it draw, join it, then continue on the main thread.
  • Repeatedly spawn-and-join one worker at a time.
  • Any pattern where exactly one thread is drawing at a time, with a happens-before relationship (join, channel receive, barrier) between each thread’s work.

§What is not guaranteed

Concurrent generation will get progressively better over time, but right now should be considered a borderline-internal feature. If you do not know exactly what you’re doing it probably won’t work.

Two or more threads drawing concurrently from clones of the same TestCase is allowed by the type system but is not deterministic: the order in which draws interleave depends on thread scheduling, and the backend has no way to reproduce that order on replay. Composite draws are also not atomic with respect to other threads — another thread’s draws can land between this thread’s start_span and stop_span, corrupting the shrink-friendly span structure. In practice this means such tests may:

  • Produce different values on successive runs of the same seed.
  • Shrink poorly or not at all.
  • Surface backend errors (e.g. StopTest) in one thread caused by another thread’s draws exhausting the budget.

§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 72)
71    fn push(&mut self, tc: TestCase) {
72        let element = tc.draw(gs::integers::<i32>());
73        self.stack.push(element);
74    }
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 51)
50    fn create_account(&mut self, tc: TestCase) {
51        let account = tc.draw(gs::text().min_size(1));
52        tc.note(&format!("create account '{}'", account.clone()));
53        self.accounts.add(account);
54    }
55
56    #[rule]
57    fn credit(&mut self, tc: TestCase) {
58        let account = self.accounts.draw().clone();
59        let amount = tc.draw(gs::integers::<i64>().min_value(0).max_value(LIMIT));
60        tc.note(&format!("credit '{}' with {}", account.clone(), amount));
61        self.ledger.credit(account, amount);
62    }
63
64    #[rule]
65    fn transfer(&mut self, tc: TestCase) {
66        let from = self.accounts.draw().clone();
67        let to = self.accounts.draw().clone();
68        let amount = tc.draw(gs::integers::<i64>().min_value(0).max_value(LIMIT));
69        tc.note(&format!(
70            "transfer '{}' from {} to {}",
71            amount,
72            from.clone(),
73            to.clone()
74        ));
75        self.ledger.transfer(from, to, amount);
76    }
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.

Only prints during the final replay of a failing 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 52)
50    fn create_account(&mut self, tc: TestCase) {
51        let account = tc.draw(gs::text().min_size(1));
52        tc.note(&format!("create account '{}'", account.clone()));
53        self.accounts.add(account);
54    }
55
56    #[rule]
57    fn credit(&mut self, tc: TestCase) {
58        let account = self.accounts.draw().clone();
59        let amount = tc.draw(gs::integers::<i64>().min_value(0).max_value(LIMIT));
60        tc.note(&format!("credit '{}' with {}", account.clone(), amount));
61        self.ledger.credit(account, amount);
62    }
63
64    #[rule]
65    fn transfer(&mut self, tc: TestCase) {
66        let from = self.accounts.draw().clone();
67        let to = self.accounts.draw().clone();
68        let amount = tc.draw(gs::integers::<i64>().min_value(0).max_value(LIMIT));
69        tc.note(&format!(
70            "transfer '{}' from {} to {}",
71            amount,
72            from.clone(),
73            to.clone()
74        ));
75        self.ledger.transfer(from, to, amount);
76    }
examples/min_stack.rs (line 81)
77    fn pop(&mut self, tc: TestCase) {
78        let element = self.stack.pop();
79        match element {
80            Some(element) => {
81                tc.note(&format!("pop {}", element));
82            }
83            _ => {
84                tc.note("pop nothing");
85            }
86        }
87    }
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 · 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.