meta-ast 0.7.0

Polyglot static-analysis engine: extract symbols and cross-language dependency graphs from 9 supported source languages, with optional MetaCall deployment manifest generation.
Documentation
//! Integration tests for the datagraph pipeline.
//!
//! Validates GraphOutput serialization, sink adapters,
//! and the full pipeline with dataflow extraction on fixtures.

use std::path::Path;

use meta_ast::graph::GraphBuilder;
use meta_ast::model::SnapshotId;
use meta_ast::output::graph::{GraphOutput, SCHEMA_VERSION};

#[test]
fn datagraph_export_from_pipeline() {
    let root = Path::new("tests/fixtures/python");
    let files = meta_ast::input::discover_files(root, None).unwrap();
    assert!(!files.is_empty());

    let result = meta_ast::extractor::extract(&files);
    let mut diags = Vec::new();
    let (graph, _scc) = GraphBuilder::from_extractions(
        &result.files,
        root,
        SnapshotId::new(1).unwrap(),
        &mut diags,
    );

    let export = GraphOutput::from_graph(&graph, None, 1);
    assert_eq!(export.schema_version, SCHEMA_VERSION);
    assert!(
        export.metadata.node_count > 0,
        "datagraph should contain nodes"
    );
    assert!(
        export.metadata.edge_count > 0,
        "datagraph should contain ownership edges"
    );
    assert_eq!(export.nodes.len(), export.metadata.node_count);
    assert_eq!(export.edges.len(), export.metadata.edge_count);
}

#[test]
fn datagraph_export_file_node_fields() {
    let root = Path::new("tests/fixtures/python");
    let files = meta_ast::input::discover_files(root, None).unwrap();
    let result = meta_ast::extractor::extract(&files);
    let mut diags = Vec::new();
    let (graph, _scc) = GraphBuilder::from_extractions(
        &result.files,
        root,
        SnapshotId::new(1).unwrap(),
        &mut diags,
    );

    let export = GraphOutput::from_graph(&graph, None, 1);
    let file_nodes: Vec<_> = export.nodes.iter().filter(|n| n.kind == "file").collect();
    assert!(!file_nodes.is_empty(), "should have file nodes");
    for node in file_nodes {
        assert!(node.path.is_some(), "file node must have path");
        assert!(node.language.is_some(), "file node must have language");
        assert!(node.name.is_none(), "file node should not have name");
        assert!(
            node.data_scope.is_none(),
            "file node should not have data_scope"
        );
    }
}

#[test]
fn datagraph_export_symbol_node_fields() {
    let root = Path::new("tests/fixtures/python");
    let files = meta_ast::input::discover_files(root, None).unwrap();
    let result = meta_ast::extractor::extract(&files);
    let mut diags = Vec::new();
    let (graph, _scc) = GraphBuilder::from_extractions(
        &result.files,
        root,
        SnapshotId::new(1).unwrap(),
        &mut diags,
    );

    let export = GraphOutput::from_graph(&graph, None, 1);
    let symbol_nodes: Vec<_> = export.nodes.iter().filter(|n| n.kind == "symbol").collect();
    assert!(!symbol_nodes.is_empty(), "should have symbol nodes");
    for node in symbol_nodes {
        assert!(node.name.is_some(), "symbol node must have name");
        assert!(
            node.symbol_kind.is_some(),
            "symbol node must have symbol_kind"
        );
        assert!(node.file_path.is_some(), "symbol node must have file_path");
        assert!(
            node.source_range.is_some(),
            "symbol node must have source_range"
        );
        assert!(
            node.data_scope.is_none(),
            "symbol node should not have data_scope"
        );
        assert!(
            node.type_hint.is_none(),
            "symbol node should not have type_hint"
        );
    }
}

