use std::collections::{BTreeMap, BTreeSet};
use crate::imports::{normalize_import_target, parse_imports};
fn build_import_graph(files: &[(&str, &str, &[&str])]) -> BTreeMap<String, BTreeSet<String>> {
let mut graph: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for &(file, lang, lines) in files {
let raw = parse_imports(lang, lines);
let targets: BTreeSet<String> = raw.iter().map(|t| normalize_import_target(t)).collect();
graph.insert(file.to_string(), targets);
}
graph
}
#[test]
fn scenario_linear_chain_a_imports_b_imports_c() {
let files: Vec<(&str, &str, &[&str])> = vec![
("a.rs", "rust", &["use b::Thing;"]),
("b.rs", "rust", &["use c::Other;"]),
("c.rs", "rust", &[]),
];
let graph = build_import_graph(&files);
assert!(graph["a.rs"].contains("b"));
assert!(graph["b.rs"].contains("c"));
assert!(graph["c.rs"].is_empty());
}
#[test]
fn scenario_circular_dependency_a_and_b_import_each_other() {
let files: Vec<(&str, &str, &[&str])> = vec![
("a.py", "python", &["import b"]),
("b.py", "python", &["import a"]),
];
let graph = build_import_graph(&files);
assert!(graph["a.py"].contains("b"));
assert!(graph["b.py"].contains("a"));
}
#[test]
fn scenario_three_way_circular_dependency() {
let files: Vec<(&str, &str, &[&str])> = vec![
("a.py", "python", &["import b"]),
("b.py", "python", &["import c"]),
("c.py", "python", &["import a"]),
];
let graph = build_import_graph(&files);
assert!(graph["a.py"].contains("b"));
assert!(graph["b.py"].contains("c"));
assert!(graph["c.py"].contains("a"));
}
#[test]
fn scenario_self_import_via_relative_normalizes_to_local() {
let files: Vec<(&str, &str, &[&str])> = vec![(
"index.js",
"javascript",
&[r#"import self from "./index";"#],
)];
let graph = build_import_graph(&files);
assert!(graph["index.js"].contains("local"));
}
#[test]
fn scenario_rust_self_import_uses_crate_keyword() {
let files: Vec<(&str, &str, &[&str])> = vec![(
"lib.rs",
"rust",
&["use crate::models::User;", "use crate::config::Settings;"],
)];
let graph = build_import_graph(&files);
assert_eq!(graph["lib.rs"].len(), 1);
assert!(graph["lib.rs"].contains("crate"));
}
#[test]
fn scenario_diamond_dependency_pattern() {
let files: Vec<(&str, &str, &[&str])> = vec![
("a.py", "python", &["import b", "import c"]),
("b.py", "python", &["import d"]),
("c.py", "python", &["import d"]),
("d.py", "python", &[]),
];
let graph = build_import_graph(&files);
assert_eq!(graph["a.py"].len(), 2);
assert!(graph["a.py"].contains("b"));
assert!(graph["a.py"].contains("c"));
assert!(graph["b.py"].contains("d"));
assert!(graph["c.py"].contains("d"));
assert!(graph["d.py"].is_empty());
}
#[test]
fn scenario_hub_module_imports_many_spokes() {
let lines: Vec<String> = (0..20).map(|i| format!("use spoke_{i}::Api;")).collect();
let refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
let files: Vec<(&str, &str, &[&str])> = vec![("hub.rs", "rust", &refs)];
let graph = build_import_graph(&files);
assert_eq!(graph["hub.rs"].len(), 20);
for i in 0..20 {
assert!(graph["hub.rs"].contains(&format!("spoke_{i}")));
}
}
#[test]
fn scenario_polyglot_monorepo_graph() {
let files: Vec<(&str, &str, &[&str])> = vec![
(
"api/main.go",
"go",
&[r#"import "fmt""#, r#"import "net/http""#],
),
(
"web/app.js",
"javascript",
&[r#"import React from "react";"#],
),
("ml/train.py", "python", &["import numpy", "import pandas"]),
(
"core/lib.rs",
"rust",
&["use serde::Serialize;", "mod config;"],
),
];
let graph = build_import_graph(&files);
assert_eq!(graph.len(), 4);
assert!(graph["api/main.go"].contains("fmt"));
assert!(graph["web/app.js"].contains("react"));
assert!(graph["ml/train.py"].contains("numpy"));
assert!(graph["core/lib.rs"].contains("serde"));
}
#[test]
fn scenario_multiple_imports_from_same_root_deduplicate_in_graph() {
let files: Vec<(&str, &str, &[&str])> = vec![(
"lib.rs",
"rust",
&[
"use serde::Serialize;",
"use serde::Deserialize;",
"use serde_json::Value;",
"use serde_json::Map;",
],
)];
let graph = build_import_graph(&files);
assert_eq!(graph["lib.rs"].len(), 2);
assert!(graph["lib.rs"].contains("serde"));
assert!(graph["lib.rs"].contains("serde_json"));
}
#[test]
fn scenario_all_relative_js_imports_collapse_to_local() {
let files: Vec<(&str, &str, &[&str])> = vec![(
"app.js",
"javascript",
&[
r#"import utils from "./utils";"#,
r#"import config from "../config";"#,
r#"import db from "./db/client";"#,
],
)];
let graph = build_import_graph(&files);
assert_eq!(graph["app.js"].len(), 1);
assert!(graph["app.js"].contains("local"));
}
#[test]
fn scenario_leaf_files_have_empty_dependency_sets() {
let files: Vec<(&str, &str, &[&str])> = vec![
("leaf.rs", "rust", &["fn main() {}", "let x = 42;"]),
("leaf.py", "python", &["x = 1", "print(x)"]),
("leaf.go", "go", &["func main() {}", "fmt.Println()"]),
(
"leaf.js",
"javascript",
&["const x = 1;", "console.log(x);"],
),
];
let graph = build_import_graph(&files);
for (file, deps) in &graph {
assert!(deps.is_empty(), "{file} should have no imports");
}
}
#[test]
fn scenario_deeply_nested_go_imports_normalize_to_domain_root() {
let files: Vec<(&str, &str, &[&str])> = vec![(
"main.go",
"go",
&[
"import (",
r#""github.com/org/repo/internal/pkg/subpkg/v2""#,
r#""gitlab.com/team/project/cmd/server""#,
r#""fmt""#,
")",
],
)];
let graph = build_import_graph(&files);
let deps = &graph["main.go"];
assert!(deps.contains("github"));
assert!(deps.contains("gitlab"));
assert!(deps.contains("fmt"));
}
#[test]
fn scenario_graph_has_one_node_per_file() {
let files: Vec<(&str, &str, &[&str])> = vec![
("a.rs", "rust", &["use std::io;"]),
("b.py", "python", &["import os"]),
("c.js", "javascript", &[r#"import x from "y";"#]),
("d.go", "go", &[r#"import "fmt""#]),
];
let graph = build_import_graph(&files);
assert_eq!(graph.len(), 4);
}
#[test]
fn scenario_isolated_files_produce_graph_with_all_empty_sets() {
let files: Vec<(&str, &str, &[&str])> = vec![
("orphan1.rs", "rust", &["// no imports here"]),
("orphan2.py", "python", &["x = 42"]),
];
let graph = build_import_graph(&files);
assert!(graph["orphan1.rs"].is_empty());
assert!(graph["orphan2.py"].is_empty());
}
#[test]
fn scenario_star_topology_many_import_one() {
let files: Vec<(&str, &str, &[&str])> = vec![
("a.py", "python", &["import hub"]),
("b.py", "python", &["import hub"]),
("c.py", "python", &["import hub"]),
("hub.py", "python", &[]),
];
let graph = build_import_graph(&files);
for name in &["a.py", "b.py", "c.py"] {
assert!(graph[*name].contains("hub"));
}
assert!(graph["hub.py"].is_empty());
}