use std::path::PathBuf;
use std::process::Command;
fn binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_code-graph"))
}
fn project_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn run_success(args: &[&str]) -> String {
let out = Command::new(binary())
.args(args)
.output()
.expect("failed to invoke code-graph binary");
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
out.status.success(),
"command {:?} failed with status {:?}\nstdout: {}\nstderr: {}",
args,
out.status,
stdout,
stderr
);
stdout
}
fn run_failure(args: &[&str]) -> (String, String) {
let out = Command::new(binary())
.args(args)
.output()
.expect("failed to invoke code-graph binary");
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
!out.status.success(),
"command {:?} expected to fail but exited successfully\nstdout: {}\nstderr: {}",
args,
stdout,
stderr
);
(stdout, stderr)
}
#[test]
fn test_index_rust_codebase() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["index", path]);
assert!(
stdout.contains("files"),
"index output should contain 'files'\nstdout: {}",
stdout
);
}
#[test]
fn test_index_json_output() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["index", "--json", path]);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("index --json output is not valid JSON");
let file_count = parsed["file_count"]
.as_u64()
.expect("JSON missing 'file_count' field");
assert!(
file_count > 0,
"file_count should be > 0, got {}",
file_count
);
let rust_file_count = parsed["rust_file_count"]
.as_u64()
.expect("JSON missing 'rust_file_count' field");
assert!(
rust_file_count > 0,
"rust_file_count should be > 0 for code-graph own source"
);
}
#[test]
fn test_find_rust_symbol() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["find", "parse_file_parallel", path]);
assert!(
stdout.contains("def"),
"find output should contain 'def' prefix\nstdout: {}",
stdout
);
assert!(
stdout.contains("parse_file_parallel"),
"find output should contain the symbol name\nstdout: {}",
stdout
);
let def_count = stdout.lines().filter(|l| l.contains("def")).count();
assert!(def_count > 0, "should have at least one 'def' result line");
}
#[test]
fn test_find_regex_pattern() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["find", ".*parse.*", path]);
let def_count = stdout.lines().filter(|l| l.starts_with("def")).count();
assert!(
def_count > 1,
"regex '.*parse.*' should match multiple symbols, got {} def lines\nstdout: {}",
def_count,
stdout
);
}
#[test]
fn test_find_nonexistent_symbol() {
let root = project_root();
let path = root.to_str().unwrap();
let (_, stderr) = run_failure(&["find", "zzz_nonexistent_symbol_zzz", path]);
assert!(
stderr.contains("no symbols matching") || stderr.contains("No"),
"stderr should indicate symbol not found\nstderr: {}",
stderr
);
}
#[test]
fn test_refs_rust_symbol() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["refs", "parse_file_parallel", path]);
assert!(
!stdout.trim().is_empty(),
"refs output should be non-empty for 'parse_file_parallel'\nstdout: {}",
stdout
);
let ref_count = stdout
.lines()
.filter(|l| l.starts_with("ref") || l.contains("ref"))
.count();
assert!(
ref_count > 0,
"refs output should contain at least one 'ref' line\nstdout: {}",
stdout
);
}
#[test]
fn test_impact_rust_symbol() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["impact", "parse_file_parallel", path]);
assert!(
!stdout.trim().is_empty(),
"impact output should be non-empty for 'parse_file_parallel'\nstdout: {}",
stdout
);
let has_impact_line = stdout
.lines()
.any(|l| l.contains("impact") || l.contains("file"));
assert!(
has_impact_line,
"impact output should contain 'impact' or 'file' lines\nstdout: {}",
stdout
);
}
#[test]
fn test_circular_on_rust_codebase() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["circular", path]);
let _ = stdout;
}
#[test]
fn test_stats_rust_codebase() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["stats", path]);
assert!(
stdout.contains("Rust"),
"stats output should contain 'Rust' section for code-graph own source\nstdout: {}",
stdout
);
}
#[test]
fn test_context_rust_symbol() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["context", "build_graph", path]);
assert!(
!stdout.trim().is_empty(),
"context output should be non-empty\nstdout: {}",
stdout
);
assert!(
stdout.contains("build_graph"),
"context output should contain the symbol name\nstdout: {}",
stdout
);
}
#[test]
fn test_language_filter_rust() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["find", ".*", "--language", "rust", path]);
assert!(
stdout.contains("def"),
"filtered find output should contain 'def' lines\nstdout: {}",
stdout
);
for line in stdout.lines() {
if line.starts_with("def") {
assert!(
line.contains(".rs"),
"--language rust filter: line should reference a .rs file but got: {}",
line
);
assert!(
!line.contains(".ts")
&& !line.contains(".tsx")
&& !line.contains(".js")
&& !line.contains(".jsx"),
"--language rust filter: line should not reference TS/JS files: {}",
line
);
}
}
}
#[test]
fn test_language_filter_invalid() {
let root = project_root();
let path = root.to_str().unwrap();
let (_, stderr) = run_failure(&["find", ".*", "--language", "python", path]);
assert!(
stderr.contains("unknown language") || stderr.contains("python"),
"stderr should mention 'unknown language' or 'python'\nstderr: {}",
stderr
);
}
#[test]
fn test_stats_language_filter() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["stats", "--language", "rust", path]);
assert!(
stdout.contains("Rust"),
"stats --language rust should contain 'Rust'\nstdout: {}",
stdout
);
assert!(
!stdout.contains("TypeScript"),
"stats --language rust should not show TypeScript section for pure-Rust project\nstdout: {}",
stdout
);
}
#[test]
fn test_mixed_language_project() {
use std::fs;
let tmp = tempfile::TempDir::new().expect("failed to create temp dir");
let tmp_path = tmp.path();
let cargo_toml = r#"[package]
name = "test"
version = "0.1.0"
edition = "2021"
"#;
fs::write(tmp_path.join("Cargo.toml"), cargo_toml).unwrap();
fs::create_dir_all(tmp_path.join("src")).unwrap();
fs::write(tmp_path.join("src").join("lib.rs"), "pub fn hello() {}\n").unwrap();
fs::write(tmp_path.join("tsconfig.json"), "{}").unwrap();
fs::write(
tmp_path.join("src").join("index.ts"),
"export function greet() {}\n",
)
.unwrap();
let path = tmp_path.to_str().unwrap();
let index_stdout = run_success(&["index", path]);
assert!(
index_stdout.contains("files"),
"index should mention 'files'\nstdout: {}",
index_stdout
);
assert!(
index_stdout.contains("Indexed"),
"index should mention 'Indexed'\nstdout: {}",
index_stdout
);
let find_stdout = run_success(&["find", ".*", path]);
assert!(
find_stdout.contains("hello"),
"mixed project find should contain Rust function 'hello'\nstdout: {}",
find_stdout
);
assert!(
find_stdout.contains("greet"),
"mixed project find should contain TypeScript function 'greet'\nstdout: {}",
find_stdout
);
let stats_stdout = run_success(&["stats", path]);
assert!(
stats_stdout.contains("Rust"),
"mixed project stats should contain 'Rust' section\nstdout: {}",
stats_stdout
);
assert!(
stats_stdout.contains("TypeScript"),
"mixed project stats should contain 'TypeScript' section\nstdout: {}",
stats_stdout
);
}
fn run_export(extra_args: &[&str]) -> (String, String) {
let root = project_root();
let mut args = vec!["export", root.to_str().unwrap()];
args.extend_from_slice(extra_args);
let out = Command::new(binary())
.args(&args)
.output()
.expect("failed to invoke code-graph binary");
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
out.status.success(),
"export {:?} failed\nstdout: {}\nstderr: {}",
extra_args,
stdout,
stderr
);
(stdout, stderr)
}
#[test]
fn test_export_dot() {
let (stdout, _stderr) = run_export(&["--format", "dot", "--stdout"]);
assert!(
stdout.contains("digraph code_graph"),
"DOT output should contain 'digraph code_graph'\nstdout: {}",
&stdout[..stdout.len().min(500)]
);
assert!(
stdout.contains("rankdir=TB"),
"DOT output should contain 'rankdir=TB'\nstdout: {}",
&stdout[..stdout.len().min(500)]
);
assert!(
stdout.contains("[label="),
"DOT output should contain at least one labeled node\nstdout: {}",
&stdout[..stdout.len().min(500)]
);
assert!(
stdout.contains("->"),
"DOT output should contain at least one edge '->'\nstdout: {}",
&stdout[..stdout.len().min(500)]
);
}
#[test]
fn test_export_mermaid() {
let (stdout, _stderr) = run_export(&["--format", "mermaid", "--stdout"]);
assert!(
stdout.contains("flowchart TB"),
"Mermaid output should contain 'flowchart TB'\nstdout: {}",
&stdout[..stdout.len().min(500)]
);
assert!(
stdout.contains("[\""),
"Mermaid output should contain node syntax '[\"'\nstdout: {}",
&stdout[..stdout.len().min(500)]
);
assert!(
stdout.contains("-->"),
"Mermaid output should contain at least one edge '-->'\nstdout: {}",
&stdout[..stdout.len().min(500)]
);
}
#[test]
fn test_export_granularity() {
let (file_stdout, _) = run_export(&["--granularity", "file", "--stdout"]);
let (symbol_stdout, _) = run_export(&["--granularity", "symbol", "--stdout"]);
assert_ne!(
file_stdout, symbol_stdout,
"file and symbol granularity should produce different output"
);
assert!(
!file_stdout.contains("(fn)"),
"file granularity output should not contain symbol-level '(fn)' annotations\n{}",
&file_stdout[..file_stdout.len().min(500)]
);
assert!(
!file_stdout.contains("(struct)"),
"file granularity output should not contain symbol-level '(struct)' annotations\n{}",
&file_stdout[..file_stdout.len().min(500)]
);
let has_kind_annotation = symbol_stdout.contains("(fn)")
|| symbol_stdout.contains("(struct)")
|| symbol_stdout.contains("(enum)");
assert!(
has_kind_annotation,
"symbol granularity output should contain kind annotations like '(fn)', '(struct)', '(enum)'\n{}",
&symbol_stdout[..symbol_stdout.len().min(500)]
);
}
#[test]
fn test_export_dot_package_clusters() {
let (stdout, _stderr) = run_export(&["--granularity", "package", "--stdout"]);
assert!(
stdout.contains("subgraph cluster_"),
"package granularity DOT output should contain 'subgraph cluster_'\nstdout: {}",
&stdout[..stdout.len().min(500)]
);
let cluster_count = stdout.matches("subgraph cluster_").count();
assert!(
cluster_count >= 2,
"package granularity DOT should have at least 2 cluster subgraphs, found {}\nstdout: {}",
cluster_count,
&stdout[..stdout.len().min(800)]
);
}
#[test]
fn test_export_mermaid_edge_limit_warning() {
let root = project_root();
let out = Command::new(binary())
.args([
"export",
root.to_str().unwrap(),
"--granularity",
"symbol",
"--stdout",
])
.output()
.expect("failed to invoke code-graph binary");
assert!(out.status.success(), "export symbol --stdout should exit 0");
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
stderr.contains("Warning:"),
"symbol granularity should produce a Warning on stderr for >200 nodes\nstderr: {}",
stderr
);
let has_relevant_warning =
stderr.contains("nodes") || stderr.contains("granularity") || stderr.contains("edges");
assert!(
has_relevant_warning,
"Warning should mention 'nodes', 'granularity', or 'edges'\nstderr: {}",
stderr
);
let dot_out = Command::new(binary())
.args([
"export",
root.to_str().unwrap(),
"--format",
"dot",
"--granularity",
"symbol",
"--stdout",
])
.output()
.expect("failed to invoke code-graph binary");
let dot_stderr = String::from_utf8_lossy(&dot_out.stderr).to_string();
assert!(
!dot_stderr.contains("Mermaid"),
"DOT export should not produce a Mermaid-specific warning\nstderr: {}",
dot_stderr
);
}
#[test]
fn test_export_mcp_tool_registered() {
let (stdout, _stderr) = run_export(&["--format", "dot", "--granularity", "file", "--stdout"]);
assert!(
stdout.contains("digraph code_graph"),
"export_graph() pipeline should produce valid DOT output\nstdout: {}",
&stdout[..stdout.len().min(500)]
);
let node_count = stdout.matches("[label=").count();
assert!(
node_count > 0,
"export_graph() should produce at least one node\nstdout: {}",
&stdout[..stdout.len().min(500)]
);
}
#[test]
fn test_find_json_output() {
let root = project_root();
let path = root.to_str().unwrap();
let stdout = run_success(&["find", "build_graph", "--format", "json", path]);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("find --format json output is not valid JSON");
let arr = parsed
.as_array()
.expect("find --format json should return a JSON array");
assert!(
!arr.is_empty(),
"JSON array should have at least one result"
);
let first = &arr[0];
assert!(
first.get("name").is_some(),
"JSON result should have 'name' key\ngot: {}",
first
);
assert!(
first.get("file").is_some(),
"JSON result should have 'file' key\ngot: {}",
first
);
assert!(
first.get("kind").is_some(),
"JSON result should have 'kind' key\ngot: {}",
first
);
assert!(
first.get("line").is_some(),
"JSON result should have 'line' key\ngot: {}",
first
);
}