use std::collections::{HashMap, HashSet, VecDeque};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::DeviceHandler;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct StateMachineDiagnostics {
pub total_states: usize,
pub graph_states: Vec<String>,
pub entry_states: Vec<String>,
pub missing_edge_sources: Vec<String>,
pub missing_edge_targets: Vec<String>,
pub unreachable_states: Vec<String>,
pub dead_end_states: Vec<String>,
pub duplicate_prompt_patterns: Vec<String>,
pub potentially_ambiguous_prompt_states: Vec<String>,
pub self_loop_only_states: Vec<String>,
}
impl StateMachineDiagnostics {
pub fn has_issues(&self) -> bool {
!self.missing_edge_sources.is_empty()
|| !self.missing_edge_targets.is_empty()
|| !self.unreachable_states.is_empty()
|| !self.dead_end_states.is_empty()
|| !self.duplicate_prompt_patterns.is_empty()
|| !self.self_loop_only_states.is_empty()
}
}
impl DeviceHandler {
pub fn diagnose_state_machine(&self) -> StateMachineDiagnostics {
let all_states_set: HashSet<String> = self.all_states.iter().cloned().collect();
let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
let mut in_degree: HashMap<String, usize> = HashMap::new();
let mut out_degree: HashMap<String, usize> = HashMap::new();
let mut graph_states_set: HashSet<String> = HashSet::new();
let mut missing_edge_sources = HashSet::new();
let mut missing_edge_targets = HashSet::new();
for (from, _cmd, to, _is_exit, _needs_format) in &self.edges {
if !all_states_set.contains(from) {
missing_edge_sources.insert(from.clone());
continue;
}
if !all_states_set.contains(to) {
missing_edge_targets.insert(to.clone());
continue;
}
graph_states_set.insert(from.clone());
graph_states_set.insert(to.clone());
adjacency.entry(from.clone()).or_default().push(to.clone());
*out_degree.entry(from.clone()).or_insert(0) += 1;
*in_degree.entry(to.clone()).or_insert(0) += 1;
in_degree.entry(from.clone()).or_insert(0);
out_degree.entry(to.clone()).or_insert(0);
}
let mut graph_states = graph_states_set.into_iter().collect::<Vec<_>>();
graph_states.sort();
let mut entry_states = graph_states
.iter()
.filter(|state| in_degree.get(*state).copied().unwrap_or(0) == 0)
.cloned()
.collect::<Vec<_>>();
entry_states.sort();
let seeds = if entry_states.is_empty() {
graph_states
.first()
.cloned()
.into_iter()
.collect::<Vec<_>>()
} else {
entry_states.clone()
};
let mut reachable = HashSet::new();
let mut queue = VecDeque::new();
for seed in seeds {
if reachable.insert(seed.clone()) {
queue.push_back(seed);
}
}
while let Some(node) = queue.pop_front() {
if let Some(neighbors) = adjacency.get(&node) {
for next in neighbors {
if reachable.insert(next.clone()) {
queue.push_back(next.clone());
}
}
}
}
let mut unreachable_states = graph_states
.iter()
.filter(|state| !reachable.contains(*state))
.cloned()
.collect::<Vec<_>>();
unreachable_states.sort();
let mut dead_end_states = graph_states
.iter()
.filter(|state| out_degree.get(*state).copied().unwrap_or(0) == 0)
.cloned()
.collect::<Vec<_>>();
dead_end_states.sort();
let mut missing_edge_sources = missing_edge_sources.into_iter().collect::<Vec<_>>();
missing_edge_sources.sort();
let mut missing_edge_targets = missing_edge_targets.into_iter().collect::<Vec<_>>();
missing_edge_targets.sort();
let mut duplicate_prompt_patterns = Vec::new();
let mut ambiguous_states = HashSet::new();
let mut pattern_states: HashMap<String, HashSet<String>> = HashMap::new();
for (state, pattern) in &self.prompt_patterns {
pattern_states
.entry(pattern.clone())
.or_default()
.insert(state.clone());
}
for (pattern, states) in pattern_states {
if states.len() > 1 {
let mut states_vec = states.into_iter().collect::<Vec<_>>();
states_vec.sort();
for state in &states_vec {
ambiguous_states.insert(state.clone());
}
duplicate_prompt_patterns.push(format!("{pattern} => {}", states_vec.join(",")));
}
}
duplicate_prompt_patterns.sort();
let mut potentially_ambiguous_prompt_states =
ambiguous_states.into_iter().collect::<Vec<_>>();
potentially_ambiguous_prompt_states.sort();
let mut self_loop_only_states = graph_states
.iter()
.filter(|state| {
let outs = adjacency.get(*state);
out_degree.get(*state).copied().unwrap_or(0) > 0
&& outs
.map(|targets| targets.iter().all(|target| target == *state))
.unwrap_or(false)
})
.cloned()
.collect::<Vec<_>>();
self_loop_only_states.sort();
StateMachineDiagnostics {
total_states: self.all_states.len(),
graph_states,
entry_states,
missing_edge_sources,
missing_edge_targets,
unreachable_states,
dead_end_states,
duplicate_prompt_patterns,
potentially_ambiguous_prompt_states,
self_loop_only_states,
}
}
}
#[cfg(test)]
mod tests {
use super::super::build_test_handler;
use super::DeviceHandler;
use crate::device::{DeviceHandlerConfig, prompt_rule, transition_rule};
#[test]
fn state_machine_diagnostics_are_clean_for_valid_template() {
let handler = build_test_handler();
let report = handler.diagnose_state_machine();
assert!(!report.has_issues());
assert!(report.missing_edge_sources.is_empty());
assert!(report.missing_edge_targets.is_empty());
assert!(report.unreachable_states.is_empty());
assert!(report.dead_end_states.is_empty());
assert!(report.duplicate_prompt_patterns.is_empty());
assert!(report.potentially_ambiguous_prompt_states.is_empty());
assert!(report.self_loop_only_states.is_empty());
}
#[test]
fn state_machine_diagnostics_detect_invalid_edges_and_dead_ends() {
let handler = DeviceHandler::new(DeviceHandlerConfig {
prompt: vec![
prompt_rule("Login", &[r"^dev>\s*$"]),
prompt_rule("Enable", &[r"^dev#\s*$"]),
],
more_regex: vec![r"^--More--$".to_string()],
error_regex: vec![r"^ERROR: .+$".to_string()],
edges: vec![
transition_rule("Login", "enable", "Enable", false, false),
transition_rule("Enable", "to-ghost", "Ghost", false, false),
],
..Default::default()
})
.expect("handler should build");
let report = handler.diagnose_state_machine();
assert!(report.has_issues());
assert_eq!(report.missing_edge_targets, vec!["ghost".to_string()]);
assert_eq!(report.dead_end_states, vec!["enable".to_string()]);
}
#[test]
fn state_machine_diagnostics_detect_duplicate_prompt_patterns() {
let handler = DeviceHandler::new(DeviceHandlerConfig {
prompt: vec![
prompt_rule("Login", &[r"^dup>\s*$"]),
prompt_rule("Enable", &[r"^dup>\s*$"]),
],
more_regex: vec![r"^--More--$".to_string()],
error_regex: vec![r"^ERROR: .+$".to_string()],
edges: vec![transition_rule("Login", "noop", "Enable", false, false)],
..Default::default()
})
.expect("handler should build");
let report = handler.diagnose_state_machine();
assert!(report.has_issues());
assert!(!report.duplicate_prompt_patterns.is_empty());
assert!(
report
.potentially_ambiguous_prompt_states
.contains(&"enable".to_string())
);
assert!(
report
.potentially_ambiguous_prompt_states
.contains(&"login".to_string())
);
}
#[test]
fn state_machine_diagnostics_detect_self_loop_only_states() {
let handler = DeviceHandler::new(DeviceHandlerConfig {
prompt: vec![
prompt_rule("Login", &[r"^dev>\s*$"]),
prompt_rule("Enable", &[r"^dev#\s*$"]),
],
more_regex: vec![r"^--More--$".to_string()],
error_regex: vec![r"^ERROR: .+$".to_string()],
edges: vec![transition_rule("Enable", "noop", "Enable", false, false)],
..Default::default()
})
.expect("handler should build");
let report = handler.diagnose_state_machine();
assert!(report.self_loop_only_states.contains(&"enable".to_string()));
}
}