use crate::{Event, Machine};
use serde::{Deserialize, Serialize};
use std::collections::{HashSet, VecDeque};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TestCase {
pub name: String,
pub initial_state: String,
pub events: Vec<Event>,
pub expected_state: String,
}
#[derive(Debug)]
pub struct TestGenerator<'a> {
machine: &'a Machine,
visited_states: HashSet<String>,
}
impl<'a> TestGenerator<'a> {
pub fn new(machine: &'a Machine) -> Self {
Self {
machine,
visited_states: HashSet::new(),
}
}
pub fn generate_all_states(&mut self) -> Vec<TestCase> {
let mut test_cases = Vec::new();
let mut state_queue: VecDeque<String> = VecDeque::new();
state_queue.push_back(self.machine.initial.clone());
self.visited_states.clear();
while let Some(state_id) = state_queue.pop_front() {
if self.visited_states.contains(&state_id) {
continue;
}
self.visited_states.insert(state_id.clone());
if let Some(test_case) = self.generate_test_to_state(&state_id) {
test_cases.push(test_case);
}
for transition in &self.machine.transitions {
if transition.source == state_id && transition.target.is_some() {
let target = transition.target.as_ref().unwrap();
if !self.visited_states.contains(target) {
state_queue.push_back(target.clone());
}
}
}
}
test_cases
}
pub fn generate_all_transitions(&mut self) -> Vec<TestCase> {
let mut test_cases = Vec::new();
for transition in &self.machine.transitions {
if let Some(target) = &transition.target {
let test_case = TestCase {
name: format!(
"Transition: {} --{}--> {}",
transition.source, transition.event, target
),
initial_state: transition.source.clone(),
events: vec![Event::new(transition.event.clone())],
expected_state: target.clone(),
};
test_cases.push(test_case);
}
}
test_cases
}
fn generate_test_to_state(&self, target_state: &str) -> Option<TestCase> {
if target_state == self.machine.initial {
return Some(TestCase {
name: format!("Initial state: {}", target_state),
initial_state: target_state.to_string(),
events: Vec::new(),
expected_state: target_state.to_string(),
});
}
let path = self.find_shortest_path_to_state(target_state)?;
Some(TestCase {
name: format!("Path to state: {}", target_state),
initial_state: self.machine.initial.clone(),
events: path,
expected_state: target_state.to_string(),
})
}
fn find_shortest_path_to_state(&self, target_state: &str) -> Option<Vec<Event>> {
let mut queue: VecDeque<(String, Vec<Event>)> = VecDeque::new();
let mut visited = HashSet::new();
queue.push_back((self.machine.initial.clone(), Vec::new()));
while let Some((current_state, events)) = queue.pop_front() {
if current_state == target_state {
return Some(events);
}
if visited.contains(¤t_state) {
continue;
}
visited.insert(current_state.clone());
for transition in &self.machine.transitions {
if transition.source == current_state && transition.target.is_some() {
let next_state = transition.target.as_ref().unwrap();
if !visited.contains(next_state) {
let mut new_events = events.clone();
new_events.push(Event::new(transition.event.clone()));
queue.push_back((next_state.clone(), new_events));
}
}
}
}
None
}
pub fn generate_loop_coverage(&mut self) -> Vec<TestCase> {
let mut test_cases = Vec::new();
let reachable_states = self.find_reachable_states();
for state in reachable_states {
let loops = self.find_loops_from_state(&state);
for loop_path in loops {
if !loop_path.is_empty() {
let events: Vec<Event> = loop_path
.iter()
.map(|event_name| Event::new(event_name.clone()))
.collect();
test_cases.push(TestCase {
name: format!("Loop from state: {}", state),
initial_state: state.clone(),
events,
expected_state: state.clone(),
});
}
}
}
test_cases
}
fn find_loops_from_state(&self, state: &str) -> Vec<Vec<String>> {
let mut loops = Vec::new();
let mut visited = HashSet::new();
let mut path = Vec::new();
self.dfs_find_loops(state, state, &mut visited, &mut path, &mut loops);
loops
}
fn dfs_find_loops(
&self,
start: &str,
current: &str,
visited: &mut HashSet<String>,
path: &mut Vec<String>,
loops: &mut Vec<Vec<String>>,
) {
if !path.is_empty() && current == start {
loops.push(path.clone());
return;
}
if visited.contains(current) {
return;
}
visited.insert(current.to_string());
for transition in &self.machine.transitions {
if transition.source == current && transition.target.is_some() {
let target = transition.target.as_ref().unwrap();
path.push(transition.event.clone());
self.dfs_find_loops(start, target, visited, path, loops);
path.pop();
}
}
visited.remove(current);
}
fn find_reachable_states(&self) -> HashSet<String> {
let mut reachable = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(self.machine.initial.clone());
while let Some(state) = queue.pop_front() {
if reachable.contains(&state) {
continue;
}
reachable.insert(state.clone());
for transition in &self.machine.transitions {
if transition.source == state && transition.target.is_some() {
let target = transition.target.as_ref().unwrap();
if !reachable.contains(target) {
queue.push_back(target.clone());
}
}
}
}
reachable
}
}