Added graphviz plotting

This commit is contained in:
AlexanderHD27
2025-06-09 23:33:57 +02:00
parent bea5076295
commit ff929b9fe9
5 changed files with 201 additions and 26 deletions

39
Cargo.lock generated
View File

@@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.98" version = "1.0.98"
@@ -43,6 +52,7 @@ dependencies = [
"itertools", "itertools",
"pad", "pad",
"rand", "rand",
"regex",
"serde", "serde",
"serde_yml", "serde_yml",
"thiserror 1.0.69", "thiserror 1.0.69",
@@ -390,6 +400,35 @@ dependencies = [
"getrandom 0.2.16", "getrandom 0.2.16",
] ]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.0.7" version = "1.0.7"

View File

@@ -11,10 +11,11 @@ thiserror = "1"
rand = "0.8" rand = "0.8"
graphviz-rust = "0.9.4" graphviz-rust = "0.9.4"
itertools = "0.14.0" itertools = "0.14.0"
regex = "1.11.1"
[dependencies.uuid] [dependencies.uuid]
version = "1.17.0" version = "1.17.0"
# Lets you generate random UUIDs # Lets you generate random UUIDs
features = [ features = [
"v4", "v4",
] ]

View File

@@ -1,4 +1,6 @@
use crate::configotron::fsm::TransitionMask; use crate::configotron::fsm::{masks::decompose_transition, FiniteStateMachine, TransitionMask};
use graphviz_rust::dot_structures::{Attribute, Edge, EdgeTy, Graph, Id, Node, NodeId, Stmt, Vertex};
use uuid::Uuid;
@@ -71,9 +73,70 @@ pub fn mask_to_string(mask: TransitionMask) -> String {
} }
pub fn create_graphviz_graph(fsm: &FiniteStateMachine) -> Graph {
let mut stmts: Vec<Stmt> = Vec::new();
let mut unamed_state_count: u32 = 0;
let mut states_sorted: Vec<_> = fsm.states.iter().collect();
states_sorted.sort_by(|a, b| a.1.name.cmp(&b.1.name));
fn format_id(id: &Uuid) -> Id {
Id::Plain(format!("\"{}\"", id.to_string()))
}
for (k, state) in states_sorted {
// Adding all nodes
let label = if state.name.len() == 0 {
format!("\"z{}\"", unamed_state_count)
} else {
format!("\"{}\"", state.name)
};
unamed_state_count += 1;
let shape = match state.state_type {
super::StateType::Terminal => "doublecircle".to_string(),
super::StateType::Error => "circle".to_string(),
super::StateType::None => "circle".to_string(),
};
let color = match state.state_type {
super::StateType::Terminal => "black".to_string(),
super::StateType::Error => "red".to_string(),
super::StateType::None => "black".to_string(),
};
stmts.push(Stmt::Node(Node {
id: NodeId(format_id(k), None),
attributes: vec![
Attribute(Id::Plain("label".to_string()), Id::Plain(label)),
Attribute(Id::Plain("shape".to_string()), Id::Plain(shape)),
Attribute(Id::Plain("color".to_string()), Id::Plain(color))
]
}));
// Adding all edges
for ((state_ref, action), mask) in decompose_transition(&state.transition) {
stmts.push(Stmt::Edge(Edge {
ty: EdgeTy::Pair(
Vertex::N(NodeId(format_id(k), None)),
Vertex::N(NodeId(format_id(&state_ref), None))
),
attributes: vec![
Attribute(Id::Plain("label".to_string()), Id::Plain(format!("\"{}\\n/{}/\"", mask_to_string(mask), action)))
]
}));
}
}
Graph::DiGraph { id: Id::Plain("FSM".to_string()), strict: true, stmts: stmts }
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::configotron::fsm::{display::mask_to_string, masks::{mask_all, mask_char, mask_char_range}}; use graphviz_rust::{cmd::{CommandArg, Format}, exec, printer::{DotPrinter, PrinterContext}};
use crate::configotron::fsm::{display::{create_graphviz_graph, mask_to_string}, masks::{mask_all, mask_char, mask_char_range}, Action, FSMBuilder, StateType};
#[test] #[test]
fn mask_to_string_test() { fn mask_to_string_test() {
@@ -111,4 +174,64 @@ mod test {
"'a','c','e'..'g','i','k'..'m','o','q','s','u','w','y','z'".to_string() "'a','c','e'..'g','i','k'..'m','o','q','s','u','w','y','z'".to_string()
); );
} }
#[test]
fn create_graph_dot_lang() {
let mut fsm_builder = FSMBuilder::new();
let z0 = fsm_builder.add_state("z0".to_string(), StateType::None);
let z1 = fsm_builder.add_state("z1".to_string(), StateType::None);
let z2 = fsm_builder.add_state("z2".to_string(), StateType::Terminal);
let z3 = fsm_builder.add_state("z3".to_string(), StateType::Error);
// Transitions:
// z0: 'a' -> z1, 'b' -> z2
// z1: 'a' -> z1, 'b' -> z2
// z2: 'a' -> z2
// z3: * -> z3
// Error/Default: z3
fsm_builder.add_link(&z0, &z1, Action::None, mask_char('a')).unwrap();
fsm_builder.add_link(&z0, &z2, Action::None, mask_char('b')).unwrap();
fsm_builder.add_link(&z1, &z1, Action::None, mask_char('a')).unwrap();
fsm_builder.add_link(&z1, &z2, Action::None, mask_char('b')).unwrap();
fsm_builder.add_link(&z2, &z2, Action::None, mask_char('a')).unwrap();
let res_fsm = fsm_builder.finish(&z3, Action::None, &z0);
assert!(res_fsm.is_ok());
if let Ok(fsm) = res_fsm {
let graph = create_graphviz_graph(&fsm);
let string = graph.print(&mut PrinterContext::default());
println!("Original: {}", string);
// Test that the DOT output contains the expected structure, ignoring node ids (UUIDs)
let string = graph.print(&mut PrinterContext::default());
// Remove all UUIDs from the output for comparison
let normalized = regex::Regex::new(r#""[0-9a-fA-F\-]{36}""#)
.unwrap()
.replace_all(&string, "\"UUID\"");
// Check for expected nodes and edges (labels, shapes, colors, etc.)
assert!(normalized.contains("label=\"z0\""));
assert!(normalized.contains("label=\"z1\""));
assert!(normalized.contains("label=\"z2\""));
assert!(normalized.contains("label=\"z3\""));
assert!(normalized.contains("shape=doublecircle"));
assert!(normalized.contains("shape=circle"));
assert!(normalized.contains("color=red"));
assert!(normalized.contains("color=black"));
assert!(normalized.contains("label=\"'a'\\n/None/\""));
assert!(normalized.contains("label=\"'b'\\n/None/\""));
// Check that all expected edges exist (ignoring UUIDs)
let expected_edges = [
"label=\"'a'\\n/None/\"",
"label=\"'b'\\n/None/\"",
];
for edge in expected_edges.iter() {
assert!(normalized.matches(edge).count() >= 2, "Missing edge: {}", edge);
}
}
}
} }

