// Hand-maintained: NOT generated by mmtorust.
//
// Rust port of `OMCompiler/Compiler/Util/TaskGraphResults.mo`, whose bodies
// are all `external "C"` wrappers over
// `OMCompiler/Compiler/runtime/TaskGraphResultsCmp.cpp`. mmtorust can only
// emit `todo!()` for `external "C"` bodies, so this module is hand-written
// and `TaskGraphResults` is listed in `HANDWRITTEN_TOP_PACKAGES`
// (mmtorust/src/codegen.rs) to skip its codegen.
//
// The hpcom testsuite checks the task graph the backend dumped (GraphML via
// `GraphML.mo`) against a reference GraphML file (`checkTaskGraph`) or
// against `// TG_NODE:`/`// TG_DEPENDENCY:` comments in generated code
// (`checkCodeGraph`). The comparison is structural: same node multiset (by
// name resp. id), same edge multiset (by source/target), every node carrying
// a calculation time and every edge a communication time (GraphML vs GraphML
// only). All diagnostic strings are kept byte-identical to the C++ since
// they appear in expected test output.

/*
 * This file is part of OpenModelica.
 *
 * Copyright (c) 1998-2026, Open Source Modelica Consortium (OSMC),
 * c/o Linköpings universitet, Department of Computer and Information Science,
 * SE-58183 Linköping, Sweden.
 *
 * All rights reserved.
 *
 * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR
 * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8.
 * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES
 * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL
 * VERSION 3, ACCORDING TO RECIPIENTS CHOICE.
 *
 * See the full OSMC Public License conditions for more details.
 *
 */
#![allow(non_snake_case)]

use std::fmt::Write as _;
use std::sync::Arc;

use anyhow::Result;
use arcstr::ArcStr;

use metamodelica::ext::{c_atof, c_atol};
use metamodelica::List;

/// One task node. `threadId`/`taskNumber`/`taskId` are parsed like upstream
/// but (also like upstream) never inspected by the comparison; only `name`
/// and `calcTime` matter. The C++ struct also has a `simCodeEqs` list that no
/// parser ever fills (it is only read by the commented-out
/// `CompareGraphsLevel`), so it is omitted here.
#[derive(Default)]
struct Node {
    id: String,
    name: String,
    calc_time: f64,
    #[allow(dead_code)]
    thread_id: String,
    #[allow(dead_code)]
    task_number: i64,
    #[allow(dead_code)]
    task_id: i64,
}

#[derive(Default)]
struct Edge {
    source_id: String,
    source_name: String,
    target_id: String,
    target_name: String,
    comm_time: f64,
}

#[derive(Default)]
struct Graph {
    /// Nodes in name order (the C++ GraphML parser collects nodes into a
    /// `std::set` ordered by name, then appends set order to the graph).
    nodes: Vec<Node>,
    edges: Vec<Edge>,
    critical_path_info: String,
}

/// The GraphML `<key>` declarations of interest: attr.name → the `id` used by
/// `<data key="...">` references. A later declaration of the same attr.name
/// overwrites an earlier one, like the C++ single-string registration slots.
#[derive(Default)]
struct KeyIds {
    comm_cost: String,
    calc_time: String,
    critical_path: String,
    name: String,
    thread_id: String,
    task_number: String,
    task_id: String,
}

