use std::collections::BTreeMap;
use wasm4pm::advanced_algorithms::discover_heuristic_miner_from_log;
use wasm4pm::discovery::discover_dfg_from_log;
use wasm4pm::genetic_discovery::{
discover_aco_algorithm_from_log, discover_genetic_algorithm_from_log,
discover_pso_algorithm_from_log,
};
use wasm4pm::models::{AttributeValue, Event, EventLog, Trace};
use wasm4pm::more_discovery::{
discover_inductive_miner_from_log, discover_simulated_annealing_from_log,
};
fn empty_log() -> EventLog {
EventLog::new()
}
fn single_trace_single_event() -> EventLog {
let mut log = EventLog::new();
let mut trace = Trace {
attributes: {
let mut m = BTreeMap::new();
m.insert(
"concept:name".to_string(),
AttributeValue::String("case-1".to_string()),
);
m
},
events: vec![],
};
let mut attrs = BTreeMap::new();
attrs.insert(
"concept:name".to_string(),
AttributeValue::String("Activity".to_string()),
);
trace.events.push(Event { attributes: attrs });
log.traces.push(trace);
log
}
fn standard_log() -> EventLog {
let mut log = EventLog::new();
for case in 0..10 {
let mut trace = Trace {
attributes: {
let mut m = BTreeMap::new();
m.insert(
"concept:name".to_string(),
AttributeValue::String(format!("case-{}", case)),
);
m
},
events: vec![],
};
for (i, act) in vec!["Start", "Process", "End"].iter().enumerate() {
let mut attrs = BTreeMap::new();
attrs.insert(
"concept:name".to_string(),
AttributeValue::String(act.to_string()),
);
attrs.insert(
"time:timestamp".to_string(),
AttributeValue::String(format!("2024-01-01T00:{:02}:00Z", i)),
);
trace.events.push(Event { attributes: attrs });
}
log.traces.push(trace);
}
log
}
fn rare_char_log() -> EventLog {
let mut log = EventLog::new();
let mut trace = Trace {
attributes: {
let mut m = BTreeMap::new();
m.insert(
"concept:name".to_string(),
AttributeValue::String("case-rare".to_string()),
);
m
},
events: vec![],
};
for act in &["Café", "データ処理", "🔧", "A|B"] {
let mut attrs = BTreeMap::new();
attrs.insert(
"concept:name".to_string(),
AttributeValue::String(act.to_string()),
);
trace.events.push(Event { attributes: attrs });
}
log.traces.push(trace);
log
}
#[test]
fn ga_determinism_same_seed_bit_identical() {
let log = standard_log();
let (dfg1, fitness1) = discover_genetic_algorithm_from_log(&log, "concept:name", 20, 30)
.expect("GA must succeed on standard log");
let (dfg2, fitness2) = discover_genetic_algorithm_from_log(&log, "concept:name", 20, 30)
.expect("GA must succeed on standard log");
assert_eq!(
fitness1, fitness2,
"GA FITNESS NOT DETERMINISTIC: {:.6} vs {:.6}",
fitness1, fitness2
);
assert_eq!(
dfg1.edges.len(),
dfg2.edges.len(),
"GA EDGE COUNT NOT DETERMINISTIC: {} vs {}",
dfg1.edges.len(),
dfg2.edges.len()
);
}
#[test]
fn pso_determinism_same_seed_bit_identical() {
let log = standard_log();
let (dfg1, fitness1) = discover_pso_algorithm_from_log(&log, "concept:name", 20, 30)
.expect("PSO must succeed on standard log");
let (dfg2, fitness2) = discover_pso_algorithm_from_log(&log, "concept:name", 20, 30)
.expect("PSO must succeed on standard log");
assert_eq!(
fitness1, fitness2,
"PSO FITNESS NOT DETERMINISTIC: {:.6} vs {:.6}",
fitness1, fitness2
);
assert_eq!(
dfg1.edges.len(),
dfg2.edges.len(),
"PSO EDGE COUNT NOT DETERMINISTIC: {} vs {}",
dfg1.edges.len(),
dfg2.edges.len()
);
}
#[test]
fn aco_determinism_same_seed_bit_identical() {
let log = standard_log();
let (dfg1, fitness1) = discover_aco_algorithm_from_log(&log, "concept:name", 20, 30)
.expect("ACO must succeed on standard log");
let (dfg2, fitness2) = discover_aco_algorithm_from_log(&log, "concept:name", 20, 30)
.expect("ACO must succeed on standard log");
assert_eq!(
fitness1, fitness2,
"ACO FITNESS NOT DETERMINISTIC: {:.6} vs {:.6}",
fitness1, fitness2
);
assert_eq!(
dfg1.edges.len(),
dfg2.edges.len(),
"ACO EDGE COUNT NOT DETERMINISTIC: {} vs {}",
dfg1.edges.len(),
dfg2.edges.len()
);
}
#[test]
fn sa_determinism_same_seed_bit_identical() {
let log = standard_log();
let (dfg1, fitness1) = discover_simulated_annealing_from_log(&log, "concept:name", 1.0, 0.95);
let (dfg2, fitness2) = discover_simulated_annealing_from_log(&log, "concept:name", 1.0, 0.95);
assert_eq!(
fitness1, fitness2,
"SA FITNESS NOT DETERMINISTIC: {:.6} vs {:.6}",
fitness1, fitness2
);
assert_eq!(
dfg1.edges.len(),
dfg2.edges.len(),
"SA EDGE COUNT NOT DETERMINISTIC: {} vs {}",
dfg1.edges.len(),
dfg2.edges.len()
);
}
#[test]
fn heuristic_miner_threshold_monotonicity() {
let log = standard_log();
let dfg_strict = discover_heuristic_miner_from_log(&log, "concept:name", 0.9);
let dfg_lenient = discover_heuristic_miner_from_log(&log, "concept:name", 0.1);
assert!(
dfg_lenient.edges.len() >= dfg_strict.edges.len(),
"MONOTONICITY VIOLATED: Lower threshold should produce >= edges. \
Strict (0.9): {}, Lenient (0.1): {}",
dfg_strict.edges.len(),
dfg_lenient.edges.len()
);
}
#[test]
fn ga_iterations_monotonicity() {
let log = standard_log();
let (_, f1) =
discover_genetic_algorithm_from_log(&log, "concept:name", 20, 5).expect("GA must succeed");
let (_, f50) =
discover_genetic_algorithm_from_log(&log, "concept:name", 20, 50).expect("GA must succeed");
assert!(
f50 >= f1 - 1e-9,
"MONOTONICITY VIOLATED: More generations should maintain fitness. \
5-gen: {:.6}, 50-gen: {:.6}",
f1,
f50
);
}
#[test]
fn pso_iterations_monotonicity() {
let log = standard_log();
let (_, f5) =
discover_pso_algorithm_from_log(&log, "concept:name", 20, 5).expect("PSO must succeed");
let (_, f50) =
discover_pso_algorithm_from_log(&log, "concept:name", 20, 50).expect("PSO must succeed");
assert!(
f50 >= f5 - 1e-9,
"MONOTONICITY VIOLATED: More iterations should maintain fitness. \
5-iter: {:.6}, 50-iter: {:.6}",
f5,
f50
);
}
#[test]
fn dfg_empty_log_returns_empty_dfg() {
let log = empty_log();
let dfg = discover_dfg_from_log(&admitted_log(log.clone()), "concept:name");
assert_eq!(
dfg.nodes.len(),
0,
"DFG of empty log should have 0 nodes, got {}",
dfg.nodes.len()
);
assert_eq!(
dfg.edges.len(),
0,
"DFG of empty log should have 0 edges, got {}",
dfg.edges.len()
);
}
#[test]
fn heuristic_miner_empty_log_returns_empty_dfg() {
let log = empty_log();
let dfg = discover_heuristic_miner_from_log(&log, "concept:name", 0.5);
assert_eq!(
dfg.nodes.len(),
0,
"Heuristic Miner on empty log should have 0 nodes, got {}",
dfg.nodes.len()
);
}
#[test]
fn inductive_miner_empty_log_returns_flower() {
let log = empty_log();
let result = discover_inductive_miner_from_log(&admitted_log(log.clone()), "concept:name");
assert!(
!result.is_empty(),
"Inductive Miner should return non-empty result"
);
}
#[test]
fn dfg_single_event_returns_single_node() {
let log = single_trace_single_event();
let dfg = discover_dfg_from_log(&admitted_log(log.clone()), "concept:name");
assert_eq!(
dfg.nodes.len(),
1,
"DFG of single-event log should have exactly 1 node, got {}",
dfg.nodes.len()
);
assert_eq!(
dfg.edges.len(),
0,
"DFG of single-event log should have 0 edges (no follows), got {}",
dfg.edges.len()
);
}
#[test]
fn ga_single_event_no_panic() {
let log = single_trace_single_event();
let result = discover_genetic_algorithm_from_log(&log, "concept:name", 20, 30);
assert!(result.is_some() || result.is_none());
}
#[test]
fn dfg_rare_chars_no_panic() {
let log = rare_char_log();
let dfg = discover_dfg_from_log(&admitted_log(log.clone()), "concept:name");
assert!(
dfg.nodes.len() > 0,
"DFG should handle UTF-8 activity names"
);
for node in &dfg.nodes {
assert!(!node.id.is_empty(), "Node ID should not be empty");
}
}
#[test]
fn heuristic_miner_rare_chars_no_panic() {
let log = rare_char_log();
let dfg = discover_heuristic_miner_from_log(&log, "concept:name", 0.5);
assert!(
dfg.nodes.len() > 0,
"Heuristic Miner should handle UTF-8 activity names"
);
}
#[test]
fn inductive_miner_rare_chars_no_panic() {
let log = rare_char_log();
let result = discover_inductive_miner_from_log(&admitted_log(log.clone()), "concept:name");
assert!(
!result.is_empty(),
"Inductive Miner should handle UTF-8 activity names"
);
}
#[test]
fn dfg_output_schema_valid() {
let log = standard_log();
let dfg = discover_dfg_from_log(&admitted_log(log.clone()), "concept:name");
let node_ids: std::collections::HashSet<_> = dfg.nodes.iter().map(|n| &n.id).collect();
for edge in &dfg.edges {
assert!(
node_ids.contains(&edge.from),
"DFG edge FROM '{}' not in nodes",
edge.from
);
assert!(
node_ids.contains(&edge.to),
"DFG edge TO '{}' not in nodes",
edge.to
);
}
}
#[test]
fn heuristic_miner_output_schema_valid() {
let log = standard_log();
let dfg = discover_heuristic_miner_from_log(&log, "concept:name", 0.5);
let node_ids: std::collections::HashSet<_> = dfg.nodes.iter().map(|n| &n.id).collect();
for edge in &dfg.edges {
assert!(
node_ids.contains(&edge.from),
"Heuristic Miner edge FROM '{}' not in nodes",
edge.from
);
assert!(
node_ids.contains(&edge.to),
"Heuristic Miner edge TO '{}' not in nodes",
edge.to
);
}
}
#[test]
#[ignore] fn ga_seed_is_hardcoded() {
let log = standard_log();
let (dfg1, _) =
discover_genetic_algorithm_from_log(&log, "concept:name", 20, 30).expect("GA must succeed");
let (dfg2, _) =
discover_genetic_algorithm_from_log(&log, "concept:name", 20, 30).expect("GA must succeed");
assert_eq!(
dfg1.edges.len(),
dfg2.edges.len(),
"Hardcoded seed ensures determinism"
);
}
fn admitted_log(
log: wasm4pm::models::EventLog,
) -> wasm4pm_compat::evidence::Evidence<
wasm4pm::models::EventLog,
wasm4pm_compat::state::Admitted,
(),
> {
wasm4pm_compat::admission::Admission::<_, ()>::new(log).into_evidence()
}