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
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 }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 }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.
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?
More examples
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 }Sourcepub fn target(&self, score: f64)
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);
}Sourcepub fn target_labelled(&self, score: f64, label: impl Into<String>)
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.
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);
});
}