/// Parse a task graph in GraphML form, mirroring
/// `GraphMLParser::ParseGraph`:
///
/// * `<key>` elements at depth 2 register attribute ids (see [`KeyIds`]);
/// * `<node>` elements at depth 3 with their `<data>` children at depth 4;
/// * `<edge>` elements at depth 3 (attributes `id`/`source`/`target`,
///   `<data>` child carrying the CommCost value);
/// * a `<data>` element at depth 3 carrying the graph's CriticalPath info.
///
/// Nodes are deduplicated *by name*; a duplicate is dropped with the same
/// warning the C++ emits. Element names are matched without their namespace
/// prefix (roxmltree's local names; the C++ strips everything up to `:`).
///
/// Divergences from the expat-based original, none of which can trigger on
/// the files written by `GraphML.mo` / yEd:
/// * an empty registered `<data>` element leaves the target field at its
///   default, while expat's dangling character-data flag would capture the
///   next stray text (typically inter-element whitespace);
/// * a `<node>` without an `id` attribute is skipped, where the C++ inserts
///   a stale/null pointer into its node set;
/// * a malformed document yields whatever a partial parse produced upstream —
///   roxmltree parses up front, so here it yields the empty graph (with the
///   same "Error during xml-parsing\n" message appended).
fn parse_graphml(file_name: &str, error_msg: &mut String) -> Graph {
    let mut graph = Graph::default();
    let Ok(content) = std::fs::read_to_string(file_name) else {
        // Unreadable-but-existing file; the C++ would crash on the null
        // FILE*. The caller has already checked existence.
        *error_msg += "Error during xml-parsing\n";
        return graph;
    };
    let doc = match roxmltree::Document::parse(&content) {
        Ok(doc) => doc,
        Err(_) => {
            *error_msg += "Error during xml-parsing\n";
            return graph;
        }
    };

    // Pass 1: register the <key> declarations (depth 2). The C++ registers
    // them in document order while streaming; key declarations precede the
    // <graph> element in valid GraphML, so collecting them up front is
    // equivalent.
    let mut keys = KeyIds::default();
    for key in doc
        .root_element()
        .children()
        .filter(|n| n.is_element() && n.tag_name().name() == "key")
    {
        let Some(attr_name) = key.attribute("attr.name") else { continue };
        let Some(id) = key.attribute("id") else { continue };
        match attr_name {
            "CommCost" => keys.comm_cost = id.to_string(),
            "CalcTime" => keys.calc_time = id.to_string(),
            "CriticalPath" => keys.critical_path = id.to_string(),
            "Name" => keys.name = id.to_string(),
            "ThreadId" => keys.thread_id = id.to_string(),
            "TaskNumber" => keys.task_number = id.to_string(),
            "TaskID" => keys.task_id = id.to_string(),
            _ => {}
        }
    }
    // An unregistered key id must not match `<data>` elements; the C++
    // compares against its (then empty) registration strings, which can only
    // match a `key=""` attribute. Treat empty registrations as "no match".
    let key_matches = |registered: &str, key_attr: &str| !registered.is_empty() && registered == key_attr;

    // Node set ordered (and deduplicated) by name, mirroring the
    // `std::set<Node*, CompareNodeNamesInt>` in ParseGraph. The first
    // insertion wins.
    let mut node_set: std::collections::BTreeMap<String, Node> = std::collections::BTreeMap::new();

    // Pass 2: walk <graph> children (depth 3) in document order.
    for level2 in doc.root_element().children().filter(|n| n.is_element()) {
        for elem in level2.children().filter(|n| n.is_element()) {
            match elem.tag_name().name() {
                "node" => {
                    let Some(id) = elem.attribute("id") else {
                        // C++ would leave currentNode null and insert that
                        // into the node set; never happens in real files.
                        continue;
                    };
                    let mut node = Node {
                        id: id.to_string(),
                        calc_time: -1.0,
                        task_number: -1,
                        task_id: -1,
                        ..Default::default()
                    };
                    for data in elem
                        .children()
                        .filter(|n| n.is_element() && n.tag_name().name() == "data")
                    {
                        let Some(key) = data.attribute("key") else { continue };
                        let text = data.text().unwrap_or("");
                        // Same first-match chain as the C++ DataElement
                        // dispatch (CommCost is edge-only: currentEdge is
                        // null while inside a node).
                        if key_matches(&keys.calc_time, key) {
                            node.calc_time = c_atof(text);
                        } else if key_matches(&keys.name, key) {
                            node.name = text.to_string();
                        } else if key_matches(&keys.thread_id, key) {
                            node.thread_id = text.to_string();
                        } else if key_matches(&keys.task_number, key) {
                            node.task_number = c_atol(text);
                        } else if key_matches(&keys.task_id, key) {
                            node.task_id = c_atol(text);
                        }
                    }
                    if node_set.contains_key(&node.name) {
                        let _ = writeln!(
                            error_msg,
                            "Warning: a node with the name '{}' was added multiple times to graph.",
                            node.name
                        );
                    } else {
                        node_set.insert(node.name.clone(), node);
                    }
                }
                "edge" => {
                    let mut edge = Edge { comm_time: -1.0, ..Default::default() };
                    if elem.attribute("id").is_none_or(str::is_empty) {
                        *error_msg += "Warning: edge without id defined\n";
                    }
                    match elem.attribute("source") {
                        Some(source) if !source.is_empty() => edge.source_id = source.to_string(),
                        _ => *error_msg += "Warning: edge without source defined\n",
                    }
                    match elem.attribute("target") {
                        Some(target) if !target.is_empty() => edge.target_id = target.to_string(),
                        _ => *error_msg += "Warning: edge without target defined\n",
                    }
                    for data in elem
                        .children()
                        .filter(|n| n.is_element() && n.tag_name().name() == "data")
                    {
                        let Some(key) = data.attribute("key") else { continue };
                        if key_matches(&keys.comm_cost, key) {
                            edge.comm_time = c_atof(data.text().unwrap_or(""));
                        }
                    }
                    graph.edges.push(edge);
                }
                "data" => {
                    if let Some(key) = elem.attribute("key")
                        && key_matches(&keys.critical_path, key)
                    {
                        graph.critical_path_info = elem.text().unwrap_or("").to_string();
                    }
                }
                _ => {}
            }
        }
    }
    graph.nodes = node_set.into_values().collect();
    graph
}