#[test]
fn datagraph_export_edges_have_required_fields() {
    let root = Path::new("tests/fixtures/python");
    let files = meta_ast::input::discover_files(root, None).unwrap();
    let result = meta_ast::extractor::extract(&files);
    let mut diags = Vec::new();
    let (graph, _scc) = GraphBuilder::from_extractions(
        &result.files,
        root,
        SnapshotId::new(1).unwrap(),
        &mut diags,
    );

    let export = GraphOutput::from_graph(&graph, None, 1);
    for edge in &export.edges {
        assert!(edge.source <= export.metadata.node_count);
        assert!(edge.target <= export.metadata.node_count);
        assert!(!edge.kind.is_empty(), "edge kind must not be empty");
        let valid_kinds = ["ownership", "import", "reference", "flow"];
        assert!(
            valid_kinds.contains(&edge.kind.as_str()),
            "unexpected edge kind: {}",
            edge.kind
        );
    }
}

#[test]
fn datagraph_export_json_roundtrip() {
    let root = Path::new("tests/fixtures/python");
    let files = meta_ast::input::discover_files(root, None).unwrap();
    let result = meta_ast::extractor::extract(&files);
    let mut diags = Vec::new();
    let (graph, _scc) = GraphBuilder::from_extractions(
        &result.files,
        root,
        SnapshotId::new(1).unwrap(),
        &mut diags,
    );

    let export = GraphOutput::from_graph(&graph, None, 1);
    let json = serde_json::to_string_pretty(&export).unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

    assert_eq!(parsed["schema_version"], 2);
    assert_eq!(parsed["metadata"]["snapshot_id"], 1);
    assert!(parsed["nodes"].is_array());
    assert!(parsed["edges"].is_array());
    assert_eq!(
        parsed["metadata"]["node_count"].as_u64().unwrap() as usize,
        export.metadata.node_count
    );
    assert_eq!(
        parsed["metadata"]["edge_count"].as_u64().unwrap() as usize,
        export.metadata.edge_count
    );
}

#[test]
fn snapshot_meta_has_schema_version() {
    let meta = meta_ast::pipeline::snapshot_meta(SnapshotId::new(42).unwrap());
    assert_eq!(meta.id, SnapshotId::new(42).unwrap());
    assert_eq!(meta.datagraph_schema_version, SCHEMA_VERSION);
}

#[cfg(feature = "dataflow")]
#[test]
fn dataflow_pipeline_extracts_on_python_fixtures() {
    let root = Path::new("tests/fixtures/python");
    let files = meta_ast::input::discover_files(root, None).unwrap();
    assert!(!files.is_empty());

    let result = meta_ast::extractor::extract(&files);
    let mut total_nodes = 0usize;
    let mut total_edges = 0usize;
    for file in &result.files {
        total_nodes += file.data_nodes.len();
        total_edges += file.flow_edges.len();
    }
    assert!(total_nodes > 0, "Python fixtures should extract DataNodes");
    assert!(total_edges > 0, "Python fixtures should extract FlowEdges");
}

#[cfg(feature = "dataflow")]
#[test]
fn dataflow_pipeline_extracts_on_javascript_fixtures() {
    let root = Path::new("tests/fixtures/javascript");
    let files = meta_ast::input::discover_files(root, None).unwrap();
    assert!(!files.is_empty());

    let result = meta_ast::extractor::extract(&files);
    let mut total_nodes = 0usize;
    let mut total_edges = 0usize;
    for file in &result.files {
        total_nodes += file.data_nodes.len();
        total_edges += file.flow_edges.len();
    }
    assert!(
        total_nodes > 0,
        "JavaScript fixtures should extract DataNodes"
    );
    assert!(
        total_edges > 0,
        "JavaScript fixtures should extract FlowEdges"
    );
}

#[cfg(feature = "dataflow")]
#[test]
fn dataflow_pipeline_extracts_on_typescript_fixtures() {
    let root = Path::new("tests/fixtures/typescript");
    let files = meta_ast::input::discover_files(root, None).unwrap();
    assert!(!files.is_empty());

    let result = meta_ast::extractor::extract(&files);
    let mut total_nodes = 0usize;
    let mut total_edges = 0usize;
    for file in &result.files {
        total_nodes += file.data_nodes.len();
        total_edges += file.flow_edges.len();
    }
    assert!(
        total_nodes > 0,
        "TypeScript fixtures should extract DataNodes"
    );
    assert!(
        total_edges > 0,
        "TypeScript fixtures should extract FlowEdges"
    );
}