View File

@@ -2,7 +2,7 @@ use std::{collections::{HashMap}, hash::Hash};
use crate::configotron::fsm::{TransitionArray, TransitionMask}; use crate::configotron::fsm::{TransitionArray, TransitionMask};
pub fn decompose_transition<T: Hash + Eq + PartialEq + Clone>(array: &TransitionArray<T>) -> Vec<(T, TransitionMask)> { pub fn decompose_transition<T: Hash + Eq + Clone>(array: &TransitionArray<T>) -> Vec<(T, TransitionMask)> {
let mut results: HashMap<T, TransitionMask> = HashMap::new(); let mut results: HashMap<T, TransitionMask> = HashMap::new();
for (pos, i) in array.iter().enumerate() { for (pos, i) in array.iter().enumerate() {

View File

@@ -1,7 +1,8 @@
pub mod display; pub mod display;
pub mod masks; pub mod masks;
use std::{collections::{HashMap, HashSet}}; use std::{collections::{HashMap, HashSet}, fmt};
use uuid::Uuid; use uuid::Uuid;
@@ -9,12 +10,12 @@ pub type StateRef = Uuid;
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum StateType { pub enum StateType {
EndState, Terminal,
ErrorState, Error,
None None
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum Action { pub enum Action {
None, None,
Push, Push,
@@ -22,6 +23,17 @@ pub enum Action {
Submit Submit
} }
impl fmt::Display for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Action::None => write!(f, "None"),
Action::Push => write!(f, "Push"),
Action::Pop => write!(f, "Pop"),
Action::Submit => write!(f, "Submit"),
}
}
}
pub type TransitionArray<T> = [T; 128]; pub type TransitionArray<T> = [T; 128];
pub type UnfinishedTransitionArray<T> = [Option<T>; 128]; pub type UnfinishedTransitionArray<T> = [Option<T>; 128];
@@ -162,12 +174,12 @@ mod test {
let mut fsm_builder = FSMBuilder::new(); let mut fsm_builder = FSMBuilder::new();
let z0 = fsm_builder.add_state("z0".to_string(), StateType::None); let z0 = fsm_builder.add_state("z0".to_string(), StateType::None);
let z1 = fsm_builder.add_state("z1".to_string(), StateType::None); let z1 = fsm_builder.add_state("z1".to_string(), StateType::None);
let z2 = fsm_builder.add_state("z2".to_string(), StateType::EndState); let z2 = fsm_builder.add_state("z2".to_string(), StateType::Terminal);
let z3 = fsm_builder.add_state("z3".to_string(), StateType::ErrorState); let z3 = fsm_builder.add_state("z3".to_string(), StateType::Error);
// These are unsed states and should be eliminated // These are unsed states and should be eliminated
fsm_builder.add_state("u4".to_string(), StateType::ErrorState); fsm_builder.add_state("u4".to_string(), StateType::Error);
let u5 =fsm_builder.add_state("u5".to_string(), StateType::ErrorState); let u5 =fsm_builder.add_state("u5".to_string(), StateType::Error);
fsm_builder.add_link(&u5, &u5, Action::None, mask_all()).unwrap(); fsm_builder.add_link(&u5, &u5, Action::None, mask_all()).unwrap();
@@ -199,22 +211,22 @@ mod test {
assert_eq!(run_fsm(&fsm, &"".to_string()), StateType::None); assert_eq!(run_fsm(&fsm, &"".to_string()), StateType::None);
assert_eq!(run_fsm(&fsm, &"a".to_string()), StateType::None); assert_eq!(run_fsm(&fsm, &"a".to_string()), StateType::None);
assert_eq!(run_fsm(&fsm, &"b".to_string()), StateType::EndState); assert_eq!(run_fsm(&fsm, &"b".to_string()), StateType::Terminal);
assert_eq!(run_fsm(&fsm, &"aa".to_string()), StateType::None); assert_eq!(run_fsm(&fsm, &"aa".to_string()), StateType::None);
assert_eq!(run_fsm(&fsm, &"ab".to_string()), StateType::EndState); assert_eq!(run_fsm(&fsm, &"ab".to_string()), StateType::Terminal);
assert_eq!(run_fsm(&fsm, &"ba".to_string()), StateType::EndState); // 'b' leads to z2, then 'a' stays in z2 assert_eq!(run_fsm(&fsm, &"ba".to_string()), StateType::Terminal); // 'b' leads to z2, then 'a' stays in z2
assert_eq!(run_fsm(&fsm, &"bb".to_string()), StateType::ErrorState); assert_eq!(run_fsm(&fsm, &"bb".to_string()), StateType::Error);
assert_eq!(run_fsm(&fsm, &"aaa".to_string()), StateType::None); assert_eq!(run_fsm(&fsm, &"aaa".to_string()), StateType::None);
assert_eq!(run_fsm(&fsm, &"aab".to_string()), StateType::EndState); assert_eq!(run_fsm(&fsm, &"aab".to_string()), StateType::Terminal);
assert_eq!(run_fsm(&fsm, &"abb".to_string()), StateType::ErrorState); assert_eq!(run_fsm(&fsm, &"abb".to_string()), StateType::Error);
assert_eq!(run_fsm(&fsm, &"abc".to_string()), StateType::ErrorState); // 'c' is not defined, should go to default z3 assert_eq!(run_fsm(&fsm, &"abc".to_string()), StateType::Error); // 'c' is not defined, should go to default z3
assert_eq!(run_fsm(&fsm, &"c".to_string()), StateType::ErrorState); assert_eq!(run_fsm(&fsm, &"c".to_string()), StateType::Error);
assert_eq!(run_fsm(&fsm, &"cab".to_string()), StateType::ErrorState); assert_eq!(run_fsm(&fsm, &"cab".to_string()), StateType::Error);
assert_eq!(run_fsm(&fsm, &"aabc".to_string()), StateType::ErrorState); assert_eq!(run_fsm(&fsm, &"aabc".to_string()), StateType::Error);
assert_eq!(run_fsm(&fsm, &"bca".to_string()), StateType::ErrorState); assert_eq!(run_fsm(&fsm, &"bca".to_string()), StateType::Error);
assert_eq!(run_fsm(&fsm, &"aaaaab".to_string()), StateType::EndState); assert_eq!(run_fsm(&fsm, &"aaaaab".to_string()), StateType::Terminal);
assert_eq!(run_fsm(&fsm, &"aaaaac".to_string()), StateType::ErrorState); assert_eq!(run_fsm(&fsm, &"aaaaac".to_string()), StateType::Error);
assert_eq!(run_fsm(&fsm, &"bbbb".to_string()), StateType::ErrorState); assert_eq!(run_fsm(&fsm, &"bbbb".to_string()), StateType::Error);
} }