/// Parse the task graph encoded as comments in generated code, mirroring
/// `GraphCodeParser::ParseGraph`. Recognised lines (anchored, `[ \t]*`
/// leading whitespace, nothing after the match):
///
/// ```text
/// // TG_NODE: <digits>
/// // TG_NODE: <digits> TG_PARENTS: <digits>,<digits>,...   (list may be empty)
/// // TG_DEPENDENCY: <digits> -> <digits>
/// ```
///
/// Nodes become `Node<idx>` (id == name); each parent / dependency becomes an
/// edge `Node<parent> -> Node<idx>`. No deduplication and no times — the code
/// graph is compared with `checkCalcTime`/`checkCommTime` off.
fn parse_graphcode(file_name: &str, graph: &mut Graph) {
    let Ok(content) = std::fs::read_to_string(file_name) else {
        // The C++ reads through a default-constructed ifstream and yields an
        // empty graph; existence was already checked by the caller.
        return;
    };
    // `^[ \t]*//[ \t]*` prefix shared by all three patterns. Returns the rest
    // of the line after the comment marker and optional whitespace.
    fn comment_body(line: &str) -> Option<&str> {
        let rest = line.trim_start_matches([' ', '\t']);
        let rest = rest.strip_prefix("//")?;
        Some(rest.trim_start_matches([' ', '\t']))
    }
    // `[0-9]*` — possibly empty digit run (the C++ regexes use `*`, not `+`).
    fn split_digits(s: &str) -> (&str, &str) {
        let end = s.bytes().take_while(u8::is_ascii_digit).count();
        s.split_at(end)
    }

    for line in content.lines() {
        let Some(body) = comment_body(line) else { continue };
        if let Some(rest) = body.strip_prefix("TG_NODE: ") {
            let (node_idx, rest) = split_digits(rest);
            let node_id = format!("Node{node_idx}");
            if rest.is_empty() {
                // `// TG_NODE: <idx>`
                graph.nodes.push(Node { id: node_id.clone(), name: node_id, ..Default::default() });
            } else if let Some(parents) = rest.strip_prefix(" TG_PARENTS: ") {
                // `// TG_NODE: <idx> TG_PARENTS: <csv>` — csv entries are
                // `[0-9]*`, so empty entries are allowed and skipped (strtok
                // ignores empty tokens).
                if !parents.bytes().all(|b| b.is_ascii_digit() || b == b',') {
                    continue;
                }
                graph.nodes.push(Node { id: node_id.clone(), name: node_id.clone(), ..Default::default() });
                for parent_idx in parents.split(',').filter(|p| !p.is_empty()) {
                    graph.edges.push(Edge {
                        source_id: format!("Node{parent_idx}"),
                        source_name: format!("Node{parent_idx}"),
                        target_id: node_id.clone(),
                        target_name: node_id.clone(),
                        comm_time: 0.0,
                    });
                }
            }
        } else if let Some(rest) = body.strip_prefix("TG_DEPENDENCY: ") {
            // `// TG_DEPENDENCY: <parent> -> <node>`
            let (parent_idx, rest) = split_digits(rest);
            let Some(rest) = rest.strip_prefix(" -> ") else { continue };
            let (node_idx, rest) = split_digits(rest);
            if !rest.is_empty() {
                continue;
            }
            graph.edges.push(Edge {
                source_id: format!("Node{parent_idx}"),
                source_name: format!("Node{parent_idx}"),
                target_id: format!("Node{node_idx}"),
                target_name: format!("Node{node_idx}"),
                comm_time: 0.0,
            });
        }
    }
}

