#[cfg(test)]
mod tests {
use etdl_parser::ast::EtlDocument;
use std::path::PathBuf;
fn fixture_path(filename: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join(filename)
}
#[test]
fn test_parse_full_worked_example() {
let path = fixture_path("order-fulfillment.etdl");
let doc = etdl_parser::parse_document_from_file(&path).unwrap();
assert_eq!(doc.etdl, "1.0.0");
assert_eq!(doc.info.title, "Order Fulfillment Event Tree");
assert_eq!(doc.info.domain, "FulfillmentContext");
assert_eq!(doc.asyncapi_imports.len(), 2);
assert!(doc.asyncapi_imports.contains_key("orders_api"));
assert!(doc.asyncapi_imports.contains_key("payment_api"));
assert_eq!(doc.event_trees.len(), 1);
let tree = &doc.event_trees["OrderFulfillment"];
assert_eq!(tree.initiating_event.id, "OrderPlacedTrigger");
assert_eq!(tree.initiating_event.message.alias, "orders_api");
assert_eq!(
tree.initiating_event.message.pointer,
"#/components/messages/OrderPlaced"
);
assert_eq!(tree.nodes.len(), 5);
let barrier = match &tree.nodes["InventoryCheckBarrier"] {
etdl_parser::ast::Node::Barrier(b) => b,
_ => panic!("expected barrier"),
};
assert_eq!(barrier.branches.len(), 2);
assert_eq!(barrier.branches[0].outcome, "SUCCESS");
assert!(
matches!(
&barrier.branches[0].condition,
etdl_parser::ast::Condition::Comparison(_)
)
);
assert!(
matches!(
&barrier.branches[1].condition,
etdl_parser::ast::Condition::Default
)
);
let op = match &tree.nodes["ProcessPaymentOperation"] {
etdl_parser::ast::Node::Operation(op) => op,
_ => panic!("expected operation"),
};
assert_eq!(op.handler, "stripe_charge_handler");
assert!(op.retry_policy.is_some());
let retry = op.retry_policy.as_ref().unwrap();
assert_eq!(retry.max_attempts, 3);
assert_eq!(op.timeout_ms, Some(5000));
assert!(op.on_failure.is_some());
assert!(op.on_failure_probability_source.is_some());
let fault_trees = doc.fault_trees.as_ref().unwrap();
assert_eq!(fault_trees.len(), 1);
let ft = &fault_trees["PaymentGatewayFailure"];
assert_eq!(ft.top_event.id, "PaymentCaptureFailed");
let gates = ft.gates.as_ref().unwrap();
assert_eq!(gates.len(), 1);
let gate = &gates["GatewayUnavailableOrRejected"];
assert!(matches!(
gate.gate_type,
etdl_parser::ast::GateType::Or
));
assert_eq!(gate.inputs.len(), 2);
assert_eq!(ft.basic_events.len(), 2);
let be1 = &ft.basic_events["GatewayUnreachable"];
assert_eq!(be1.probability, Some(0.008));
let be2 = &ft.basic_events["ChargeRejected"];
assert_eq!(be2.failure_rate, Some(0.00021));
assert_eq!(be2.mission_time, Some(24.0));
}
#[test]
fn test_asyncapi_loading() {
let base_dir = fixture_path("");
let doc_path = fixture_path("order-fulfillment.etdl");
let doc = etdl_parser::parse_document_from_file(&doc_path).unwrap();
let mut registry = etdl_parser::asyncapi::AsyncApiRegistry::new();
for (alias, location) in &doc.asyncapi_imports {
registry.load(alias, location, &base_dir).unwrap();
}
let message_ref = &doc.event_trees["OrderFulfillment"]
.initiating_event
.message;
let resolved = registry.resolve(message_ref).unwrap();
assert!(resolved.get("payload").is_some());
let payload = &resolved["payload"];
assert_eq!(payload["type"], "object");
}
#[test]
fn test_compile_worked_example() {
let base_dir = fixture_path("");
let doc_path = fixture_path("order-fulfillment.etdl");
let doc = etdl_parser::parse_document_from_file(&doc_path).unwrap();
let registry = etdl_parser::load_asyncapi_imports(&doc, &base_dir).unwrap();
let compiler = etdl_compiler::Compiler::new();
let result = compiler.compile(&doc, ®istry);
assert!(result.rust_output.is_some(), "Compilation should produce Rust code. Diagnostics: {:?}", result.diagnostics);
let rust_code = result.rust_output.unwrap();
println!("=== GENERATED RUST CODE ===\n{}\n=== END ===", rust_code);
assert!(rust_code.contains("AUTOGENERATED BY ETDL COMPILER"));
assert!(rust_code.contains("handle_order_placed_trigger"));
assert!(rust_code.contains("BranchMonitor::new"));
assert!(rust_code.contains("stripe_charge_handler"));
assert!(rust_code.contains("record_branch"));
assert!(
rust_code.contains("InventoryCheckBarrier"),
"Generated code should reference InventoryCheckBarrier"
);
let errors: Vec<_> = result.diagnostics.iter().filter(|d| d.is_error()).collect();
assert!(errors.is_empty(), "Compilation should have no errors, got: {:?}", errors);
}
#[test]
fn test_ecel_parsing() {
use etdl_parser::ecel::*;
let cond = parse_condition("message.payload.items[*].qty > 0").unwrap();
match cond {
Condition::Comparison(cmp) => {
assert_eq!(cmp.op, Comparator::Gt);
match &cmp.left {
Operand::Path(path) => {
assert_eq!(path.segments.len(), 5);
}
_ => panic!("expected path"),
}
match &cmp.right {
Operand::Literal(Literal::Number(n)) => assert_eq!(*n, 0.0),
_ => panic!("expected number literal"),
}
}
_ => panic!("expected comparison"),
}
}
#[test]
fn test_fault_tree_probability() {
let _base_dir = fixture_path("");
let doc_path = fixture_path("order-fulfillment.etdl");
let doc = etdl_parser::parse_document_from_file(&doc_path).unwrap();
let mut diagnostics = Vec::new();
let probs = etdl_compiler::fault_tree::resolve_fault_trees(&doc, &mut diagnostics);
assert!(
diagnostics.iter().all(|d| !d.is_error()),
"Fault tree resolution should have no errors: {:?}",
diagnostics
);
let payment_failure_prob = probs.get("PaymentGatewayFailure").unwrap();
let charge_rejected_prob = 1.0 - (-0.00021_f64 * 24.0).exp();
let expected = 1.0 - (1.0 - 0.008) * (1.0 - charge_rejected_prob);
assert!(
(payment_failure_prob - expected).abs() < 0.00001,
"Expected ~{:.6}, got {:.6}",
expected,
payment_failure_prob
);
}
#[test]
fn test_validation_detects_errors() {
let yaml = r#"
etdl: "1.0.0"
info:
title: "Bad Tree"
version: "1.0.0"
domain: "Test"
asyncapi_imports: {}
eventTrees:
Bad:
initiatingEvent:
id: Test
message: "bad#/foo"
next: MissingNode
nodes: {}
"#;
let doc: EtlDocument = serde_yaml::from_str(yaml).unwrap();
let mut diagnostics = Vec::new();
let registry = etdl_parser::asyncapi::AsyncApiRegistry::new();
etdl_compiler::validate::validate_document(&doc, ®istry, &mut diagnostics);
let errors: Vec<_> = diagnostics.iter().filter(|d| d.is_error()).collect();
assert!(!errors.is_empty(), "Should detect errors in bad document");
assert!(
errors.iter().any(|d| d.code == "E-103" || d.code == "V-101"),
"Should have reference or structural errors, got: {:?}",
errors
);
}
#[test]
fn test_advanced_fault_tree_features() {
use etdl_parser::ast::{BasicEventType, GateType};
let path = fixture_path("advanced-fault-tree.etdl");
let doc = etdl_parser::parse_document_from_file(&path).unwrap();
let ft = &doc.fault_trees.as_ref().unwrap()["AdvancedTree"];
let gates = ft.gates.as_ref().unwrap();
assert_eq!(
gates["InhibitedFailure"].gate_type,
GateType::Inhibit
);
assert_eq!(
gates["InhibitedFailure"].inhibit_condition.as_deref(),
Some("Control loop armed")
);
assert_eq!(
gates["PriorityFailure"].gate_type,
GateType::PriorityAnd
);
assert_eq!(
ft.basic_events["ExternalCause"].event_type,
Some(BasicEventType::House)
);
assert_eq!(
ft.basic_events["UnderAnalyzed"].event_type,
Some(BasicEventType::Undeveloped)
);
assert_eq!(ft.basic_events["EventA"].event_type, None);
let transfers = ft.transfers.as_ref().unwrap();
assert_eq!(
transfers["SubAnalysis"].target,
"#/faultTrees/OtherTree/topEvent"
);
assert_eq!(
transfers["SubAnalysis"].label.as_deref(),
Some("See sub-analysis")
);
let mut diags = Vec::new();
let probs = etdl_compiler::fault_tree::resolve_fault_trees(&doc, &mut diags);
let advanced = probs["AdvancedTree"];
let inhibit = 0.1 * 0.5;
let priority = (0.2 * 0.3) / 2.0;
let expected = 1.0
- (1.0 - inhibit)
* (1.0 - priority)
* (1.0 - 0.01)
* (1.0 - 0.02);
assert!(
(advanced - expected).abs() < 0.00001,
"Expected ~{:.6}, got {:.6}",
expected,
advanced
);
let errors: Vec<_> = diags.iter().filter(|d| d.is_error()).collect();
assert!(
errors.is_empty(),
"advanced fixture should have no errors: {:?}",
errors
);
}
#[test]
fn test_inhibit_gate_requires_condition() {
use etdl_parser::ast::GateType;
let yaml = r#"
etdl: "1.0.0"
info:
title: "Inhibit Missing Condition"
version: "1.0.0"
domain: "Test"
asyncapi_imports: {}
eventTrees:
T:
initiatingEvent:
id: Trig
message: "api#/m"
next: C
nodes:
C:
type: consequence
operation: terminate
faultTrees:
F:
topEvent:
id: Top
description: "top"
rootCause: G
gates:
G:
type: INHIBIT
inputs: [A, B]
basicEvents:
A:
description: "a"
probability: 0.1
B:
description: "b"
probability: 0.2
"#;
let doc: EtlDocument = serde_yaml::from_str(yaml).unwrap();
let mut diags = Vec::new();
let registry = etdl_parser::asyncapi::AsyncApiRegistry::new();
etdl_compiler::validate::validate_document(&doc, ®istry, &mut diags);
assert!(
diags.iter().any(|d| d.code == "V-505"),
"INHIBIT without inhibitCondition should produce V-505, got {:?}",
diags
);
let ft = &doc.fault_trees.as_ref().unwrap()["F"];
assert_eq!(ft.gates.as_ref().unwrap()["G"].gate_type, GateType::Inhibit);
}
}