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,
joinit, 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
impl TestCase
Sourcepub fn draw<T: Debug>(&self, generator: impl Generator<T>) -> T
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?
More examples
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 }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 }Sourcepub fn __draw_named<T: Debug>(
&self,
generator: impl Generator<T>,
name: &str,
repeatable: bool,
) -> T
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.
Sourcepub fn draw_silent<T>(&self, generator: impl Generator<T>) -> T
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.
Sourcepub fn assume(&self, condition: bool)
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);
}Sourcepub fn reject(&self) -> !
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;
}Sourcepub fn note(&self, message: &str)
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?
More examples
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 }Sourcepub fn repeat<F: FnMut()>(&self, body: F) -> !
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);
});
}