rulemorph 0.3.4

YAML-based declarative data transformation engine for CSV/JSON to JSON
Documentation
use super::*;
use serde_json::json;

#[test]
fn collector_assigns_parent_ids_from_span_stack() {
    let mut collector = TraceCollector::new(TransformTraceOptions::metadata_only());
    collector.start_record(0, &json!({"name":"alice"}));

    collector
        .start_span(TraceEventKind::MappingStart, TracePhase::Start)
        .rule_path("mappings[0]")
        .finish(&mut collector);
    collector
        .start_span(TraceEventKind::OpStart, TracePhase::Start)
        .operator("uppercase")
        .finish(&mut collector);
    collector
        .end_span(TraceEventKind::OpEnd, TracePhase::End)
        .operator("uppercase")
        .finish(&mut collector);
    collector
        .end_span(TraceEventKind::MappingEnd, TracePhase::End)
        .rule_path("mappings[0]")
        .finish(&mut collector);

    let trace = collector.finish();
    let events = &trace.records[0].events;
    let mapping_start = events
        .iter()
        .find(|event| event.kind == TraceEventKind::MappingStart)
        .unwrap();
    let op_start = events
        .iter()
        .find(|event| event.kind == TraceEventKind::OpStart)
        .unwrap();
    let op_end = events
        .iter()
        .find(|event| event.kind == TraceEventKind::OpEnd)
        .unwrap();
    let mapping_end = events
        .iter()
        .find(|event| event.kind == TraceEventKind::MappingEnd)
        .unwrap();
    assert_eq!(op_start.parent_id, Some(mapping_start.id));
    assert_eq!(op_end.parent_id, Some(op_start.id));
    assert_eq!(mapping_end.parent_id, Some(mapping_start.id));
}

#[test]
fn collector_freezes_before_unemitted_span_enters_stack() {
    let mut options = TransformTraceOptions::metadata_only();
    options.max_events = Some(2);
    let mut collector = TraceCollector::new(options);
    collector.start_record(0, &json!({"name":"alice"}));

    assert!(
        collector
            .start_span(TraceEventKind::MappingStart, TracePhase::Start)
            .rule_path("mappings[0]")
            .finish(&mut collector)
    );
    assert!(
        !collector
            .start_span(TraceEventKind::OpStart, TracePhase::Start)
            .operator("uppercase")
            .finish(&mut collector)
    );
    collector
        .emit(TraceEventKind::SourceRead, TracePhase::Instant)
        .input_path("@input.name")
        .finish(&mut collector);

    let trace = collector.finish();
    assert!(!trace.complete);
    let ids = trace.records[0]
        .events
        .iter()
        .map(|event| event.id)
        .collect::<std::collections::BTreeSet<_>>();
    for event in &trace.records[0].events {
        if let Some(parent_id) = event.parent_id {
            assert!(
                ids.contains(&parent_id),
                "parent_id points to a non-emitted event"
            );
        }
    }
}

#[test]
fn collector_error_span_records_error_and_closes_span() {
    let mut collector = TraceCollector::new(TransformTraceOptions::metadata_only());
    collector.start_record(0, &json!({"name":"alice"}));

    collector
        .start_span(TraceEventKind::OpStart, TracePhase::Start)
        .operator("uppercase")
        .finish(&mut collector);
    collector
        .error_span(TraceEventKind::OpError, "OP_ERROR", "operator failed")
        .operator("uppercase")
        .finish(&mut collector);
    collector
        .emit(TraceEventKind::SourceRead, TracePhase::Instant)
        .input_path("@input.name")
        .finish(&mut collector);

    let trace = collector.finish();
    let events = &trace.records[0].events;
    let op_start = events
        .iter()
        .find(|event| event.kind == TraceEventKind::OpStart)
        .unwrap();
    let op_error = events
        .iter()
        .find(|event| event.kind == TraceEventKind::OpError)
        .unwrap();
    let source_read = events
        .iter()
        .find(|event| event.kind == TraceEventKind::SourceRead)
        .unwrap();
    assert_eq!(op_error.parent_id, Some(op_start.id));
    assert_eq!(source_read.parent_id, None);
}

#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "complete trace finished with open spans")]
fn collector_finish_rejects_open_span_after_snapshot_only_truncation() {
    let mut options = TransformTraceOptions::raw();
    options.max_snapshot_bytes = Some(1);
    let mut collector = TraceCollector::new(options);
    collector.start_record(0, &json!({"name":"alice"}));
    collector
        .start_span(TraceEventKind::MappingStart, TracePhase::Start)
        .finish_with_output(&mut collector, &json!("oversized"), None);
    let _ = collector.finish();
}

#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "start_record called while a span is open")]
fn collector_start_record_requires_empty_span_stack() {
    let mut collector = TraceCollector::new(TransformTraceOptions::metadata_only());
    collector.start_record(0, &json!({"name":"alice"}));
    collector
        .start_span(TraceEventKind::MappingStart, TracePhase::Start)
        .finish(&mut collector);
    collector.start_record(1, &json!({"name":"bob"}));
}

#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "start_finalize called while a span is open")]
fn collector_start_finalize_requires_empty_span_stack() {
    let mut collector = TraceCollector::new(TransformTraceOptions::metadata_only());
    collector.start_record(0, &json!({"name":"alice"}));
    collector
        .start_span(TraceEventKind::MappingStart, TracePhase::Start)
        .finish(&mut collector);
    collector.start_finalize(&json!([]));
}