use serial_test::serial;
use std::fs;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
mod common;
use common::sqry_bin;
fn init_git_repo(dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
let output = Command::new("git")
.arg("-C")
.arg(dir)
.args(["init"])
.output()?;
if !output.status.success() {
return Err(format!(
"git init failed: {}",
String::from_utf8_lossy(&output.stderr)
)
.into());
}
Command::new("git")
.arg("-C")
.arg(dir)
.args(["config", "user.name", "Test User"])
.output()?;
Command::new("git")
.arg("-C")
.arg(dir)
.args(["config", "user.email", "test@example.com"])
.output()?;
Command::new("git")
.arg("-C")
.arg(dir)
.args(["config", "commit.gpgsign", "false"])
.output()?;
Ok(())
}
fn create_and_commit_file(
dir: &Path,
filename: &str,
content: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let file_path = dir.join(filename);
fs::write(&file_path, content)?;
Command::new("git")
.arg("-C")
.arg(dir)
.args(["add", filename])
.output()?;
let output = Command::new("git")
.arg("-C")
.arg(dir)
.args(["commit", "-m", &format!("Add {filename}")])
.output()?;
if !output.status.success() {
return Err(format!(
"git commit failed: {}",
String::from_utf8_lossy(&output.stderr)
)
.into());
}
Ok(())
}
fn run_sqry_index(dir: &Path, force: bool) -> Result<String, Box<dyn std::error::Error>> {
let mut cmd = Command::new(sqry_bin());
cmd.arg("index").arg(dir);
if force {
cmd.arg("--force");
}
let output = cmd.output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string()
+ &String::from_utf8_lossy(&output.stderr))
}
fn run_sqry_update(dir: &Path) -> Result<(String, bool), Box<dyn std::error::Error>> {
let output = Command::new(sqry_bin()).arg("update").arg(dir).output()?;
let stdout_stderr = String::from_utf8_lossy(&output.stdout).to_string()
+ &String::from_utf8_lossy(&output.stderr);
Ok((stdout_stderr, output.status.success()))
}
fn is_git_available() -> bool {
Command::new("git")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn get_head_commit(dir: &Path) -> Result<String, Box<dyn std::error::Error>> {
let output = Command::new("git")
.arg("-C")
.arg(dir)
.args(["rev-parse", "HEAD"])
.output()?;
if !output.status.success() {
return Err("Failed to get HEAD commit".into());
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn get_last_indexed_commit(dir: &Path) -> Result<Option<String>, Box<dyn std::error::Error>> {
use sqry_core::graph::unified::persistence::GraphStorage;
let storage = GraphStorage::new(dir);
if !storage.exists() {
return Ok(None);
}
let manifest = storage.load_manifest()?;
Ok(manifest.last_indexed_commit)
}
#[test]
#[serial]
fn test_git_aware_update_single_file_change() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
create_and_commit_file(repo_path, "main.rs", "fn main() { println!(\"v1\"); }")?;
let output = run_sqry_index(repo_path, false)?;
assert!(output.contains("Index built successfully") || output.contains("indexed"));
let initial_commit = get_head_commit(repo_path)?;
let indexed_commit = get_last_indexed_commit(repo_path)?;
assert_eq!(indexed_commit, Some(initial_commit.clone()));
create_and_commit_file(repo_path, "main.rs", "fn main() { println!(\"v2\"); }")?;
let (update_output, success) = run_sqry_update(repo_path)?;
assert!(success, "Update should succeed");
assert!(
update_output.contains("updated") || update_output.contains("Updated"),
"Output should indicate files were updated: {update_output}"
);
let new_commit = get_head_commit(repo_path)?;
let new_indexed_commit = get_last_indexed_commit(repo_path)?;
assert_eq!(new_indexed_commit, Some(new_commit));
assert_ne!(Some(initial_commit), new_indexed_commit);
Ok(())
}
#[test]
#[serial]
fn test_git_aware_handles_renames() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
create_and_commit_file(
repo_path,
"old_name.rs",
"fn old_function() { println!(\"hello\"); }",
)?;
run_sqry_index(repo_path, false)?;
Command::new("git")
.arg("-C")
.arg(repo_path)
.args(["mv", "old_name.rs", "new_name.rs"])
.output()?;
Command::new("git")
.arg("-C")
.arg(repo_path)
.args(["commit", "-m", "Rename file"])
.output()?;
let (update_output, success) = run_sqry_update(repo_path)?;
assert!(success);
assert!(
update_output.contains("updated") || update_output.contains("Updated"),
"Should detect rename as change: {update_output}"
);
Ok(())
}
#[test]
#[serial]
fn test_full_build_populates_baseline() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
create_and_commit_file(repo_path, "test.rs", "fn test() {}")?;
run_sqry_index(repo_path, false)?;
let head = get_head_commit(repo_path)?;
let baseline = get_last_indexed_commit(repo_path)?;
assert_eq!(
baseline,
Some(head),
"Full build should record HEAD as baseline"
);
Ok(())
}
#[test]
#[serial]
fn test_uncommitted_changes_detection() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
create_and_commit_file(repo_path, "main.rs", "fn main() {}")?;
run_sqry_index(repo_path, false)?;
fs::write(
repo_path.join("main.rs"),
"fn main() { println!(\"modified\"); }",
)?;
let (update_output, success) = run_sqry_update(repo_path)?;
assert!(success);
assert!(
update_output.contains("updated") || update_output.contains("Updated"),
"Should detect uncommitted changes: {update_output}"
);
Ok(())
}
#[test]
#[serial]
fn test_empty_changeset() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
create_and_commit_file(repo_path, "main.rs", "fn main() {}")?;
run_sqry_index(repo_path, false)?;
let (update_output, success) = run_sqry_update(repo_path)?;
assert!(success);
assert!(
update_output.contains("unchanged") || update_output.contains("successfully"),
"Empty changeset should complete successfully: {update_output}"
);
Ok(())
}
#[test]
#[serial]
fn test_fallback_when_not_git_repo() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
fs::write(repo_path.join("main.rs"), "fn main() {}")?;
let output = run_sqry_index(repo_path, false)?;
assert!(output.contains("Index built successfully") || output.contains("indexed"));
fs::write(repo_path.join("main.rs"), "fn main() { println!(\"v2\"); }")?;
let (update_output, success) = run_sqry_update(repo_path)?;
assert!(success);
assert!(
update_output.contains("hash-based") || update_output.contains("updated"),
"Should fall back to hash-based when not a git repo: {update_output}"
);
Ok(())
}
#[test]
#[serial]
fn test_repo_without_commits() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
fs::write(repo_path.join("main.rs"), "fn main() {}")?;
let output = run_sqry_index(repo_path, false)?;
assert!(output.contains("Index built successfully") || output.contains("indexed"));
let baseline = get_last_indexed_commit(repo_path)?;
assert_eq!(baseline, None, "HEAD-less repo should have no baseline");
fs::write(repo_path.join("main.rs"), "fn main() { println!(\"v2\"); }")?;
let (update_output, success) = run_sqry_update(repo_path)?;
assert!(success);
assert!(
update_output.contains("updated") || update_output.contains("hash-based"),
"HEAD-less repo should fall back to hash-based: {update_output}"
);
Ok(())
}
#[test]
#[serial]
fn test_untracked_toggle() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
create_and_commit_file(repo_path, "main.rs", "fn main() {}")?;
run_sqry_index(repo_path, false)?;
fs::write(repo_path.join("new.rs"), "fn new() {}")?;
let output1 = Command::new(sqry_bin())
.arg("update")
.arg(repo_path)
.env("SQRY_GIT_INCLUDE_UNTRACKED", "1")
.output()?;
let output1_str = String::from_utf8_lossy(&output1.stdout).to_string()
+ &String::from_utf8_lossy(&output1.stderr);
assert!(
output1_str.contains("updated") || output1_str.contains("Updated"),
"With SQRY_GIT_INCLUDE_UNTRACKED=1, should index untracked files"
);
run_sqry_index(repo_path, true)?;
fs::write(repo_path.join("another.rs"), "fn another() {}")?;
let output2 = Command::new(sqry_bin())
.arg("update")
.arg(repo_path)
.env("SQRY_GIT_INCLUDE_UNTRACKED", "0")
.output()?;
assert!(
output2.status.success(),
"Update should succeed even with SQRY_GIT_INCLUDE_UNTRACKED=0"
);
Ok(())
}
#[test]
#[serial]
fn test_git_backend_none() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
create_and_commit_file(repo_path, "main.rs", "fn main() {}")?;
run_sqry_index(repo_path, false)?;
create_and_commit_file(repo_path, "main.rs", "fn main() { println!(\"v2\"); }")?;
let output = Command::new(sqry_bin())
.arg("update")
.arg(repo_path)
.env("SQRY_GIT_BACKEND", "none")
.output()?;
assert!(output.status.success());
let output_str = String::from_utf8_lossy(&output.stdout).to_string()
+ &String::from_utf8_lossy(&output.stderr);
assert!(
output_str.contains("hash-based") || output_str.contains("updated"),
"SQRY_GIT_BACKEND=none should force hash-based mode"
);
Ok(())
}
#[test]
#[serial]
fn test_rename_case_change() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
create_and_commit_file(repo_path, "Main.rs", "fn main() {}")?;
run_sqry_index(repo_path, false)?;
Command::new("git")
.arg("-C")
.arg(repo_path)
.args(["mv", "Main.rs", "main.rs"])
.output()?;
Command::new("git")
.arg("-C")
.arg(repo_path)
.args(["commit", "-m", "Change case"])
.output()?;
let (update_output, success) = run_sqry_update(repo_path)?;
assert!(success, "Case-change rename should be handled");
assert!(
update_output.contains("updated") || update_output.contains("successfully"),
"Case change should be detected: {update_output}"
);
Ok(())
}
#[test]
#[serial]
fn test_multiple_files_changed() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
for i in 1..=10 {
create_and_commit_file(
repo_path,
&format!("file{i}.rs"),
&format!("fn func{i}() {{}}"),
)?;
}
run_sqry_index(repo_path, false)?;
for i in 1..=5 {
fs::write(
repo_path.join(format!("file{i}.rs")),
format!("fn func{i}() {{ println!(\"modified\"); }}"),
)?;
}
Command::new("git")
.arg("-C")
.arg(repo_path)
.args(["add", "."])
.output()?;
Command::new("git")
.arg("-C")
.arg(repo_path)
.args(["commit", "-m", "Modify 5 files"])
.output()?;
let (update_output, success) = run_sqry_update(repo_path)?;
assert!(success);
assert!(
update_output.contains("updated") || update_output.contains("Updated"),
"Should detect multiple file changes: {update_output}"
);
Ok(())
}
#[test]
#[serial]
fn test_deleted_file() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
create_and_commit_file(repo_path, "to_delete.rs", "fn delete_me() {}")?;
create_and_commit_file(repo_path, "keep.rs", "fn keep() {}")?;
run_sqry_index(repo_path, false)?;
Command::new("git")
.arg("-C")
.arg(repo_path)
.args(["rm", "to_delete.rs"])
.output()?;
Command::new("git")
.arg("-C")
.arg(repo_path)
.args(["commit", "-m", "Delete file"])
.output()?;
let (update_output, success) = run_sqry_update(repo_path)?;
assert!(success);
assert!(
update_output.contains("removed")
|| update_output.contains("updated")
|| update_output.contains("successfully"),
"Should handle file deletion: {update_output}"
);
Ok(())
}
#[test]
#[serial]
fn test_added_file() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
create_and_commit_file(repo_path, "existing.rs", "fn existing() {}")?;
run_sqry_index(repo_path, false)?;
create_and_commit_file(repo_path, "new.rs", "fn new() {}")?;
let (update_output, success) = run_sqry_update(repo_path)?;
assert!(success);
assert!(
update_output.contains("updated") || update_output.contains("Updated"),
"Should detect new file: {update_output}"
);
Ok(())
}
#[test]
#[serial]
fn test_baseline_commit_updates_after_each_update() -> Result<(), Box<dyn std::error::Error>> {
if !is_git_available() {
eprintln!("Skipping test: git not available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path();
init_git_repo(repo_path)?;
create_and_commit_file(repo_path, "v1.rs", "fn v1() {}")?;
run_sqry_index(repo_path, false)?;
let baseline1 = get_last_indexed_commit(repo_path)?;
let head1 = get_head_commit(repo_path)?;
assert_eq!(baseline1, Some(head1.clone()));
create_and_commit_file(repo_path, "v2.rs", "fn v2() {}")?;
run_sqry_update(repo_path)?;
let baseline2 = get_last_indexed_commit(repo_path)?;
let head2 = get_head_commit(repo_path)?;
assert_eq!(baseline2, Some(head2.clone()));
assert_ne!(baseline1, baseline2);
create_and_commit_file(repo_path, "v3.rs", "fn v3() {}")?;
run_sqry_update(repo_path)?;
let baseline3 = get_last_indexed_commit(repo_path)?;
let head3 = get_head_commit(repo_path)?;
assert_eq!(baseline3, Some(head3));
assert_ne!(baseline2, baseline3);
Ok(())
}