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 (one created through any 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>(&self, generator: impl PrintableGenerator<T>) -> T
pub fn draw<T>(&self, generator: impl PrintableGenerator<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.
Requires a PrintableGenerator so the drawn value’s representation
can be reported with a failing test case; to draw from a plain
Generator, use draw_silent, or make the
generator printable with
print_as_value,
print_as_debug, or
print_with.
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 }71 fn register(&self, tc: TestCase) {
72 let key = tc.draw(gs::integers::<u64>().max_value(3));
73 if self.store.put_if_absent(key, 0) {
74 self.keys.add(&tc, key);
75 }
76 }
77
78 #[rule(group = "rw")]
79 fn increment(&self, tc: TestCase) {
80 let key = tc.draw(self.keys.values_reusable());
81 self.store.increment(key);
82 self.increments.fetch_add(1, Ordering::SeqCst);
83 }
84
85 #[rule(group = "rw")]
86 fn read(&self, tc: TestCase) {
87 let key = tc.draw(self.keys.values_reusable());
88 let value = self.store.get(key);
89 tc.note(&format!("read {key} -> {value:?}"));
90 }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>(
&self,
generator: impl PrintableGenerator<T>,
name: &str,
repeatable: bool,
) -> T
pub fn __draw_named<T>( &self, generator: impl PrintableGenerator<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
Sourcepub fn draw_and_print<T>(
&self,
generator: impl PrintableGenerator<T>,
printer: &mut PrettyPrinter,
) -> T
pub fn draw_and_print<T>( &self, generator: impl PrintableGenerator<T>, printer: &mut PrettyPrinter, ) -> T
Draw a value from a generator, printing its representation to
printer as it is drawn.
This is how a compositional PrintableGenerator draws an inner
generator from its own
do_draw_and_print (and how
draw runs its argument): routing every inner draw
through this one entry point keeps the printed region of a draw a
framework concern rather than something each generator re-implements.
A generator that merely forwards to an inner printable generator
without printing or drawing anything itself should call the inner
generator’s do_draw_and_print directly instead, so the forwarding
layer doesn’t register as a second region.
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 event(&self, label: impl AsRef<str>)
pub fn event(&self, label: impl AsRef<str>)
Record an event for the end-of-run statistics report.
With statistics enabled
(Settings::show_statistics, or
the HEGEL_STATISTICS environment variable), the end of the run
reports, per label, the fraction of generation-phase test cases in
which the label was recorded at least once — a quick check that the
interesting situations in a test actually occur. With statistics
off, events cost almost nothing and report nothing.
§Example
use hegel::generators as gs;
#[hegel::test]
fn my_test(tc: hegel::TestCase) {
let xs: Vec<u8> = tc.draw(gs::vecs(gs::integers()));
if xs.is_empty() {
tc.event("empty input");
}
}Sourcepub fn event_value(&self, label: impl AsRef<str>, value: f64)
pub fn event_value(&self, label: impl AsRef<str>, value: f64)
Record a numeric observation under label for the end-of-run
statistics report.
With statistics enabled
(Settings::show_statistics, or
the HEGEL_STATISTICS environment variable), the end of the run
reports, per label, a summary of the observed distribution — count,
min, median, mean, p90, max — over generation-phase test cases, so a test
can check the sizes or shapes it actually exercises. Unlike
target, a label may be observed any number of
times per test case, and the observations do not steer generation.
value must be finite.
§Example
use hegel::generators as gs;
#[hegel::test]
fn my_test(tc: hegel::TestCase) {
let xs: Vec<u8> = tc.draw(gs::vecs(gs::integers()));
tc.event_value("input length", xs.len() as f64);
}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);
});
}