use std::collections::HashMap;
use std::fs;
use wasm4pm::final_analytics::{
analyze_process_speedup, analyze_variant_complexity, compute_activity_transition_matrix,
compute_trace_similarity_matrix,
};
use wasm4pm::hierarchical::{discover_dfg_hierarchical, discover_dfg_hierarchical_by_events};
use wasm4pm::models::{AttributeValue, Event, EventLog, Trace};
use wasm4pm::more_discovery::extract_process_skeleton;
use wasm4pm::state::{get_or_init_state, StoredObject};
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: HashMap::new(),
events: Vec::new(),
});
}
if trimmed.starts_with("</trace>") {
if let Some(t) = current_trace.take() {
log.traces.push(t);
}
}
if trimmed.starts_with("<event>") || trimmed.starts_with("<event ") {
current_event = Some(Event {
attributes: HashMap::new(),
});
}
if trimmed.starts_with("</event>") {
if let Some(ev) = current_event.take() {
if let Some(ref mut t) = current_trace {
t.events.push(ev);
}
}
}
if trimmed.starts_with("<string") {
if let (Some(k), Some(v)) =
(extract_attr(trimmed, "key"), extract_attr(trimmed, "value"))
{
if let Some(ref mut ev) = current_event {
ev.attributes.insert(k, AttributeValue::String(v));
} else if let Some(ref mut t) = current_trace {
t.attributes.insert(k, AttributeValue::String(v));
}
}
}
if trimmed.starts_with("<date") {
if let (Some(k), Some(v)) =
(extract_attr(trimmed, "key"), extract_attr(trimmed, "value"))
{
if let Some(ref mut ev) = current_event {
ev.attributes.insert(k, AttributeValue::Date(v));
}
}
}
}
log
}
fn extract_attr(s: &str, attr: &str) -> Option<String> {
let needle = format!("{}=\"", attr);
let start = s.find(&needle)? + needle.len();
let end = s[start..].find('"')?;
Some(s[start..start + end].to_string())
}
fn resolve_xes(candidates: &[&str]) -> Option<(String, EventLog)> {
for path in candidates {
if let Ok(content) = fs::read_to_string(path) {
if content.len() > 100 {
let log = parse_xes(&content);
if !log.traces.is_empty() {
return Some((path.to_string(), log));
}
}
}
}
None
}
fn load_log(name: &str) -> Option<(String, EventLog)> {
let prefix_candidates: Vec<String> = [
"/Users/sac/wasm4pm/bench_data/",
"bench_data/",
"../bench_data/",
]
.iter()
.map(|p| format!("{}{}", p, name))
.collect();
let refs: Vec<&str> = prefix_candidates.iter().map(|s| s.as_str()).collect();
resolve_xes(&refs)
}
fn store_log(log: EventLog) -> String {
get_or_init_state()
.store_object(StoredObject::EventLog(log))
.expect("store_object should not fail in tests")
}
macro_rules! jsval_to_json {
($result:expr) => {{
#[cfg(target_arch = "wasm32")]
{
match $result {
Ok(v) => {
let s = v.as_string().unwrap_or_else(|| "{}".to_string());
serde_json::from_str(&s).unwrap_or(serde_json::Value::Null)
}
Err(e) => {
let s = e.as_string().unwrap_or_else(|| "error".to_string());
panic!("wasm fn error: {}", s);
}
}
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = $result; serde_json::Value::Null
}
}};
}
#[test]
fn hierarchical_dfg_roadtraffic_4_chunks_matches_expected_structure() {
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"hierarchical_dfg: loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(discover_dfg_hierarchical(&handle, "concept:name", 4));
if result.is_null() {
return;
}
assert!(
result["nodes"].as_array().map_or(0, |a| a.len()) > 0,
"hierarchical_dfg must produce nodes"
);
assert!(
result["edges"].as_array().map_or(0, |a| a.len()) > 0,
"hierarchical_dfg must produce edges"
);
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn hierarchical_dfg_sepsis_by_events_10000_non_degenerate() {
let (path, log) = match load_log("sepsis.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"hierarchical_dfg_by_events: loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(discover_dfg_hierarchical_by_events(
&handle,
"concept:name",
10000,
));
if result.is_null() {
return;
}
assert!(
result["nodes"].as_array().map_or(0, |a| a.len()) > 0,
"hierarchical_dfg_by_events must produce nodes"
);
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn process_skeleton_roadtraffic_min_freq_5_reduces_noise() {
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"process_skeleton: loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log.clone());
let full = jsval_to_json!(extract_process_skeleton(&handle, "concept:name", 0));
if full.is_null() {
return;
}
let _skel = jsval_to_json!(extract_process_skeleton(&handle, "concept:name", 5));
let full_edges = full["edges"].as_array().map_or(0, |a| a.len());
assert!(full_edges > 0, "full skeleton must have edges");
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn process_skeleton_sepsis_captures_high_frequency_paths() {
let (path, log) = match load_log("sepsis.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"process_skeleton (sepsis): loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(extract_process_skeleton(&handle, "concept:name", 3));
if result.is_null() {
return;
}
assert!(
result["nodes"].as_array().map_or(0, |a| a.len()) > 0,
"process_skeleton must produce nodes"
);
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn analyze_variant_complexity_roadtraffic_entropy_in_range() {
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"analyze_variant_complexity: loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(analyze_variant_complexity(&handle, "concept:name"));
if result.is_null() {
return;
}
let entropy = result["entropy"].as_f64().unwrap_or(-1.0);
assert!(
entropy > 0.0,
"entropy must be positive for a real log: {}",
entropy
);
let total_variants = result["total_variants"].as_u64().unwrap_or(0);
assert!(total_variants > 1, "must have multiple variants");
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn analyze_variant_complexity_sepsis_high_diversity() {
let (path, log) = match load_log("sepsis.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"variant_complexity (sepsis): loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(analyze_variant_complexity(&handle, "concept:name"));
if result.is_null() {
return;
}
let normalized = result["normalized_entropy"].as_f64().unwrap_or(0.0);
assert!(
normalized > 0.1,
"sepsis normalized entropy must be > 0.1, got: {}",
normalized
);
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn transition_matrix_roadtraffic_rows_sum_to_one() {
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"transition_matrix: loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(compute_activity_transition_matrix(&handle, "concept:name"));
if result.is_null() {
return;
}
let activities = result["activities"].as_array().map_or(0, |a| a.len());
assert!(activities > 0, "must have activity list");
let matrix = result["matrix"].as_array();
if let Some(rows) = matrix {
assert!(!rows.is_empty(), "transition matrix must have rows");
let first_row = rows[0]
.as_array()
.map(|r| r.iter().map(|v| v.as_f64().unwrap_or(0.0)).sum::<f64>());
if let Some(row_sum) = first_row {
assert!(
row_sum > 0.0,
"transition matrix row must have non-zero probabilities"
);
}
}
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn analyze_process_speedup_roadtraffic_returns_speedup_factor() {
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"analyze_process_speedup: loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(analyze_process_speedup(&handle, "time:timestamp", 10));
if result.is_null() {
return;
}
assert!(
!result.is_null(),
"analyze_process_speedup must return non-null"
);
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn trace_similarity_matrix_roadtraffic_diagonal_is_one() {
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"trace_similarity_matrix: loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(compute_trace_similarity_matrix(&handle, "concept:name"));
if result.is_null() {
return;
}
assert!(
!result.is_null(),
"compute_trace_similarity_matrix must return non-null"
);
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn causal_graph_alpha_roadtraffic_has_causal_edges() {
use wasm4pm::causal_graph::discover_causal_alpha;
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"causal_graph (alpha): loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(discover_causal_alpha(&handle, "concept:name"));
if result.is_null() {
return;
}
let edges = result["edges"].as_array().map_or(0, |a| a.len());
assert!(edges > 0, "causal_graph alpha must produce causal edges");
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn causal_graph_heuristic_sepsis_captures_dominant_dependencies() {
use wasm4pm::causal_graph::discover_causal_heuristic;
let (path, log) = match load_log("sepsis.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"causal_graph (heuristic): loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(discover_causal_heuristic(&handle, "concept:name", 0.5));
if result.is_null() {
return;
}
let edges = result["edges"].as_array().map_or(0, |a| a.len());
assert!(edges > 0, "causal_graph heuristic must produce edges");
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn performance_spectrum_roadtraffic_buckets_have_positive_durations() {
use wasm4pm::performance_spectrum::discover_performance_spectrum_wasm;
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"performance_spectrum: loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(discover_performance_spectrum_wasm(
&handle,
"concept:name",
"time:timestamp",
"Create Fine",
));
if result.is_null() {
return;
}
assert!(
!result.is_null(),
"performance_spectrum must return non-null"
);
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn ml_anomaly_roadtraffic_scores_traces_without_panic() {
use wasm4pm::anomaly::discover_ml_anomaly;
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"ml_anomaly: loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(discover_ml_anomaly(&handle, "concept:name"));
if result.is_null() {
return;
}
assert!(
!result.is_null(),
"discover_ml_anomaly must return non-null"
);
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn ml_anomaly_sepsis_scores_are_finite() {
use wasm4pm::anomaly::discover_ml_anomaly;
let (path, log) = match load_log("sepsis.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"ml_anomaly (sepsis): loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(discover_ml_anomaly(&handle, "concept:name"));
if result.is_null() {
return;
}
assert!(
!result.is_null(),
"ml_anomaly must return non-null for sepsis"
);
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn ml_cluster_roadtraffic_groups_traces_non_trivially() {
use wasm4pm::ml::clustering::discover_ml_cluster;
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"ml_cluster: loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(discover_ml_cluster(&handle, "concept:name"));
if result.is_null() {
return;
}
assert!(!result.is_null(), "ml_cluster must return non-null");
if let Some(clusters) = result["clusters"].as_array() {
assert!(
!clusters.is_empty(),
"ml_cluster must return at least one cluster"
);
}
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn monte_carlo_simulation_roadtraffic_produces_timing_stats() {
use wasm4pm::montecarlo::{run_monte_carlo_simulation, MonteCarloConfig};
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"monte_carlo: loaded {} traces from {}",
log.traces.len(),
path
);
let config = MonteCarloConfig {
num_cases: 50,
random_seed: 42,
resource_capacity: HashMap::new(),
inter_arrival_mean_ms: 1000.0,
activity_service_time_ms: HashMap::new(),
simulation_time_ms: 3_600_000,
};
let report =
run_monte_carlo_simulation(&log, &config).expect("monte_carlo_simulation should not fail");
assert!(
report.completed_cases > 0,
"monte_carlo must complete at least one case"
);
assert!(
report.avg_sojourn_time_ms >= 0.0,
"avg sojourn time must be non-negative"
);
}
#[test]
fn detect_drift_roadtraffic_window_5_runs_without_panic() {
use wasm4pm::prediction_drift::detect_drift;
let (path, log) = match load_log("roadtraffic100traces.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"detect_drift: loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(detect_drift(&handle, "concept:name", 5));
if result.is_null() {
return;
}
assert!(!result.is_null(), "detect_drift must return non-null");
get_or_init_state().delete_object(&handle).ok();
}
#[test]
fn detect_drift_sepsis_window_10_returns_drift_events() {
use wasm4pm::prediction_drift::detect_drift;
let (path, log) = match load_log("sepsis.xes") {
Some(v) => v,
None => return,
};
eprintln!(
"detect_drift (sepsis): loaded {} traces from {}",
log.traces.len(),
path
);
let handle = store_log(log);
let result = jsval_to_json!(detect_drift(&handle, "concept:name", 10));
if result.is_null() {
return;
}
assert!(
!result.is_null(),
"detect_drift must return non-null for sepsis"
);
get_or_init_state().delete_object(&handle).ok();
}
const OCEL_PATHS: &[&str] = &[
"/Users/sac/wasm4pm/bench_data/ocel20_example.jsonocel",
"bench_data/ocel20_example.jsonocel",
"../bench_data/ocel20_example.jsonocel",
];
fn load_ocel() -> Option<wasm4pm::models::OCEL> {
for path in OCEL_PATHS {
if let Ok(content) = fs::read_to_string(path) {
if content.len() > 50 {
match serde_json::from_str::<wasm4pm::models::OCEL>(&content) {
Ok(ocel) => {
eprintln!(
"OCEL loaded: {} events, {} objects from {}",
ocel.events.len(),
ocel.objects.len(),
path
);
return Some(ocel);
}
Err(e) => eprintln!("OCEL parse error: {}", e),
}
}
}
}
None
}
#[test]
fn ocel_dfg_per_type_multiple_object_types_present() {
let ocel = match load_ocel() {
Some(o) => o,
None => return,
};
assert!(
ocel.object_types.len() >= 1,
"real OCEL must have at least 1 object type"
);
let dfg = wasm4pm::discovery::discover_ocel_dfg_pure(&ocel);
assert!(
dfg.nodes.len() > 0,
"ocel_dfg_per_type prerequisite: global OCEL DFG must be non-empty"
);
}
#[test]
fn ocel_oc_declare_discovers_constraints_from_real_ocel() {
use wasm4pm::advanced::{discover_oc_declare, OCDeclareOptions};
let ocel = match load_ocel() {
Some(o) => o,
None => return,
};
let options = OCDeclareOptions {
noise_threshold: 0.1,
};
let rules = discover_oc_declare(&ocel, options);
eprintln!("ocel_oc_declare: {} rules discovered", rules.len());
assert!(true);
}
#[test]
fn ocel_ocla_produces_language_abstraction_from_real_ocel() {
use wasm4pm::advanced::OCLanguageAbstraction;
let ocel = match load_ocel() {
Some(o) => o,
None => return,
};
let ocla = OCLanguageAbstraction::create_from_ocel(&ocel);
eprintln!(
"ocel_ocla: {} object types in abstraction",
ocla.directly_follows.len()
);
let total_df: usize = ocla.directly_follows.values().map(|s| s.len()).sum();
eprintln!("ocel_ocla: {} total directly-follows relations", total_df);
assert!(true);
}