#[cfg(feature = "dataflow")]
#[test]
fn dataflow_pipeline_extracts_on_tsx_fixtures() {
    let root = Path::new("tests/fixtures/tsx");
    let files = meta_ast::input::discover_files(root, None).unwrap();
    assert!(!files.is_empty());

    let result = meta_ast::extractor::extract(&files);
    let mut total_nodes = 0usize;
    let mut total_edges = 0usize;
    for file in &result.files {
        total_nodes += file.data_nodes.len();
        total_edges += file.flow_edges.len();
    }
    assert!(total_nodes > 0, "TSX fixtures should extract DataNodes");
    assert!(total_edges > 0, "TSX fixtures should extract FlowEdges");
}

#[cfg(feature = "dataflow")]
#[test]
fn sink_json_writes_datagraph_to_file() {
    use meta_ast::sink::GraphSink;
    let root = Path::new("tests/fixtures/python");
    let files = meta_ast::input::discover_files(root, None).unwrap();
    let result = meta_ast::extractor::extract(&files);
    let mut diags = Vec::new();
    let (graph, _scc) = GraphBuilder::from_extractions(
        &result.files,
        root,
        SnapshotId::new(1).unwrap(),
        &mut diags,
    );
    let export = GraphOutput::from_graph(&graph, None, 1);

    let temp = std::env::temp_dir().join("meta_ast_dg_integration.json");
    let sink = meta_ast::sink::JsonSink::new(Some(temp.clone()));
    sink.emit(&export).unwrap();

    let content = std::fs::read_to_string(&temp).unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
    assert_eq!(parsed["schema_version"], 2);
    assert!(parsed["nodes"].is_array());
    assert!(parsed["edges"].is_array());

    let has_data_node = parsed["nodes"]
        .as_array()
        .unwrap()
        .iter()
        .any(|n| n["kind"] == "data");
    assert!(
        has_data_node,
        "exported datagraph should contain data nodes"
    );
    let _ = std::fs::remove_file(&temp);
}

/// The datagraph export must not reuse the graph output path.
#[cfg(feature = "dataflow")]
#[test]
fn datagraph_has_its_own_output_path() {
    let run = |datagraph_output: Option<&std::path::Path>, graph_output: &std::path::Path| {
        let mut command = std::process::Command::new(env!("CARGO_BIN_EXE_meta-ast"));
        command
            .arg("graph")
            .arg("tests/fixtures/python")
            .arg("--datagraph")
            .arg("-o")
            .arg(graph_output)
            .arg("-f")
            .arg("json")
            .current_dir(env!("CARGO_MANIFEST_DIR"));
        if let Some(path) = datagraph_output {
            command.arg("--datagraph-output").arg(path);
        }
        command.output().unwrap()
    };

    let temp = tempfile::tempdir().unwrap();

    // Without an explicit path the datagraph derives one from the graph
    // output, so the graph output is never the file that the datagraph wrote.
    let graph_path = temp.path().join("graph.json");
    let output = run(None, &graph_path);
    assert!(
        output.status.success(),
        "graph run failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(graph_path.is_file(), "the graph output is written");
    assert!(
        temp.path().join("graph.datagraph.json").is_file(),
        "the datagraph is written beside the graph output"
    );

    // An explicit datagraph path is used as given.
    let explicit = temp.path().join("dg.json");
    let second_graph = temp.path().join("graph2.json");
    let output = run(Some(&explicit), &second_graph);
    assert!(
        output.status.success(),
        "graph run failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(explicit.is_file(), "the explicit datagraph path is written");
    assert!(second_graph.is_file(), "the graph output is written");
}