use std::path::{Path, PathBuf};
use tempfile::TempDir;
use thread_flow::incremental::dependency_builder::{DependencyGraphBuilder, Language};
use thread_flow::incremental::extractors::LanguageDetector;
use thread_flow::incremental::storage::InMemoryStorage;
fn setup_temp_dir() -> TempDir {
tempfile::tempdir().expect("create temp dir")
}
fn create_rust_test_file(dir: &Path, name: &str, imports: &[&str]) -> PathBuf {
let path = dir.join(format!("{}.rs", name));
let mut content = String::new();
for import in imports {
content.push_str(&format!("use {};\n", import));
}
content.push_str("\nfn main() {}\n");
std::fs::write(&path, content).expect("write rust file");
path
}
fn create_typescript_test_file(dir: &Path, name: &str, imports: &[&str]) -> PathBuf {
let path = dir.join(format!("{}.ts", name));
let mut content = String::new();
for import in imports {
content.push_str(&format!("import {{ thing }} from '{}';\n", import));
}
content.push_str("\nexport function main() {}\n");
std::fs::write(&path, content).expect("write typescript file");
path
}
fn create_python_test_file(dir: &Path, name: &str, imports: &[&str]) -> PathBuf {
let path = dir.join(format!("{}.py", name));
let mut content = String::new();
for import in imports {
content.push_str(&format!("import {}\n", import));
}
content.push_str("\ndef main():\n pass\n");
std::fs::write(&path, content).expect("write python file");
path
}
fn create_go_test_file(dir: &Path, name: &str, imports: &[&str]) -> PathBuf {
let path = dir.join(format!("{}.go", name));
let mut content = String::from("package main\n\nimport (\n");
for import in imports {
content.push_str(&format!(" \"{}\"\n", import));
}
content.push_str(")\n\nfunc main() {}\n");
std::fs::write(&path, content).expect("write go file");
path
}
#[tokio::test]
async fn test_rust_file_extraction() {
let temp_dir = setup_temp_dir();
let rust_file = create_rust_test_file(
temp_dir.path(),
"main",
&["std::collections::HashMap", "crate::utils::config"],
);
let storage = Box::new(InMemoryStorage::new());
let mut builder = DependencyGraphBuilder::new(storage);
builder
.extract_file(&rust_file)
.await
.expect("extract rust file");
let graph = builder.graph();
assert!(
graph.edge_count() >= 1,
"Expected at least 1 edge for local crate import (stdlib import filtered)"
);
assert!(graph.contains_node(&rust_file));
}
#[tokio::test]
async fn test_typescript_file_extraction() {
let temp_dir = setup_temp_dir();
let ts_file = create_typescript_test_file(
temp_dir.path(),
"app",
&["./utils/config", "./components/Button"],
);
let storage = Box::new(InMemoryStorage::new());
let mut builder = DependencyGraphBuilder::new(storage);
builder
.extract_file(&ts_file)
.await
.expect("extract typescript file");
let graph = builder.graph();
assert!(
graph.edge_count() >= 2,
"Expected at least 2 edges for 2 imports"
);
assert!(graph.contains_node(&ts_file));
}
#[tokio::test]
async fn test_python_file_extraction() {
let temp_dir = setup_temp_dir();
let py_file = create_python_test_file(temp_dir.path(), "main", &["os", "sys", "json"]);
let storage = Box::new(InMemoryStorage::new());
let mut builder = DependencyGraphBuilder::new(storage);
builder
.extract_file(&py_file)
.await
.expect("extract python file");
let graph = builder.graph();
assert!(
graph.edge_count() >= 3,
"Expected at least 3 edges for 3 imports"
);
assert!(graph.contains_node(&py_file));
}
#[tokio::test]
async fn test_go_file_extraction() {
let temp_dir = setup_temp_dir();
let go_file = create_go_test_file(temp_dir.path(), "main", &["fmt", "os", "strings"]);
let storage = Box::new(InMemoryStorage::new());
let mut builder = DependencyGraphBuilder::new(storage);
builder
.extract_file(&go_file)
.await
.expect("extract go file");
let graph = builder.graph();
assert!(
graph.contains_node(&go_file),
"Go file node should be added to graph even if no edges extracted"
);
}
#[tokio::test]
async fn test_batch_extraction_mixed_languages() {
let temp_dir = setup_temp_dir();
let rust_file = create_rust_test_file(temp_dir.path(), "app", &["std::fs"]);
let ts_file = create_typescript_test_file(temp_dir.path(), "index", &["./app"]);
let py_file = create_python_test_file(temp_dir.path(), "config", &["os"]);
let go_file = create_go_test_file(temp_dir.path(), "server", &["fmt"]);
let files = vec![
rust_file.clone(),
ts_file.clone(),
py_file.clone(),
go_file.clone(),
];
let storage = Box::new(InMemoryStorage::new());
let mut builder = DependencyGraphBuilder::new(storage);
builder
.extract_files(&files)
.await
.expect("batch extraction");
let graph = builder.graph();
assert!(graph.contains_node(&rust_file));
assert!(graph.contains_node(&ts_file));
assert!(graph.contains_node(&py_file));
assert!(graph.contains_node(&go_file));
assert!(
graph.edge_count() >= 3,
"Expected at least 3 edges from Rust/TS/Python files"
);
}
#[tokio::test]
async fn test_graph_construction_multi_file() {
let temp_dir = setup_temp_dir();
let config_file = create_rust_test_file(temp_dir.path(), "config", &[]);
let utils_file = create_rust_test_file(temp_dir.path(), "utils", &["crate::config"]);
let main_file = create_rust_test_file(temp_dir.path(), "main", &["crate::utils"]);
let storage = Box::new(InMemoryStorage::new());
let mut builder = DependencyGraphBuilder::new(storage);
builder
.extract_files(&[main_file.clone(), utils_file.clone(), config_file.clone()])
.await
.expect("extract files");
let graph = builder.graph();
assert!(
graph.contains_node(&main_file),
"main file should be in graph"
);
assert!(
graph.contains_node(&utils_file),
"utils file should be in graph"
);
assert!(
graph.contains_node(&config_file),
"config file should be in graph"
);
assert!(
graph.edge_count() > 0,
"Graph should have at least some edges"
);
}
#[tokio::test]
async fn test_storage_persistence() {
let temp_dir = setup_temp_dir();
let rust_file = create_rust_test_file(temp_dir.path(), "main", &["std::fs", "std::io"]);
let storage = InMemoryStorage::new();
let mut builder = DependencyGraphBuilder::new(Box::new(storage));
builder
.extract_file(&rust_file)
.await
.expect("extract file");
let edge_count_before = builder.graph().edge_count();
assert!(edge_count_before > 0, "Graph should have edges");
builder.persist().await.expect("persist graph");
assert_eq!(
builder.graph().edge_count(),
edge_count_before,
"Graph should maintain edge count after persist"
);
}
#[test]
fn test_language_detection() {
assert_eq!(
LanguageDetector::detect_language(Path::new("file.rs")),
Some(Language::Rust)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.ts")),
Some(Language::TypeScript)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.tsx")),
Some(Language::TypeScript)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.js")),
Some(Language::JavaScript)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.jsx")),
Some(Language::JavaScript)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.py")),
Some(Language::Python)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.go")),
Some(Language::Go)
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.java")),
None
);
assert_eq!(
LanguageDetector::detect_language(Path::new("file.cpp")),
None
);
assert_eq!(
LanguageDetector::detect_language(Path::new("FILE.RS")),
Some(Language::Rust)
);
}
#[tokio::test]
async fn test_symbol_level_tracking() {
let temp_dir = setup_temp_dir();
let rust_content = r#"
use std::collections::HashMap;
use crate::utils::Config;
pub struct App {
config: Config,
}
"#;
let rust_file = temp_dir.path().join("app.rs");
std::fs::write(&rust_file, rust_content).expect("write rust file");
let storage = Box::new(InMemoryStorage::new());
let mut builder = DependencyGraphBuilder::new(storage);
builder
.extract_file(&rust_file)
.await
.expect("extract file");
let graph = builder.graph();
let edges = graph.get_dependencies(&rust_file);
let has_symbol_info = edges.iter().any(|edge| edge.symbol.is_some());
assert!(
has_symbol_info,
"At least one edge should have symbol-level tracking"
);
}
#[tokio::test]
async fn test_batch_performance() {
let temp_dir = setup_temp_dir();
let mut files = Vec::new();
for i in 0..100 {
let file = create_rust_test_file(
temp_dir.path(),
&format!("file{}", i),
&["std::fs", "std::io"],
);
files.push(file);
}
let storage = Box::new(InMemoryStorage::new());
let mut builder = DependencyGraphBuilder::new(storage);
let start = std::time::Instant::now();
builder.extract_files(&files).await.expect("batch extract");
let duration = start.elapsed();
if duration.as_millis() >= 100 {
eprintln!(
"⚠️ Performance: Batch extraction took {:?} (target: <100ms)",
duration
);
}
assert!(
duration.as_millis() < 1000,
"Batch extraction took {:?}, expected <1s (stretch goal: <100ms)",
duration
);
let graph = builder.graph();
assert!(
graph.node_count() >= 100,
"At least 100 file nodes should be in graph, got {}",
graph.node_count()
);
}
#[tokio::test]
async fn test_extraction_error_handling() {
let temp_dir = setup_temp_dir();
let bad_rust_file = temp_dir.path().join("bad.rs");
std::fs::write(&bad_rust_file, "use incomplete syntax without semicolon")
.expect("write bad file");
let good_rust_file = create_rust_test_file(temp_dir.path(), "good", &["std::fs"]);
let storage = Box::new(InMemoryStorage::new());
let mut builder = DependencyGraphBuilder::new(storage);
let result = builder
.extract_files(&[bad_rust_file.clone(), good_rust_file.clone()])
.await;
match result {
Ok(_) => {
assert!(builder.graph().contains_node(&good_rust_file));
}
Err(_) => {
}
}
}
#[tokio::test]
async fn test_unsupported_language() {
let temp_dir = setup_temp_dir();
let java_file = temp_dir.path().join("Main.java");
std::fs::write(&java_file, "public class Main {}").expect("write java file");
let storage = Box::new(InMemoryStorage::new());
let mut builder = DependencyGraphBuilder::new(storage);
let result = builder.extract_file(&java_file).await;
assert!(
result.is_err(),
"Extracting unsupported language should fail"
);
}