use std::collections::{BTreeMap, HashSet};
use wasm4pm::discovery::discover_ocel_dfg_pure;
use wasm4pm::models::{AttributeValue, OCELEvent, OCELEventObjectRef, OCELObject, OCEL};
use wasm4pm::ocel_io::validate_ocel_object_lifecycles;
fn create_order_invoice_ocel() -> OCEL {
OCEL {
event_types: vec![
"Create Order".to_string(),
"Check Payment".to_string(),
"Approve Order".to_string(),
"Create Invoice".to_string(),
"Ship Order".to_string(),
],
object_types: vec!["Order".to_string(), "Invoice".to_string()],
events: vec![
OCELEvent {
id: "e1".to_string(),
event_type: "Create Order".to_string(),
timestamp: "2024-01-01T10:00:00Z".to_string(),
attributes: {
let mut m = BTreeMap::new();
m.insert("amount".to_string(), AttributeValue::Float(1000.0));
m.insert(
"customer".to_string(),
AttributeValue::String("Alice".to_string()),
);
m
},
object_ids: vec!["order1".to_string()],
object_refs: vec![OCELEventObjectRef {
object_id: "order1".to_string(),
qualifier: "main".to_string(),
}],
},
OCELEvent {
id: "e2".to_string(),
event_type: "Check Payment".to_string(),
timestamp: "2024-01-01T10:05:00Z".to_string(),
attributes: {
let mut m = BTreeMap::new();
m.insert(
"status".to_string(),
AttributeValue::String("verified".to_string()),
);
m
},
object_ids: vec!["order1".to_string()],
object_refs: vec![],
},
OCELEvent {
id: "e3".to_string(),
event_type: "Approve Order".to_string(),
timestamp: "2024-01-01T10:10:00Z".to_string(),
attributes: {
let mut m = BTreeMap::new();
m.insert(
"approver".to_string(),
AttributeValue::String("Manager1".to_string()),
);
m
},
object_ids: vec!["order1".to_string()],
object_refs: vec![],
},
OCELEvent {
id: "e4".to_string(),
event_type: "Create Invoice".to_string(),
timestamp: "2024-01-01T10:15:00Z".to_string(),
attributes: {
let mut m = BTreeMap::new();
m.insert("invoice_amount".to_string(), AttributeValue::Float(1000.0));
m
},
object_ids: vec!["order1".to_string(), "invoice1".to_string()],
object_refs: vec![
OCELEventObjectRef {
object_id: "order1".to_string(),
qualifier: "source".to_string(),
},
OCELEventObjectRef {
object_id: "invoice1".to_string(),
qualifier: "created".to_string(),
},
],
},
OCELEvent {
id: "e5".to_string(),
event_type: "Ship Order".to_string(),
timestamp: "2024-01-01T10:20:00Z".to_string(),
attributes: {
let mut m = BTreeMap::new();
m.insert(
"carrier".to_string(),
AttributeValue::String("UPS".to_string()),
);
m
},
object_ids: vec!["order1".to_string()],
object_refs: vec![],
},
OCELEvent {
id: "e6".to_string(),
event_type: "Create Order".to_string(),
timestamp: "2024-01-01T11:00:00Z".to_string(),
attributes: {
let mut m = BTreeMap::new();
m.insert("amount".to_string(), AttributeValue::Float(500.0));
m.insert(
"customer".to_string(),
AttributeValue::String("Bob".to_string()),
);
m
},
object_ids: vec!["order2".to_string()],
object_refs: vec![],
},
OCELEvent {
id: "e7".to_string(),
event_type: "Check Payment".to_string(),
timestamp: "2024-01-01T11:05:00Z".to_string(),
attributes: {
let mut m = BTreeMap::new();
m.insert(
"status".to_string(),
AttributeValue::String("rejected".to_string()),
);
m
},
object_ids: vec!["order2".to_string()],
object_refs: vec![],
},
],
objects: vec![
OCELObject {
id: "order1".to_string(),
object_type: "Order".to_string(),
attributes: {
let mut m = BTreeMap::new();
m.insert(
"status".to_string(),
AttributeValue::String("completed".to_string()),
);
m.insert("amount".to_string(), AttributeValue::Float(1000.0));
m
},
changes: vec![],
embedded_relations: vec![],
},
OCELObject {
id: "order2".to_string(),
object_type: "Order".to_string(),
attributes: {
let mut m = BTreeMap::new();
m.insert(
"status".to_string(),
AttributeValue::String("rejected".to_string()),
);
m.insert("amount".to_string(), AttributeValue::Float(500.0));
m
},
changes: vec![],
embedded_relations: vec![],
},
OCELObject {
id: "invoice1".to_string(),
object_type: "Invoice".to_string(),
attributes: {
let mut m = BTreeMap::new();
m.insert("amount".to_string(), AttributeValue::Float(1000.0));
m.insert(
"date".to_string(),
AttributeValue::String("2024-01-01".to_string()),
);
m
},
changes: vec![],
embedded_relations: vec![],
},
],
object_relations: vec![],
}
}
#[test]
fn test_ocel_loading_event_count() {
let ocel = create_order_invoice_ocel();
assert_eq!(
ocel.events.len(),
7,
"OCEL should have 7 events (2 orders × main flow + 1 alternative flow variant)"
);
}
#[test]
fn test_ocel_loading_object_count() {
let ocel = create_order_invoice_ocel();
assert_eq!(
ocel.objects.len(),
3,
"OCEL should have 3 objects (order1, order2, invoice1)"
);
}
#[test]
fn test_ocel_loading_object_types() {
let ocel = create_order_invoice_ocel();
let obj_types: HashSet<String> = ocel.object_types.iter().cloned().collect();
assert!(obj_types.contains("Order"), "Should have Order type");
assert!(obj_types.contains("Invoice"), "Should have Invoice type");
assert_eq!(obj_types.len(), 2, "Should have exactly 2 object types");
}
#[test]
fn test_ocel_loading_event_attributes() {
let ocel = create_order_invoice_ocel();
let create_order_event = ocel.events.iter().find(|e| e.id == "e1").unwrap();
assert_eq!(
create_order_event.attributes.len(),
2,
"Create Order event should have 2 attributes (amount, customer)"
);
assert!(create_order_event.attributes.contains_key("amount"));
assert!(create_order_event.attributes.contains_key("customer"));
}
#[test]
fn test_ocel_loading_multi_object_event() {
let ocel = create_order_invoice_ocel();
let multi_event = ocel.events.iter().find(|e| e.id == "e4").unwrap();
assert_eq!(
multi_event.object_ids.len(),
2,
"Create Invoice event should reference 2 objects (order1, invoice1)"
);
assert!(multi_event.object_ids.contains(&"order1".to_string()));
assert!(multi_event.object_ids.contains(&"invoice1".to_string()));
}
#[test]
fn test_discover_ocel_dfg_basic() {
let ocel = create_order_invoice_ocel();
let dfg = discover_ocel_dfg_pure(&ocel);
assert_eq!(
dfg.nodes.len(),
5,
"DFG should have 5 nodes (one per event type)"
);
let node_labels: HashSet<String> = dfg.nodes.iter().map(|n| n.label.clone()).collect();
assert!(node_labels.contains("Create Order"));
assert!(node_labels.contains("Check Payment"));
assert!(node_labels.contains("Approve Order"));
assert!(node_labels.contains("Create Invoice"));
assert!(node_labels.contains("Ship Order"));
}
#[test]
fn test_discover_ocel_dfg_edges() {
let ocel = create_order_invoice_ocel();
let dfg = discover_ocel_dfg_pure(&ocel);
assert!(!dfg.edges.is_empty(), "DFG should have edges");
let create_to_check = dfg
.edges
.iter()
.find(|e| e.from == "Create Order" && e.to == "Check Payment");
assert!(
create_to_check.is_some(),
"Should have Create Order → Check Payment edge"
);
assert_eq!(
create_to_check.unwrap().frequency,
2,
"Edge frequency should be 2 (both order1 and order2)"
);
}
#[test]
fn test_discover_ocel_dfg_start_end_activities() {
let ocel = create_order_invoice_ocel();
let dfg = discover_ocel_dfg_pure(&ocel);
assert_eq!(
*dfg.start_activities.get("Create Order").unwrap_or(&0),
2,
"Create Order should be start activity for 2 objects (order1, order2)"
);
assert!(
dfg.end_activities.contains_key("Ship Order")
|| dfg.end_activities.contains_key("Check Payment"),
"Should have end activities"
);
}
#[test]
fn test_discover_ocel_dfg_per_type() {
let ocel = create_order_invoice_ocel();
let mut per_type_dfgs: BTreeMap<String, Vec<(usize, &str)>> = BTreeMap::new();
for obj_type in &ocel.object_types {
for obj in ocel.objects.iter().filter(|o| &o.object_type == obj_type) {
let mut type_events = Vec::new();
for (idx, event) in ocel.events.iter().enumerate() {
if event.object_ids.contains(&obj.id) {
type_events.push((idx, event.event_type.as_str()));
}
}
per_type_dfgs
.entry(obj_type.clone())
.or_insert_with(Vec::new)
.extend(type_events);
}
}
assert!(
per_type_dfgs.contains_key("Order"),
"Should have Order type in per-type discovery"
);
assert!(
per_type_dfgs["Order"].len() > 0,
"Order type should have events"
);
assert!(
per_type_dfgs.contains_key("Invoice"),
"Should have Invoice type in per-type discovery"
);
assert_eq!(
per_type_dfgs["Invoice"].len(),
1,
"Invoice type should have 1 event (e4)"
);
}
#[test]
fn test_multi_perspective_control_flow_violation() {
let mut ocel = create_order_invoice_ocel();
let create_order_event = ocel.events.iter_mut().find(|e| e.id == "e6").unwrap();
create_order_event.timestamp = "2024-01-01T11:10:00Z".to_string();
let check_payment_event = ocel.events.iter_mut().find(|e| e.id == "e7").unwrap();
check_payment_event.timestamp = "2024-01-01T11:05:00Z".to_string();
let violations = validate_ocel_object_lifecycles(&ocel);
assert!(
!violations.is_empty(),
"Should detect lifecycle violation: Check Payment before Create Order"
);
assert!(
violations.iter().any(|v| v.object_id == "order2"),
"Violation should be for order2"
);
}
#[test]
fn test_multi_perspective_object_integrity() {
let ocel = create_order_invoice_ocel();
let violations = validate_ocel_object_lifecycles(&ocel);
assert!(
violations.is_empty(),
"Valid OCEL should have no lifecycle violations. Found: {:?}",
violations
);
}
#[test]
fn test_object_lifecycle_creation_violation() {
let ocel = OCEL {
event_types: vec!["Create Item".to_string(), "Update Item".to_string()],
object_types: vec!["Item".to_string()],
events: vec![
OCELEvent {
id: "e1".to_string(),
event_type: "Update Item".to_string(),
timestamp: "2024-01-01T10:05:00Z".to_string(),
attributes: BTreeMap::new(),
object_ids: vec!["item1".to_string()],
object_refs: vec![],
},
OCELEvent {
id: "e2".to_string(),
event_type: "Create Item".to_string(),
timestamp: "2024-01-01T10:00:00Z".to_string(),
attributes: BTreeMap::new(),
object_ids: vec!["item1".to_string()],
object_refs: vec![],
},
],
objects: vec![OCELObject {
id: "item1".to_string(),
object_type: "Item".to_string(),
attributes: BTreeMap::new(),
changes: vec![],
embedded_relations: vec![],
}],
object_relations: vec![],
};
let violations = validate_ocel_object_lifecycles(&ocel);
assert!(
!violations.is_empty(),
"Should detect lifecycle violation: timestamp inversion (e1 at 10:05, e2 at 10:00)"
);
assert_eq!(
violations[0].object_id, "item1",
"Violation should be on item1"
);
assert_eq!(
violations[0].event_a_id, "e1",
"First event (chronologically) should be e1"
);
assert_eq!(
violations[0].event_b_id, "e2",
"Second event in log should be e2"
);
}
#[test]
fn test_object_lifecycle_multiple_violations() {
let ocel = OCEL {
event_types: vec!["A".to_string(), "B".to_string(), "C".to_string()],
object_types: vec!["Type1".to_string(), "Type2".to_string()],
events: vec![
OCELEvent {
id: "e1".to_string(),
event_type: "B".to_string(),
timestamp: "2024-01-01T10:00:00Z".to_string(),
attributes: BTreeMap::new(),
object_ids: vec!["obj1".to_string()],
object_refs: vec![],
},
OCELEvent {
id: "e2".to_string(),
event_type: "A".to_string(),
timestamp: "2024-01-01T09:00:00Z".to_string(),
attributes: BTreeMap::new(),
object_ids: vec!["obj1".to_string()],
object_refs: vec![],
},
OCELEvent {
id: "e3".to_string(),
event_type: "C".to_string(),
timestamp: "2024-01-01T11:00:00Z".to_string(),
attributes: BTreeMap::new(),
object_ids: vec!["obj2".to_string()],
object_refs: vec![],
},
OCELEvent {
id: "e4".to_string(),
event_type: "B".to_string(),
timestamp: "2024-01-01T10:00:00Z".to_string(),
attributes: BTreeMap::new(),
object_ids: vec!["obj2".to_string()],
object_refs: vec![],
},
],
objects: vec![
OCELObject {
id: "obj1".to_string(),
object_type: "Type1".to_string(),
attributes: BTreeMap::new(),
changes: vec![],
embedded_relations: vec![],
},
OCELObject {
id: "obj2".to_string(),
object_type: "Type2".to_string(),
attributes: BTreeMap::new(),
changes: vec![],
embedded_relations: vec![],
},
],
object_relations: vec![],
};
let violations = validate_ocel_object_lifecycles(&ocel);
assert!(!violations.is_empty(), "Should detect multiple violations");
assert!(
violations.iter().any(|v| v.object_id == "obj1"),
"Should have obj1 violation"
);
assert!(
violations.iter().any(|v| v.object_id == "obj2"),
"Should have obj2 violation"
);
}
#[test]
fn test_object_lifecycle_concurrent_events() {
let ocel = OCEL {
event_types: vec!["A".to_string(), "B".to_string()],
object_types: vec!["Order".to_string()],
events: vec![
OCELEvent {
id: "e1".to_string(),
event_type: "A".to_string(),
timestamp: "2024-01-01T10:00:00Z".to_string(),
attributes: BTreeMap::new(),
object_ids: vec!["order1".to_string()],
object_refs: vec![],
},
OCELEvent {
id: "e2".to_string(),
event_type: "B".to_string(),
timestamp: "2024-01-01T10:00:00Z".to_string(), attributes: BTreeMap::new(),
object_ids: vec!["order1".to_string()],
object_refs: vec![],
},
],
objects: vec![OCELObject {
id: "order1".to_string(),
object_type: "Order".to_string(),
attributes: BTreeMap::new(),
changes: vec![],
embedded_relations: vec![],
}],
object_relations: vec![],
};
let violations = validate_ocel_object_lifecycles(&ocel);
assert!(
violations.is_empty(),
"Concurrent events (same timestamp) should be allowed"
);
}