use std::collections::HashMap;
use std::fs;
use wasm4pm::models::{AttributeValue, EventLog};
const FIXTURES_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
fn get_fixture_path(name: &str) -> String {
format!("{FIXTURES_DIR}/{name}")
}
fn load_event_log_json(name: &str) -> EventLog {
let path = get_fixture_path(name);
let json_str = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("Failed to load fixture {}: {}", path, e));
serde_json::from_str(&json_str)
.unwrap_or_else(|e| panic!("Failed to parse event log JSON from {}: {}", path, e))
}
fn load_pm4py_output<T: serde::de::DeserializeOwned>(name: &str) -> T {
let path = get_fixture_path(name);
let json_str = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("Failed to load pm4py output {}: {}", path, e));
serde_json::from_str(&json_str)
.unwrap_or_else(|e| panic!("Failed to parse pm4py output JSON from {}: {}", path, e))
}
#[test]
fn test_load_running_example_json() {
let log = load_event_log_json("running-example.json");
assert!(!log.traces.is_empty(), "Log should have traces");
for trace in &log.traces {
assert!(!trace.events.is_empty(), "Each trace should have events");
}
for trace in &log.traces {
for event in &trace.events {
assert!(
event.attributes.contains_key("activity"),
"Each event should have 'activity' attribute"
);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct CanonicalEdge {
from: String,
to: String,
frequency: usize,
}
impl CanonicalEdge {
fn from_pm4py_format(from: &str, to: &str, freq: usize) -> Self {
Self {
from: from.to_string(),
to: to.to_string(),
frequency: freq,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct CanonicalActivityCount {
activity: String,
count: usize,
}
#[derive(Debug, Clone, PartialEq)]
struct CanonicalDFG {
activities: Vec<String>,
edges: Vec<CanonicalEdge>,
start_activities: Vec<CanonicalActivityCount>,
end_activities: Vec<CanonicalActivityCount>,
}
impl CanonicalDFG {
fn sort(&mut self) {
self.activities.sort();
self.edges.sort_by(|a, b| {
a.from
.cmp(&b.from)
.then_with(|| a.to.cmp(&b.to))
.then_with(|| a.frequency.cmp(&b.frequency))
});
self.start_activities.sort_by(|a, b| {
a.activity
.cmp(&b.activity)
.then_with(|| a.count.cmp(&b.count))
});
self.end_activities.sort_by(|a, b| {
a.activity
.cmp(&b.activity)
.then_with(|| a.count.cmp(&b.count))
});
}
}
#[test]
fn test_dfg_parity_with_pm4py() {
let log = load_event_log_json("running-example.json");
type Pm4pyDFG = serde_json::Value;
let pm4py_dfg: Pm4pyDFG = load_pm4py_output("pm4py_dfg_output.json");
let pm4py_activities: Vec<String> = pm4py_dfg["activities"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
let pm4py_edges: Vec<CanonicalEdge> = pm4py_dfg["edges"]
.as_array()
.unwrap()
.iter()
.map(|v| {
let from = v["from"].as_str().unwrap();
let to = v["to"].as_str().unwrap();
let freq = v["frequency"].as_u64().unwrap() as usize;
CanonicalEdge::from_pm4py_format(from, to, freq)
})
.collect();
let pm4py_start: Vec<CanonicalActivityCount> = pm4py_dfg["start_activities"]
.as_array()
.unwrap()
.iter()
.map(|v| CanonicalActivityCount {
activity: v["activity"].as_str().unwrap().to_string(),
count: v["count"].as_u64().unwrap() as usize,
})
.collect();
let pm4py_end: Vec<CanonicalActivityCount> = pm4py_dfg["end_activities"]
.as_array()
.unwrap()
.iter()
.map(|v| CanonicalActivityCount {
activity: v["activity"].as_str().unwrap().to_string(),
count: v["count"].as_u64().unwrap() as usize,
})
.collect();
let activity_key = "activity";
let mut activities_set = std::collections::HashSet::new();
for trace in &log.traces {
for event in &trace.events {
if let Some(AttributeValue::String(activity)) = event.attributes.get(activity_key) {
activities_set.insert(activity.clone());
}
}
}
let wasm4pm_activities: Vec<String> = activities_set.iter().cloned().collect();
let mut edge_counts: HashMap<(String, String), usize> = HashMap::new();
let mut start_activities: HashMap<String, usize> = HashMap::new();
let mut end_activities: HashMap<String, usize> = HashMap::new();
for trace in &log.traces {
let events = &trace.events;
if events.is_empty() {
continue;
}
if let Some(AttributeValue::String(first)) = events[0].attributes.get(activity_key) {
*start_activities.entry(first.clone()).or_insert(0) += 1;
}
if let Some(AttributeValue::String(last)) =
events[events.len() - 1].attributes.get(activity_key)
{
*end_activities.entry(last.clone()).or_insert(0) += 1;
}
for i in 0..events.len().saturating_sub(1) {
if let (Some(AttributeValue::String(from)), Some(AttributeValue::String(to))) = (
events[i].attributes.get(activity_key),
events[i + 1].attributes.get(activity_key),
) {
*edge_counts.entry((from.clone(), to.clone())).or_insert(0) += 1;
}
}
}
let wasm4pm_edges: Vec<CanonicalEdge> = edge_counts
.into_iter()
.map(|((from, to), freq)| CanonicalEdge {
from,
to,
frequency: freq,
})
.collect();
let wasm4pm_start: Vec<CanonicalActivityCount> = start_activities
.into_iter()
.map(|(activity, count)| CanonicalActivityCount { activity, count })
.collect();
let wasm4pm_end: Vec<CanonicalActivityCount> = end_activities
.into_iter()
.map(|(activity, count)| CanonicalActivityCount { activity, count })
.collect();
let mut pm4py_canonical = CanonicalDFG {
activities: pm4py_activities,
edges: pm4py_edges,
start_activities: pm4py_start,
end_activities: pm4py_end,
};
pm4py_canonical.sort();
let mut wasm4pm_canonical = CanonicalDFG {
activities: wasm4pm_activities,
edges: wasm4pm_edges,
start_activities: wasm4pm_start,
end_activities: wasm4pm_end,
};
wasm4pm_canonical.sort();
assert_eq!(
wasm4pm_canonical.activities.len(),
pm4py_canonical.activities.len(),
"Activity count mismatch: wasm4pm has {}, pm4py has {}",
wasm4pm_canonical.activities.len(),
pm4py_canonical.activities.len()
);
for activity in &pm4py_canonical.activities {
assert!(
wasm4pm_canonical.activities.contains(activity),
"Missing activity in wasm4pm: {}",
activity
);
}
println!("pm4py edges: {}", pm4py_canonical.edges.len());
println!("wasm4pm edges: {}", wasm4pm_canonical.edges.len());
for pm4py_edge in &pm4py_canonical.edges {
let found = wasm4pm_canonical
.edges
.iter()
.any(|e| e.from == pm4py_edge.from && e.to == pm4py_edge.to);
assert!(
found,
"Missing edge in wasm4pm: {} -> {} (count: {})",
pm4py_edge.from, pm4py_edge.to, pm4py_edge.frequency
);
}
assert_eq!(
wasm4pm_canonical.start_activities.len(),
pm4py_canonical.start_activities.len(),
"Start activity count mismatch"
);
for start in &pm4py_canonical.start_activities {
assert!(
wasm4pm_canonical.start_activities.contains(start),
"Missing start activity in wasm4pm: {} (count: {})",
start.activity,
start.count
);
}
assert_eq!(
wasm4pm_canonical.end_activities.len(),
pm4py_canonical.end_activities.len(),
"End activity count mismatch"
);
for end in &pm4py_canonical.end_activities {
assert!(
wasm4pm_canonical.end_activities.contains(end),
"Missing end activity in wasm4pm: {} (count: {})",
end.activity,
end.count
);
}
}
#[test]
fn test_event_log_statistics() {
let log = load_event_log_json("running-example.json");
let total_events: usize = log.traces.iter().map(|t| t.events.len()).sum();
println!("Total traces: {}", log.traces.len());
println!("Total events: {}", total_events);
let mut activities = std::collections::HashSet::new();
for trace in &log.traces {
for event in &trace.events {
if let Some(AttributeValue::String(activity)) = event.attributes.get("activity") {
activities.insert(activity.clone());
}
}
}
println!("Unique activities: {}", activities.len());
for activity in &activities {
println!(" - {}", activity);
}
assert!(!log.traces.is_empty(), "Should have traces");
assert!(total_events > 0, "Should have events");
assert!(!activities.is_empty(), "Should have activities");
}
#[test]
fn test_generate_reference_instructions() {
println!("To regenerate pm4py reference outputs:");
println!(" cd /Users/sac/chatmangpt/wasm4pm");
println!(" python3 wasm4pm/tests/generate_pm4py_reference.py");
println!();
println!("This will generate:");
println!(" - running-example.json (event log for wasm4pm)");
println!(" - pm4py_dfg_output.json (reference DFG)");
println!(" - pm4py_inductive_output.json (reference process tree)");
println!(" - pm4py_alpha_output.json (reference Petri net)");
assert!(true);
}