I am trying to test state-transitions for a state-machine. The following modes are available:
Operational
Standby
Off
Failure
Consider that I want to organize these like a Boolean truth table like matrix with an objective to test possible transitions between different states.
Each transition itself could be triggered by discrete sub-conditions.
I have segregated boilerplate code for initialization into a fixture
and have written my test cases as respective files for transitions, for eg: standby_to_operational.rs
. Each of these files themselves contain sub-conditions
which could facilitate the respective transition.
Please find skeleton implementation:
common.rs:
pub mod utils {
use rstest::fixture;
pub struct TestParams {
/// Calibration object
pub calib: Calibration,
// Other members
// ...
/// State object
pub state: State,
}
#[fixture]
pub fn init() -> TestParams {
let calib = Calibration::new();
// Other members
// ...
TestParams {
calib,
// Other members
// ...
State::new()
}
}
pub fn test_transition(
name: &str,
mut test_params: TestParams,
before: (&dyn Fn(&mut TestParams), State),
after: (&dyn Fn(&mut TestParams), State),
) {
println!();
println!(
"====================== {}::before() =====================",
name
);
println!("{:>2} => {:?}", 0, test_params.state);
before.0(&mut test_params);
println!();
assert_eq!(test_params.state, before.1);
println!(
"====================== {}::after() ======================",
name
);
println!("{:>2} => {:?}", 0, test_params.state);
after.0(&mut test_params);
println!();
assert_eq!(test_params.state, after.1);
}
}
standby_to_operational.rs:
#[cfg(test)]
use rstest::rstest;
#[rstest]
#[case::condition_1(
"condition_1",
&|test_params: &mut Elka| {
let calib = &mut test_params.calib;
let other = &mut test_params.other;
let state = &mut test_params.state;
// Modify calibration
calib.parameter_xyz = 0.01_f32;
// Force to move to Standby state
other.variable = true;
// NotActive => Standby
for _n in 1..=5 {
state.update(calib, myego);
state.process();
println!(
"{:>2} => {:?}", _n,
state
);
}
},
&|test_params: &mut Elka| {
let calib = &mut test_params.calib;
let other = &mut test_params.other;
let state = &mut test_params.state;
// Modify calibration
calib.parameter_xyz = 0.01_f32;
// Remove Inhibition Condition
other.variable = false;
// NotActive => Standby
for _n in 1..=5 {
state.update(calib, myego);
state.process();
println!(
"{:>2} => {:?}", _n,
state
);
}
},
)]
#[case::condition_2(
"condition_2",
&|test_params: &mut Elka| {
let calib = &mut test_params.calib;
let other = &mut test_params.other;
let state = &mut test_params.state;
// Modify calibration
calib.parameter_abc = 0.01_f32;
// Force to move to Standby state
other.variable_1 = true;
// NotActive => Standby
for _n in 1..=5 {
state.update(calib, myego);
state.process();
println!(
"{:>2} => {:?}", _n,
state
);
}
},
&|test_params: &mut Elka| {
let calib = &mut test_params.calib;
let other = &mut test_params.other;
let state = &mut test_params.state;
// Modify calibration
calib.parameter_abc = 0.03_f32;
// Remove Inhibition Condition
other.variable_1 = false;
// NotActive => Standby
for _n in 1..=5 {
state.update(calib, myego);
state.process();
println!(
"{:>2} => {:?}", _n,
state
);
}
},
)]
fn standby_to_operational(
init: Elka,
#[case] name: &str,
#[case] before: &dyn Fn(&mut Elka),
#[case] after: &dyn Fn(&mut Elka),
) {
let test_params = init;
test_ot_transition(
name,
test_params,
(before, State::On((OnState::StandBy, OnState::StandBy))),
(
after,
State::On((
OnState::Operational(OperationalState::Active),
OnState::Operational(OperationalState::NotActive),
)),
),
);
}
It looks okay for repeat transitions, but some requirements are specified in a more obscure way, so creating a dedicated file to cover just one odd condition looks like an overhead. Consider cases like:
pub enum FailureState {
/// Temporary Failure
Temporary,
/// Permanent Failure
Permanent,
}
/// Note: This is for 2 sides
pub enum OnState {
/// ON and in Stand by mode
StandBy,
/// Operational and Warning is sent
Operational(OperationalState),
}
/// Operational state
pub enum OperationalState {
/// Active State
Active,
/// Not Active state
NotActive,
}
Is there a better way to approach the problem, or does rust
provide some kind of construct to facilitate this? thanks.