etdl-compiler 0.2.2

ETDL compiler: IEC 61025 fault tree resolution, MOCUS cut sets, ECEL type-checking, semantic validation, and code generation for event-driven microservices
Documentation
//! Integration test for reliability-aware compilation: an `.etdl` document
//! declares the reliability supplement and an external artifact; the compiler
//! resolves the artifact to deterministic probabilities that feed fault-tree
//! evaluation, and emits a build manifest.

use std::path::PathBuf;

fn fixture_dir() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("reliability_fixtures")
}

#[test]
fn external_artifact_resolves_into_fault_tree() {
    let base = fixture_dir();
    let doc = etdl_parser::parse_document_from_file(&base.join("external.etdl"))
        .expect("document parses");
    let registry = etdl_parser::load_asyncapi_imports(&doc, &base).expect("imports load");
    let compiler = etdl_compiler::Compiler::new();
    let result = compiler.compile_with_base(&doc, &registry, &base);

    let errors: Vec<_> = result.diagnostics.iter().filter(|d| d.is_error()).collect();
    assert!(
        errors.is_empty(),
        "expected no errors, got {:?}",
        errors
            .iter()
            .map(|d| (&d.code, &d.message))
            .collect::<Vec<_>>()
    );

    // The generated code should embed the resolved probabilities.
    let _rust = result.rust_output.expect("rust output present");
    let manifest = result
        .build_manifest
        .expect("build manifest present for reliability build");
    let resolved = manifest["resolved_probabilities"].as_array().unwrap();
    assert_eq!(resolved.len(), 2);

    // Verify the resolved values appear in the manifest with provenance.
    let values: Vec<f64> = resolved
        .iter()
        .map(|r| r["value"].as_f64().unwrap())
        .collect();
    assert!(values.contains(&0.0027));
    assert!(values.contains(&0.0012));

    // Fault-tree evaluation must use the resolved values: OR(0.0027, 0.0012).
    let mut diags = Vec::new();
    let probs = etdl_compiler::fault_tree::resolve_fault_trees_with_overrides(
        &doc,
        &etdl_compiler::fault_tree::BasicEventOverrides::from([
            ("GatewayTimeout".to_string(), 0.0027),
            ("GatewayUnreachable".to_string(), 0.0012),
        ]),
        &mut diags,
    );
    let expected = 1.0 - (1.0 - 0.0027) * (1.0 - 0.0012);
    let actual = probs["PaymentGatewayFailure"];
    assert!(
        (actual - expected).abs() < 1e-9,
        "expected {expected}, got {actual}"
    );
}

#[test]
fn backward_compat_no_supplement_unchanged() {
    // The worked example, with no supplement, must resolve exactly as before.
    let base = fixture_dir();
    let doc = etdl_parser::parse_document_from_file(&base.join("core-only.etdl")).expect("parses");
    let registry = etdl_parser::load_asyncapi_imports(&doc, &base).expect("imports load");
    let compiler = etdl_compiler::Compiler::new();
    let result = compiler.compile(&doc, &registry);

    let errors: Vec<_> = result.diagnostics.iter().filter(|d| d.is_error()).collect();
    assert!(errors.is_empty(), "core-only should validate: {:?}", errors);
    assert!(
        result.build_manifest.is_none(),
        "no reliability supplement => no manifest"
    );
    // Fault-tree probability unchanged: OR(0.008, 1-e^(-0.00021*24)).
    let mut diags = Vec::new();
    let probs = etdl_compiler::fault_tree::resolve_fault_trees(&doc, &mut diags);
    let expected = 1.0 - (1.0 - 0.008) * (1.0 - 0.005027);
    let actual = probs["PaymentGatewayFailure"];
    assert!(
        (actual - expected).abs() < 0.0001,
        "expected ~{expected}, got {actual}"
    );
}

#[test]
fn supplement_declared_but_optional_and_unsupported_warns() {
    let yaml = r#"
etdl: "1.0.0"
info:
  title: "T"
  version: "1.0.0"
  domain: "D"
asyncapi_imports: {}
supplements:
  - id: etdl.performance
    version: "1.0"
eventTrees:
  T:
    initiatingEvent: { id: I, message: "a#/m", next: C }
    nodes:
      C: { type: consequence, operation: terminate }
"#;
    let doc: etdl_parser::ast::EtlDocument = serde_yaml::from_str(yaml).unwrap();
    let registry = etdl_parser::asyncapi::AsyncApiRegistry::new();
    let compiler = etdl_compiler::Compiler::new();
    let result = compiler.validate(&doc, &registry);
    assert!(
        result.iter().any(|d| d.code == "W-407"),
        "optional unsupported supplement should warn W-407, got {:?}",
        result
    );
}

#[test]
fn golden_worked_example_probability_is_exact() {
    // The canonical worked example resolves the two Basic Events through their
    // OR gate to ~0.012987. This MUST be unchanged with no reliability
    // supplement involved (spec ยง13, Section 5.16).
    let base = fixture_dir();
    let doc = etdl_parser::parse_document_from_file(&base.join("core-only.etdl")).unwrap();
    let mut diags = Vec::new();
    let probs = etdl_compiler::fault_tree::resolve_fault_trees(&doc, &mut diags);
    let p = probs["PaymentGatewayFailure"];
    let expected = 1.0 - (1.0 - 0.008) * (1.0 - 0.005027);
    assert!(
        (p - expected).abs() < 0.0001,
        "worked example probability drifted: expected ~{expected}, got {p}"
    );
    // The generated constant format is 6 decimals -> 0.012987.
    assert!((p - 0.012987).abs() < 0.00001, "expected 0.012987, got {p}");
}

#[test]
fn required_unsupported_supplement_errors() {
    let yaml = r#"
etdl: "1.0.0"
info:
  title: "T"
  version: "1.0.0"
  domain: "D"
asyncapi_imports: {}
supplements:
  - id: etdl.safety
    version: "1.0"
    required: true
eventTrees:
  T:
    initiatingEvent: { id: I, message: "a#/m", next: C }
    nodes:
      C: { type: consequence, operation: terminate }
"#;
    let doc: etdl_parser::ast::EtlDocument = serde_yaml::from_str(yaml).unwrap();
    let registry = etdl_parser::asyncapi::AsyncApiRegistry::new();
    let compiler = etdl_compiler::Compiler::new();
    let result = compiler.validate(&doc, &registry);
    assert!(
        result.iter().any(|d| d.code == "E-108"),
        "required unsupported supplement should error E-108, got {:?}",
        result
    );
}