use crate::TestCase;
use crate::control::{
AssumeFailed, InternalError, InvalidArgument, LoopDone, StopTest, hegel_internal_assert,
raise_control, with_test_context,
};
use crate::ffi::{PoolHandle, StateMachineHandle};
use crate::generators::Generator;
use crate::run_lifecycle::{self, PanicInfo};
use crate::test_case::{labels, raise_for_rc};
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
use std::sync::{Mutex, mpsc};
pub const ANONYMOUS_GROUP: &str = "<anonymous>";
thread_local! {
static WORKER_INDEX: Cell<Option<usize>> = const { Cell::new(None) };
}
pub(crate) fn current_worker_index() -> Option<usize> {
WORKER_INDEX.with(|cell| cell.get())
}
pub struct Rule<M: ?Sized> {
pub name: String,
pub apply: fn(&mut M, TestCase),
}
impl<M> Rule<M> {
pub fn new(name: &str, apply: fn(&mut M, TestCase)) -> Self {
Rule {
name: name.to_string(),
apply,
}
}
}
pub struct Pool<T> {
pool: crate::ffi::PoolHandle,
tc: TestCase,
values: HashMap<i64, T>,
}
fn pool_generate(tc: &TestCase, pool: &crate::ffi::PoolHandle, consume: bool) -> i64 {
match tc.with_ctc(|ctc| ctc.pool_generate(pool, consume)) {
Ok(id) => id,
Err(rc) => raise_for_rc(rc),
}
}
impl<T> Pool<T> {
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn add(&mut self, v: T) {
let variable_id: i64 = match self.tc.with_ctc(|ctc| ctc.pool_add(&self.pool)) {
Ok(id) => id,
Err(rc) => raise_for_rc(rc),
};
if self.values.contains_key(&variable_id) {
panic!("unexpected variable id in map"); }
self.values.insert(variable_id, v);
}
pub fn values_reusable(&self) -> ValuesReusable<'_, T> {
ValuesReusable {
pool: &self.pool,
values: &self.values,
}
}
pub fn values_consumed(&mut self) -> ValuesConsumed<'_, T> {
ValuesConsumed {
pool: &self.pool,
values: RefCell::new(&mut self.values),
}
}
}
pub struct ValuesReusable<'a, T> {
pool: &'a crate::ffi::PoolHandle,
values: &'a HashMap<i64, T>,
}
impl<'a, T> Generator<&'a T> for ValuesReusable<'a, T> {
fn do_draw(&self, tc: &TestCase) -> &'a T {
tc.assume(!self.values.is_empty());
let variable_id = pool_generate(tc, self.pool, false);
self.values.get(&variable_id).unwrap()
}
}
impl<'a, T: crate::PrettyPrintable> crate::generators::PrintableGenerator<&'a T>
for ValuesReusable<'a, T>
{
fn do_draw_and_print(&self, tc: &TestCase, printer: &mut crate::PrettyPrinter) -> &'a T {
crate::generators::draw_and_print_value(self, tc, printer)
}
}
pub struct ValuesConsumed<'a, T> {
pool: &'a crate::ffi::PoolHandle,
values: RefCell<&'a mut HashMap<i64, T>>,
}
impl<T> Generator<T> for ValuesConsumed<'_, T> {
fn do_draw(&self, tc: &TestCase) -> T {
tc.assume(!self.values.borrow().is_empty());
let variable_id = pool_generate(tc, self.pool, true);
self.values.borrow_mut().remove(&variable_id).unwrap()
}
}
impl<T: crate::PrettyPrintable> crate::generators::PrintableGenerator<T> for ValuesConsumed<'_, T> {
fn do_draw_and_print(&self, tc: &TestCase, printer: &mut crate::PrettyPrinter) -> T {
crate::generators::draw_and_print_value(self, tc, printer)
}
}
pub fn pool<T>(tc: &TestCase) -> Pool<T> {
let pool = match tc.with_ctc(|ctc| ctc.new_pool()) {
Ok(handle) => handle,
Err(rc) => raise_for_rc(rc), };
Pool {
pool,
tc: tc.clone(),
values: HashMap::new(),
}
}
pub struct ConcurrentPool<T> {
handle: PoolHandle,
values: Mutex<HashMap<i64, T>>,
}
impl<T> ConcurrentPool<T> {
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<i64, T>> {
self.values.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn is_empty(&self) -> bool {
self.lock().is_empty()
}
pub fn len(&self) -> usize {
self.lock().len()
}
pub fn add(&self, tc: &TestCase, v: T) {
let mut values = self.lock();
match tc.with_ctc(|ctc| ctc.pool_add(&self.handle)) {
Ok(variable_id) => {
let previous = values.insert(variable_id, v);
hegel_internal_assert!(previous.is_none(), "unexpected variable id in map");
}
Err(rc) => {
drop(values);
raise_for_rc(rc)
}
}
}
pub fn values_reusable(&self) -> ConcurrentValuesReusable<'_, T> {
ConcurrentValuesReusable { pool: self }
}
pub fn values_consumed(&self) -> ConcurrentValuesConsumed<'_, T> {
ConcurrentValuesConsumed { pool: self }
}
}
pub struct ConcurrentValuesReusable<'a, T> {
pool: &'a ConcurrentPool<T>,
}
impl<T: Clone> Generator<T> for ConcurrentValuesReusable<'_, T> {
fn do_draw(&self, tc: &TestCase) -> T {
let values = self.pool.lock();
match tc.with_ctc(|ctc| ctc.pool_generate(&self.pool.handle, false)) {
Ok(variable_id) => values.get(&variable_id).unwrap().clone(),
Err(rc) => {
drop(values);
raise_for_rc(rc)
}
}
}
}
impl<T: Clone + crate::PrettyPrintable> crate::generators::PrintableGenerator<T>
for ConcurrentValuesReusable<'_, T>
{
fn do_draw_and_print(&self, tc: &TestCase, printer: &mut crate::PrettyPrinter) -> T {
crate::generators::draw_and_print_value(self, tc, printer)
}
}
pub struct ConcurrentValuesConsumed<'a, T> {
pool: &'a ConcurrentPool<T>,
}
impl<T> Generator<T> for ConcurrentValuesConsumed<'_, T> {
fn do_draw(&self, tc: &TestCase) -> T {
let mut values = self.pool.lock();
match tc.with_ctc(|ctc| ctc.pool_generate(&self.pool.handle, true)) {
Ok(variable_id) => values.remove(&variable_id).unwrap(),
Err(rc) => {
drop(values);
raise_for_rc(rc)
}
}
}
}
impl<T: crate::PrettyPrintable> crate::generators::PrintableGenerator<T>
for ConcurrentValuesConsumed<'_, T>
{
fn do_draw_and_print(&self, tc: &TestCase, printer: &mut crate::PrettyPrinter) -> T {
crate::generators::draw_and_print_value(self, tc, printer)
}
}
pub fn concurrent_pool<T>(tc: &TestCase) -> ConcurrentPool<T> {
let handle = match tc.with_ctc(|ctc| ctc.new_pool()) {
Ok(handle) => handle,
Err(rc) => raise_for_rc(rc),
};
ConcurrentPool {
handle,
values: Mutex::new(HashMap::new()),
}
}
pub trait StateMachine {
fn rules(&self) -> Vec<Rule<Self>>;
fn invariants(&self) -> Vec<Rule<Self>>;
}
fn check_invariants<M: StateMachine>(
m: &mut M,
invariants: &[Rule<M>],
tc: &TestCase,
machine: Option<&StateMachineHandle>,
) {
for (index, invariant) in invariants.iter().enumerate() {
if let Some(machine) = machine {
if !machine_should_check_invariant(tc, machine, index as i64) {
continue;
}
}
let inv_tc = tc.child(2); (invariant.apply)(m, inv_tc); }
}
fn machine_next_group(tc: &TestCase, machine: &StateMachineHandle) -> Option<usize> {
match tc.with_ctc(|ctc| ctc.state_machine_next_group(machine)) {
Ok(group) => group.map(|g| g as usize),
Err(rc) => raise_for_rc(rc),
}
}
fn machine_next_rule(
tc: &TestCase,
machine: &StateMachineHandle,
worker_index: i64,
) -> Option<i64> {
match tc.with_ctc(|ctc| ctc.state_machine_next_rule(machine, worker_index)) {
Ok(next) => next,
Err(rc) => raise_for_rc(rc),
}
}
fn machine_rule_rejected(tc: &TestCase, machine: &StateMachineHandle, worker_index: i64) {
if let Err(rc) = tc.with_ctc(|ctc| ctc.state_machine_rule_rejected(machine, worker_index)) {
raise_for_rc(rc);
}
}
fn machine_should_check_invariant(
tc: &TestCase,
machine: &StateMachineHandle,
invariant_index: i64,
) -> bool {
match tc.with_ctc(|ctc| ctc.state_machine_should_check_invariant(machine, invariant_index)) {
Ok(should_check) => should_check,
Err(rc) => raise_for_rc(rc),
}
}
pub fn run<M: StateMachine>(mut m: M, tc: TestCase) {
let rules = m.rules();
let rule_names: Vec<&str> = rules.iter().map(|r| r.name.as_str()).collect();
let rule_groups = vec![0i64; rules.len()];
let invariants = m.invariants();
let invariant_names: Vec<&str> = invariants.iter().map(|r| r.name.as_str()).collect();
let machine = match tc
.with_ctc(|ctc| ctc.new_state_machine(&rule_names, &rule_groups, &invariant_names, 1, 1))
{
Ok((handle, _)) => handle,
Err(rc) => raise_for_rc(rc),
};
tc.note("Initial invariant check.");
check_invariants(&mut m, &invariants, &tc, None);
let mut steps_attempted: i64 = 0;
loop {
tc.start_span(labels::STATEFUL_RULE);
if machine_next_group(&tc, &machine).is_none() {
tc.stop_span(false);
break;
}
let mut round_rejected = false;
while let Some(rule_index) = machine_next_rule(&tc, &machine, 0) {
hegel_internal_assert!(
(0..rules.len() as i64).contains(&rule_index),
"state_machine_next_rule returned out-of-range rule index {rule_index}"
);
let rule = &rules[rule_index as usize];
tc.note(&format!("Step {}: {}", steps_attempted + 1, rule.name));
let rule_tc = tc.child(2);
let thunk = || (rule.apply)(&mut m, rule_tc);
let result = catch_unwind(AssertUnwindSafe(thunk));
steps_attempted += 1;
match result {
Ok(()) => {}
Err(e) if e.downcast_ref::<AssumeFailed>().is_some() => {
machine_rule_rejected(&tc, &machine, 0);
round_rejected = true;
tc.note("Rule stopped early due to violated assumption.");
}
Err(e) => {
tc.stop_span(false);
resume_unwind(e)
}
};
}
tc.stop_span(round_rejected);
check_invariants(&mut m, &invariants, &tc, Some(&machine));
}
tc.note("Final invariant check.");
check_invariants(&mut m, &invariants, &tc, None);
}
pub struct ConcurrentRule<M: ?Sized> {
pub name: String,
pub group: String,
pub apply: fn(&M, TestCase),
}
impl<M> ConcurrentRule<M> {
pub fn new(name: &str, group: &str, apply: fn(&M, TestCase)) -> Self {
ConcurrentRule {
name: name.to_string(),
group: group.to_string(),
apply,
}
}
}
pub struct ConcurrentInvariant<M: ?Sized> {
pub name: String,
pub apply: fn(&M, TestCase),
}
impl<M> ConcurrentInvariant<M> {
pub fn new(name: &str, apply: fn(&M, TestCase)) -> Self {
ConcurrentInvariant {
name: name.to_string(),
apply,
}
}
}
pub trait ConcurrentStateMachine {
fn rules(&self) -> Vec<ConcurrentRule<Self>>;
fn invariants(&self) -> Vec<ConcurrentInvariant<Self>>;
}
fn check_concurrent_invariants<M: ConcurrentStateMachine + ?Sized>(
m: &M,
invariants: &[ConcurrentInvariant<M>],
tc: &TestCase,
machine: Option<&StateMachineHandle>,
) {
for (index, invariant) in invariants.iter().enumerate() {
if let Some(machine) = machine {
if !machine_should_check_invariant(tc, machine, index as i64) {
continue;
}
}
let inv_tc = tc.child(2);
(invariant.apply)(m, inv_tc);
}
}
enum WorkerEvent {
RoundDone,
Invalid,
Overrun,
ControlPayload(Box<dyn std::any::Any + Send>),
Panicked {
payload: Box<dyn std::any::Any + Send>,
info: Option<PanicInfo>,
},
Died,
}
fn classify_worker_unwind(e: Box<dyn std::any::Any + Send>) -> WorkerEvent {
if e.downcast_ref::<AssumeFailed>().is_some() {
return WorkerEvent::Invalid;
}
if e.downcast_ref::<StopTest>().is_some() {
return WorkerEvent::Overrun;
}
if e.downcast_ref::<InvalidArgument>().is_some()
|| e.downcast_ref::<InternalError>().is_some()
|| e.downcast_ref::<LoopDone>().is_some()
{
return WorkerEvent::ControlPayload(e);
}
WorkerEvent::Panicked {
payload: e,
info: run_lifecycle::take_panic_info(),
}
}
fn run_worker_round<M: ConcurrentStateMachine + ?Sized>(
worker: usize,
tc: &TestCase,
m: &M,
rules: &[ConcurrentRule<M>],
machine: &StateMachineHandle,
) -> WorkerEvent {
loop {
let next = catch_unwind(AssertUnwindSafe(|| {
let next = machine_next_rule(tc, machine, worker as i64);
if let Some(rule_index) = next {
hegel_internal_assert!(
(0..rules.len() as i64).contains(&rule_index),
"state_machine_next_rule returned out-of-range rule index {rule_index}"
);
}
next
}));
let rule_index = match next {
Ok(Some(rule_index)) => rule_index,
Ok(None) => return WorkerEvent::RoundDone,
Err(e) => return classify_worker_unwind(e),
};
let rule = &rules[rule_index as usize];
tc.note(&format!("Rule: {}", rule.name));
let rule_tc = tc.child(2);
let result = catch_unwind(AssertUnwindSafe(|| (rule.apply)(m, rule_tc)));
match result {
Ok(()) => {}
Err(e) => match classify_worker_unwind(e) {
WorkerEvent::Invalid => {
let rejected = catch_unwind(AssertUnwindSafe(|| {
machine_rule_rejected(tc, machine, worker as i64);
}));
if let Err(e) = rejected {
return classify_worker_unwind(e);
}
tc.note("Rule stopped early due to violated assumption.");
}
event => return event,
},
}
}
}
fn worker_loop<M: ConcurrentStateMachine + ?Sized>(
worker: usize,
m: &M,
rules: &[ConcurrentRule<M>],
machine: &StateMachineHandle,
capture_backtraces: bool,
rounds: mpsc::Receiver<TestCase>,
events: mpsc::Sender<WorkerEvent>,
) {
WORKER_INDEX.with(|cell| cell.set(Some(worker)));
run_lifecycle::set_backtrace_capture(capture_backtraces);
with_test_context(|| {
while let Ok(tc) = rounds.recv() {
let event = run_worker_round(worker, &tc, m, rules, machine);
if events.send(event).is_err() {
break;
}
}
});
}
pub fn run_concurrent<M: ConcurrentStateMachine + Sync>(
m: M,
tc: TestCase,
min_concurrency: i64,
max_concurrency: i64,
) {
let rules = m.rules();
let invariants = m.invariants();
let rule_names: Vec<&str> = rules.iter().map(|r| r.name.as_str()).collect();
let invariant_names: Vec<&str> = invariants.iter().map(|r| r.name.as_str()).collect();
let mut group_names: Vec<&str> = Vec::new();
let mut rule_groups: Vec<i64> = Vec::with_capacity(rules.len());
for rule in &rules {
let index = group_names
.iter()
.position(|name| *name == rule.group)
.unwrap_or_else(|| {
group_names.push(rule.group.as_str());
group_names.len() - 1
});
rule_groups.push(index as i64);
}
let (machine, concurrency) = match tc.with_ctc(|ctc| {
ctc.new_state_machine(
&rule_names,
&rule_groups,
&invariant_names,
min_concurrency,
max_concurrency,
)
}) {
Ok(created) => created,
Err(rc) => raise_for_rc(rc),
};
tc.note(&format!("Concurrency level: {concurrency}"));
tc.note("Initial invariant check.");
check_concurrent_invariants(&m, &invariants, &tc, None);
let capture_backtraces = run_lifecycle::backtrace_capture_enabled();
let concurrency = concurrency as usize;
let m = &m;
let rules = &rules;
let machine = &machine;
std::thread::scope(|scope| {
let mut round_txs: Vec<mpsc::Sender<TestCase>> = Vec::with_capacity(concurrency);
let mut event_rxs: Vec<mpsc::Receiver<WorkerEvent>> = Vec::with_capacity(concurrency);
for worker in 0..concurrency {
let (round_tx, round_rx) = mpsc::channel();
let (event_tx, event_rx) = mpsc::channel();
round_txs.push(round_tx);
event_rxs.push(event_rx);
scope.spawn(move || {
worker_loop(
worker,
m,
rules,
machine,
capture_backtraces,
round_rx,
event_tx,
);
});
}
let mut round = 0u64;
while let Some(group) = machine_next_group(&tc, machine) {
hegel_internal_assert!(
group < group_names.len(),
"state_machine_next_group returned unknown group id {group}"
);
round += 1;
tc.note(&format!(
"---------------- Round {round}: group {:?} ----------------",
group_names[group]
));
for tx in &round_txs {
let _ = tx.send(tc.clone());
}
let events: Vec<WorkerEvent> = event_rxs
.iter()
.map(|rx| rx.recv().unwrap_or(WorkerEvent::Died))
.collect();
resolve_round(events, &tc);
check_concurrent_invariants(m, &invariants, &tc, Some(machine));
}
tc.note("Final invariant check.");
check_concurrent_invariants(m, &invariants, &tc, None);
});
}
fn resolve_round(events: Vec<WorkerEvent>, tc: &TestCase) {
struct WorkerPanic {
worker: usize,
payload: Box<dyn std::any::Any + Send>,
info: Option<PanicInfo>,
}
let mut control: Option<Box<dyn std::any::Any + Send>> = None;
let mut saw_overrun = false;
let mut saw_invalid = false;
let mut panics: Vec<WorkerPanic> = Vec::new();
for (worker, event) in events.into_iter().enumerate() {
match event {
WorkerEvent::RoundDone => {}
WorkerEvent::Invalid => saw_invalid = true,
WorkerEvent::Overrun => saw_overrun = true,
WorkerEvent::ControlPayload(payload) => {
if control.is_none() {
control = Some(payload);
}
}
WorkerEvent::Panicked { payload, info } => panics.push(WorkerPanic {
worker,
payload,
info,
}),
WorkerEvent::Died => {
if control.is_none() {
control = Some(Box::new(InternalError(format!(
"Internal error in hegel: concurrent stateful worker {worker} exited \
without reporting an outcome. This is a bug in hegel itself; please \
report it at https://github.com/hegeldev/hegel-rust/issues"
))));
}
}
}
}
let note_dropped = |dropped: &[WorkerPanic]| {
for p in dropped {
let location = p
.info
.as_ref()
.map_or("<unknown>", |(_, _, location, _)| location.as_str());
tc.note(&format!(
"Dropped concurrent panic from worker {} at {}: {}",
p.worker,
location,
run_lifecycle::panic_message(&p.payload)
));
}
};
if let Some(payload) = control {
resume_unwind(payload);
}
if saw_overrun || saw_invalid {
note_dropped(&panics);
if saw_overrun {
raise_control(StopTest);
}
raise_control(AssumeFailed);
}
if !panics.is_empty() {
note_dropped(&panics[1..]);
let winner = panics.remove(0);
if let Some(info) = winner.info {
run_lifecycle::install_panic_info(info);
}
resume_unwind(winner.payload);
}
}
#[cfg(test)]
#[path = "../tests/embedded/stateful_tests.rs"]
mod tests;