/// Node comparison key selector: by name (`checkTaskGraph`) or by id
/// (`checkCodeGraph`); edges compare by the concatenated source+target key
/// of their endpoints' names resp. ids.
#[derive(Clone, Copy)]
enum CompareBy {
    Name,
    Id,
}

impl CompareBy {
    fn node_key<'a>(&self, n: &'a Node) -> &'a str {
        match self {
            CompareBy::Name => &n.name,
            CompareBy::Id => &n.id,
        }
    }
    fn edge_key(&self, e: &Edge) -> String {
        match self {
            CompareBy::Name => format!("{}{}", e.source_name, e.target_name),
            CompareBy::Id => format!("{}{}", e.source_id, e.target_id),
        }
    }
}

/// Structural graph comparison, mirroring `GraphComparator::CompareGraphs`.
/// On the first difference a diagnostic is appended to `error_msg` and the
/// comparison reports failure. `g1` is the graph under test, `g2` the
/// reference.
fn compare_graphs(
    g1: &mut Graph,
    g2: &mut Graph,
    cmp: CompareBy,
    check_calc_time: bool,
    check_comm_time: bool,
    error_msg: &mut String,
) -> bool {
    // Critical path: only checked when the reference declares one (after
    // stripping trailing whitespace); the test value must match exactly.
    let g2_critical = g2.critical_path_info.trim_end_matches([' ', '\n', '\r', '\t']);
    if !g2_critical.is_empty() && g1.critical_path_info != g2_critical {
        let _ = write!(
            error_msg,
            "The first graph has the critical path '{}', but the second graph has '{}' as critical path",
            g1.critical_path_info, g2_critical
        );
        return false;
    }

    // Nodes: equal counts, then pairwise equal after sorting by the key.
    if g1.nodes.len() != g2.nodes.len() {
        let _ = write!(
            error_msg,
            "The first graph has {} nodes, but the second graph has {} nodes.",
            g1.nodes.len(),
            g2.nodes.len()
        );
        return false;
    }
    g1.nodes.sort_by(|a, b| cmp.node_key(a).cmp(cmp.node_key(b)));
    g2.nodes.sort_by(|a, b| cmp.node_key(a).cmp(cmp.node_key(b)));
    for (n1, n2) in g1.nodes.iter().zip(g2.nodes.iter()) {
        if cmp.node_key(n1) != cmp.node_key(n2) {
            // Report the direction of the mismatch like the C++ does.
            if !g2.nodes.iter().any(|n| cmp.node_key(n) == cmp.node_key(n1)) {
                let _ = write!(error_msg, "Node '{}(id: {})' is not part of the second graph.", n1.name, n1.id);
            } else {
                let _ = write!(error_msg, "Node '{}(id: {})' is not part of the first graph.", n2.name, n2.id);
            }
            return false;
        }
        if check_calc_time && n1.calc_time < 0.0 {
            let _ = write!(error_msg, "Node '{}' has no calculation time.", n1.name);
            return false;
        }
    }

    // Edges: equal counts, endpoint names resolved through the node ids,
    // then pairwise equal after sorting by the key.
    if g1.edges.len() != g2.edges.len() {
        let _ = write!(
            error_msg,
            "The first graph has {} edges, but the second graph has {} edges.",
            g1.edges.len(),
            g2.edges.len()
        );
        return false;
    }
    if !fill_edges_with_node_names(g1) || !fill_edges_with_node_names(g2) {
        // The C++ fails silently here (an edge references a missing node).
        return false;
    }
    g1.edges.sort_by_key(|e| cmp.edge_key(e));
    g2.edges.sort_by_key(|e| cmp.edge_key(e));
    for (e1, e2) in g1.edges.iter().zip(g2.edges.iter()) {
        if cmp.edge_key(e1) != cmp.edge_key(e2) {
            if !g2.edges.iter().any(|e| cmp.edge_key(e) == cmp.edge_key(e1)) {
                let _ = write!(
                    error_msg,
                    "Edge '{}(id: {}) --> {}(id: {})' is not part of the second graph.",
                    e1.source_name, e1.source_id, e1.target_name, e1.target_id
                );
            } else {
                let _ = write!(
                    error_msg,
                    "Edge '{}(id: {}) --> {}(id: {})' is not part of the first graph.",
                    e2.source_name, e2.source_id, e2.target_name, e2.target_id
                );
            }
            return false;
        }
        if check_comm_time && e1.comm_time < 0.0 {
            let _ = write!(
                error_msg,
                "Edge '{} --> {}' has no communication time.",
                e1.source_name, e1.target_name
            );
            return false;
        }
    }
    true
}

