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.
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]]
name = "anyhow"
version = "1.0.98"
@@ -43,6 +52,7 @@ dependencies = [
"itertools",
"pad",
"rand",
"regex",
"serde",
"serde_yml",
"thiserror 1.0.69",
@@ -390,6 +400,35 @@ dependencies = [
"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]]
name = "rustix"
version = "1.0.7"

View File

@@ -11,10 +11,11 @@ thiserror = "1"
rand = "0.8"
graphviz-rust = "0.9.4"
itertools = "0.14.0"
regex = "1.11.1"
[dependencies.uuid]
version = "1.17.0"
# Lets you generate random UUIDs
features = [
"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)]
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]
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()
);
}
#[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};
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();
for (pos, i) in array.iter().enumerate() {

View File

@@ -1,7 +1,8 @@
pub mod display;
pub mod masks;
use std::{collections::{HashMap, HashSet}};
use std::{collections::{HashMap, HashSet}, fmt};
use uuid::Uuid;
@@ -9,12 +10,12 @@ pub type StateRef = Uuid;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum StateType {
EndState,
ErrorState,
Terminal,
Error,
None
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum Action {
None,
Push,
@@ -22,6 +23,17 @@ pub enum Action {
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 UnfinishedTransitionArray<T> = [Option<T>; 128];
@@ -162,12 +174,12 @@ mod test {
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::EndState);
let z3 = fsm_builder.add_state("z3".to_string(), StateType::ErrorState);
let z2 = fsm_builder.add_state("z2".to_string(), StateType::Terminal);
let z3 = fsm_builder.add_state("z3".to_string(), StateType::Error);
// These are unsed states and should be eliminated
fsm_builder.add_state("u4".to_string(), StateType::ErrorState);
let u5 =fsm_builder.add_state("u5".to_string(), StateType::ErrorState);
fsm_builder.add_state("u4".to_string(), StateType::Error);
let u5 =fsm_builder.add_state("u5".to_string(), StateType::Error);
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, &"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, &"ab".to_string()), StateType::EndState);
assert_eq!(run_fsm(&fsm, &"ba".to_string()), StateType::EndState); // 'b' leads to z2, then 'a' stays in z2
assert_eq!(run_fsm(&fsm, &"bb".to_string()), StateType::ErrorState);
assert_eq!(run_fsm(&fsm, &"ab".to_string()), StateType::Terminal);
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::Error);
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, &"abb".to_string()), StateType::ErrorState);
assert_eq!(run_fsm(&fsm, &"abc".to_string()), StateType::ErrorState); // 'c' is not defined, should go to default z3
assert_eq!(run_fsm(&fsm, &"c".to_string()), StateType::ErrorState);
assert_eq!(run_fsm(&fsm, &"cab".to_string()), StateType::ErrorState);
assert_eq!(run_fsm(&fsm, &"aabc".to_string()), StateType::ErrorState);
assert_eq!(run_fsm(&fsm, &"bca".to_string()), StateType::ErrorState);
assert_eq!(run_fsm(&fsm, &"aaaaab".to_string()), StateType::EndState);
assert_eq!(run_fsm(&fsm, &"aaaaac".to_string()), StateType::ErrorState);
assert_eq!(run_fsm(&fsm, &"bbbb".to_string()), StateType::ErrorState);
assert_eq!(run_fsm(&fsm, &"aab".to_string()), StateType::Terminal);
assert_eq!(run_fsm(&fsm, &"abb".to_string()), StateType::Error);
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::Error);
assert_eq!(run_fsm(&fsm, &"cab".to_string()), StateType::Error);
assert_eq!(run_fsm(&fsm, &"aabc".to_string()), StateType::Error);
assert_eq!(run_fsm(&fsm, &"bca".to_string()), StateType::Error);
assert_eq!(run_fsm(&fsm, &"aaaaab".to_string()), StateType::Terminal);
assert_eq!(run_fsm(&fsm, &"aaaaac".to_string()), StateType::Error);
assert_eq!(run_fsm(&fsm, &"bbbb".to_string()), StateType::Error);
}