use std::collections::BTreeMap;
use wasm4pm::models::{AttributeValue, Event, EventLog, Trace};
fn parse_xes(content: &str) -> EventLog {
let mut log = EventLog::new();
let mut current_trace: Option<Trace> = None;
let mut current_event: Option<Event> = None;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("<trace>") || trimmed.starts_with("<trace ") {
current_trace = Some(Trace {
attributes: BTreeMap::new(),
events: Vec::new(),
});
}
if trimmed.starts_with("</trace>") {
if let Some(trace) = current_trace.take() {
log.traces.push(trace);
}
}
if trimmed.starts_with("<event>") || trimmed.starts_with("<event ") {
current_event = Some(Event {
attributes: BTreeMap::new(),
});
}
if trimmed.starts_with("</event>") {
if let Some(event) = current_event.take() {
if let Some(ref mut trace) = current_trace {
trace.events.push(event);
}
}
}
if trimmed.starts_with("<string") {
if let Some(key_start) = trimmed.find("key=\"") {
let key_start = key_start + 5;
if let Some(key_end) = trimmed[key_start..].find('"') {
let key = trimmed[key_start..key_start + key_end].to_string();
if let Some(val_start) = trimmed.find("value=\"") {
let val_start = val_start + 7;
if let Some(val_end) = trimmed[val_start..].find('"') {
let value = trimmed[val_start..val_start + val_end].to_string();
if let Some(ref mut event) = current_event {
event.attributes.insert(key, AttributeValue::String(value));
} else if let Some(ref mut trace) = current_trace {
trace.attributes.insert(key, AttributeValue::String(value));
}
}
}
}
}
}
if trimmed.starts_with("<date") {
if let Some(key_start) = trimmed.find("key=\"") {
let key_start = key_start + 5;
if let Some(key_end) = trimmed[key_start..].find('"') {
let key = trimmed[key_start..key_start + key_end].to_string();
if let Some(val_start) = trimmed.find("value=\"") {
let val_start = val_start + 7;
if let Some(val_end) = trimmed[val_start..].find('"') {
let value = trimmed[val_start..val_start + val_end].to_string();
if let Some(ref mut event) = current_event {
event.attributes.insert(key, AttributeValue::String(value));
} else if let Some(ref mut trace) = current_trace {
trace.attributes.insert(key, AttributeValue::String(value));
}
}
}
}
}
}
}
log
}
fn activity_name(event: &Event) -> Option<&str> {
match event.attributes.get("concept:name") {
Some(AttributeValue::String(s)) => Some(s.as_str()),
_ => None,
}
}
fn timestamp_value(event: &Event) -> Option<&str> {
match event.attributes.get("time:timestamp") {
Some(AttributeValue::String(s)) => Some(s.as_str()),
_ => None,
}
}
#[test]
fn test_missing_timestamps_parses_without_panic() {
let content = include_str!("fixtures/dirty_data/missing_timestamps.xes");
let log = parse_xes(content);
assert_eq!(log.traces.len(), 1, "expected 1 trace");
assert_eq!(log.traces[0].events.len(), 2, "expected 2 events");
}
#[test]
fn test_missing_timestamps_first_event_lacks_timestamp() {
let content = include_str!("fixtures/dirty_data/missing_timestamps.xes");
let log = parse_xes(content);
let first_event = &log.traces[0].events[0];
assert!(
timestamp_value(first_event).is_none(),
"first event should have no timestamp"
);
}
#[test]
fn test_missing_timestamps_second_event_has_timestamp() {
let content = include_str!("fixtures/dirty_data/missing_timestamps.xes");
let log = parse_xes(content);
let second_event = &log.traces[0].events[1];
assert_eq!(
timestamp_value(second_event),
Some("2024-01-01T10:00:00Z"),
"second event should have a timestamp"
);
}
#[test]
fn test_duplicate_events_parses_without_panic() {
let content = include_str!("fixtures/dirty_data/duplicate_events.xes");
let log = parse_xes(content);
assert_eq!(log.traces.len(), 1, "expected 1 trace");
assert_eq!(log.traces[0].events.len(), 2, "expected 2 events");
}
#[test]
fn test_duplicate_events_detectable_by_same_activity_and_timestamp() {
let content = include_str!("fixtures/dirty_data/duplicate_events.xes");
let log = parse_xes(content);
let events = &log.traces[0].events;
let activity_a = activity_name(&events[0]).unwrap_or("");
let activity_b = activity_name(&events[1]).unwrap_or("");
assert_eq!(activity_a, activity_b, "duplicate activity names");
let ts_a = timestamp_value(&events[0]).unwrap_or("");
let ts_b = timestamp_value(&events[1]).unwrap_or("");
assert_eq!(ts_a, ts_b, "duplicate timestamps");
}
#[test]
fn test_out_of_order_parses_without_panic() {
let content = include_str!("fixtures/dirty_data/out_of_order.xes");
let log = parse_xes(content);
assert_eq!(log.traces.len(), 1, "expected 1 trace");
assert_eq!(log.traces[0].events.len(), 2, "expected 2 events");
}
#[test]
fn test_out_of_order_detectable_by_descending_timestamps() {
let content = include_str!("fixtures/dirty_data/out_of_order.xes");
let log = parse_xes(content);
let events = &log.traces[0].events;
let ts0 = timestamp_value(&events[0]).unwrap_or("");
let ts1 = timestamp_value(&events[1]).unwrap_or("");
assert!(
ts0 > ts1,
"events are out of order: first ts={:?}, second ts={:?}",
ts0,
ts1
);
}
#[test]
fn test_out_of_order_activity_order_in_xml() {
let content = include_str!("fixtures/dirty_data/out_of_order.xes");
let log = parse_xes(content);
assert_eq!(activity_name(&log.traces[0].events[0]), Some("ActivityB"));
assert_eq!(activity_name(&log.traces[0].events[1]), Some("ActivityA"));
}
#[test]
fn test_missing_case_id_parses_without_panic() {
let content = include_str!("fixtures/dirty_data/missing_case_id.xes");
let log = parse_xes(content);
assert_eq!(log.traces.len(), 1, "expected 1 trace");
assert_eq!(log.traces[0].events.len(), 1, "expected 1 event");
}
#[test]
fn test_missing_case_id_trace_lacks_concept_name() {
let content = include_str!("fixtures/dirty_data/missing_case_id.xes");
let log = parse_xes(content);
let trace = &log.traces[0];
let has_case_id = matches!(
trace.attributes.get("concept:name"),
Some(AttributeValue::String(_))
);
assert!(
!has_case_id,
"trace should not have a concept:name (case ID)"
);
}