/// Resolve each edge's source/target node names through the graph's node ids
/// (`GraphComparator::FillEdgesWithNodeNames`). False when an edge references
/// a node id that is not in the graph.
fn fill_edges_with_node_names(graph: &mut Graph) -> bool {
    let id_to_name: std::collections::HashMap<String, String> =
        graph.nodes.iter().map(|n| (n.id.clone(), n.name.clone())).collect();
    for edge in &mut graph.edges {
        let (Some(source), Some(target)) =
            (id_to_name.get(&edge.source_id), id_to_name.get(&edge.target_id))
        else {
            return false;
        };
        edge.source_name = source.clone();
        edge.target_name = target.clone();
    }
    true
}

/// Build the result list `{[errorMsg,] verdict}` the way the C++ entry
/// points do: the verdict string first (cons last), the accumulated
/// diagnostics prepended when non-empty.
fn result_list(error_msg: String, verdict: &'static str) -> Arc<List<ArcStr>> {
    let mut res = metamodelica::cons(ArcStr::from(verdict), metamodelica::nil());
    if !error_msg.is_empty() {
        res = metamodelica::cons(ArcStr::from(error_msg), res);
    }
    res
}

/// `TaskGraphResults_checkTaskGraph(filename, reffilename)`: compare the
/// dumped task graph against a reference GraphML file by node names, with
/// calc/comm-time presence checks.
pub fn checkTaskGraph(filename: ArcStr, reffilename: ArcStr) -> Result<Arc<List<ArcStr>>> {
    for f in [&filename, &reffilename] {
        if !std::path::Path::new(f.as_str()).exists() {
            return Ok(metamodelica::cons(
                format!("File '{f}' does not exist").into(),
                metamodelica::nil(),
            ));
        }
    }
    let mut error_msg = String::new();
    let mut g1 = parse_graphml(filename.as_str(), &mut error_msg);
    let mut g2 = parse_graphml(reffilename.as_str(), &mut error_msg);
    let ok = compare_graphs(&mut g1, &mut g2, CompareBy::Name, true, true, &mut error_msg);
    Ok(result_list(error_msg, if ok { "Taskgraph correct" } else { "Taskgraph not correct" }))
}

/// `TaskGraphResults_checkCodeGraph(graphfile, codefile)`: compare the dumped
/// task graph against the TG_NODE/TG_DEPENDENCY comments in generated code,
/// by node ids and without time checks.
pub fn checkCodeGraph(graphfile: ArcStr, codefile: ArcStr) -> Result<Arc<List<ArcStr>>> {
    for f in [&graphfile, &codefile] {
        if !std::path::Path::new(f.as_str()).exists() {
            return Ok(metamodelica::cons(
                format!("File '{f}' does not exist").into(),
                metamodelica::nil(),
            ));
        }
    }
    let mut error_msg = String::new();
    let mut g1 = parse_graphml(graphfile.as_str(), &mut error_msg);
    let mut g2 = Graph::default();
    parse_graphcode(codefile.as_str(), &mut g2);
    let ok = compare_graphs(&mut g1, &mut g2, CompareBy::Id, false, false, &mut error_msg);
    Ok(result_list(error_msg, if ok { "Codegraph correct" } else { "Codegraph not correct" }))
}

#[cfg(test)]
mod tests {
    use super::*;

    const GRAPHML: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns">
  <key attr.name="CriticalPath" attr.type="string" for="graph" id="d25"/>
  <key attr.name="CommCost" attr.type="int" for="edge" id="d24"/>
  <key attr.name="CalcTime" attr.type="double" for="node" id="d21"/>
  <key attr.name="Name" attr.type="string" for="node" id="d20"/>
  <graph edgedefault="directed" id="G">
    <data key="d25">cp-info</data>
    <node id="n1">
      <data key="d20">eq1</data>
      <data key="d21">0.5</data>
    </node>
    <node id="n2">
      <data key="d20">eq2</data>
      <data key="d21">1.5</data>
    </node>
    <edge id="e1" source="n1" target="n2">
      <data key="d24">7</data>
    </edge>
  </graph>
</graphml>"#;

    fn write_temp(name: &str, content: &str) -> String {
        let dir = std::env::temp_dir().join("taskgraphresults_test");
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join(name);
        std::fs::write(&path, content).unwrap();
        path.to_str().unwrap().to_string()
    }

    fn to_vec(lst: Arc<List<ArcStr>>) -> Vec<String> {
        (&*lst).into_iter().map(|s| s.to_string()).collect()
    }

    #[test]
    fn identical_graphs_are_correct() {
        let f1 = write_temp("a.graphml", GRAPHML);
        let f2 = write_temp("b.graphml", GRAPHML);
        let res = to_vec(checkTaskGraph(f1.into(), f2.into()).unwrap());
        assert_eq!(res, vec!["Taskgraph correct"]);
    }

    #[test]
    fn node_renamed_is_detected() {
        let f1 = write_temp("c.graphml", GRAPHML);
        let f2 = write_temp("d.graphml", &GRAPHML.replace("eq2", "eq3"));
        let res = to_vec(checkTaskGraph(f1.into(), f2.into()).unwrap());
        assert_eq!(
            res,
            vec![
                "Node 'eq2(id: n2)' is not part of the second graph.",
                "Taskgraph not correct"
            ]
        );
    }

    #[test]
    fn missing_calc_time_is_detected() {
        // Drop eq1's CalcTime <data> from the *first* graph.
        let without = GRAPHML.replace("      <data key=\"d21\">0.5</data>\n", "");
        let f1 = write_temp("e.graphml", &without);
        let f2 = write_temp("f.graphml", GRAPHML);
        let res = to_vec(checkTaskGraph(f1.into(), f2.into()).unwrap());
        assert_eq!(
            res,
            vec!["Node 'eq1' has no calculation time.", "Taskgraph not correct"]
        );
    }

    #[test]
    fn critical_path_mismatch_is_detected() {
        let f1 = write_temp("g.graphml", &GRAPHML.replace("cp-info", "other"));
        let f2 = write_temp("h.graphml", GRAPHML);
        let res = to_vec(checkTaskGraph(f1.into(), f2.into()).unwrap());
        assert_eq!(
            res,
            vec![
                "The first graph has the critical path 'other', but the second graph has 'cp-info' as critical path",
                "Taskgraph not correct"
            ]
        );
    }

    #[test]
    fn missing_file_is_reported() {
        let f1 = write_temp("i.graphml", GRAPHML);
        let res = to_vec(checkTaskGraph(f1.into(), "/nonexistent/x.graphml".into()).unwrap());
        assert_eq!(res, vec!["File '/nonexistent/x.graphml' does not exist"]);
    }

    #[test]
    fn code_graph_comments_match_graphml_ids() {
        // checkCodeGraph compares by *ids*, so the GraphML node ids must be
        // Node<idx> to match the comment graph.
        let graphml = GRAPHML.replace("\"n1\"", "\"Node1\"").replace("\"n2\"", "\"Node2\"");
        let f1 = write_temp("j.graphml", &graphml);
        let code = "int x; // unrelated\n// TG_NODE: 1 TG_PARENTS: \n  // TG_NODE: 2 TG_PARENTS: 1\n";
        let f2 = write_temp("j.c", code);
        let res = to_vec(checkCodeGraph(f1.into(), f2.into()).unwrap());
        assert_eq!(res, vec!["Codegraph correct"]);
    }

    #[test]
    fn code_graph_dependency_lines() {
        let graphml = GRAPHML.replace("\"n1\"", "\"Node1\"").replace("\"n2\"", "\"Node2\"");
        let f1 = write_temp("k.graphml", &graphml);
        let code = "// TG_NODE: 1\n// TG_NODE: 2\n// TG_DEPENDENCY: 1 -> 2\n";
        let f2 = write_temp("k.c", code);
        let res = to_vec(checkCodeGraph(f1.into(), f2.into()).unwrap());
        assert_eq!(res, vec!["Codegraph correct"]);
    }
}
