use anyhow::Context;
use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;
use std::process::Command as StdCommand;
use tempfile::TempDir;
fn init_git_repo(dir: &TempDir) -> Result<(), Box<dyn std::error::Error>> {
let repo_path = dir.path();
StdCommand::new("git")
.args(["init"])
.current_dir(repo_path)
.output()
.context("Failed to initialize git repository")?;
StdCommand::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(repo_path)
.output()
.context("Failed to configure git user.name")?;
StdCommand::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(repo_path)
.output()
.context("Failed to configure git user.email")?;
StdCommand::new("git")
.args(["config", "commit.gpgsign", "false"])
.current_dir(repo_path)
.output()
.context("Failed to disable GPG signing for tests")?;
Ok(())
}
fn create_commit(
dir: &TempDir,
files: &[(&str, &str)],
message: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let repo_path = dir.path();
for (filename, content) in files {
std::fs::write(repo_path.join(filename), content)
.with_context(|| format!("Failed to write file: {filename}"))?;
}
StdCommand::new("git")
.args(["add", "-A"])
.current_dir(repo_path)
.output()
.context("Failed to stage files with git add")?;
StdCommand::new("git")
.args(["commit", "-m", message])
.current_dir(repo_path)
.output()
.with_context(|| format!("Failed to create commit: {message}"))?;
let output = StdCommand::new("git")
.args(["rev-list", "-n", "1", "--abbrev-commit", "HEAD"])
.current_dir(repo_path)
.output()
.context("Failed to get commit hash")?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
#[test]
fn test_from_option_checks_only_changed_files() {
let temp_dir = TempDir::new().unwrap();
init_git_repo(&temp_dir).unwrap();
create_commit(
&temp_dir,
&[
("file1.txt", "line 1\nline 2\n"),
("file2.txt", "line 1\nline 2\n"),
],
"Initial commit",
)
.unwrap();
let first_commit =
create_commit(&temp_dir, &[("file3.txt", "line 1\nline 2\n")], "Add file3").unwrap();
create_commit(
&temp_dir,
&[
("file4.txt", "line 1 \nline 2"), ],
"Add file4 with issues",
)
.unwrap();
let mut cmd = cargo_bin_cmd!("lineguard");
cmd.current_dir(&temp_dir);
cmd.arg("--from").arg(&first_commit);
cmd.arg(".");
cmd.assert()
.code(1) .stdout(predicate::str::contains("file4.txt"))
.stdout(predicate::str::contains("file1.txt").not())
.stdout(predicate::str::contains("file2.txt").not())
.stdout(predicate::str::contains("file3.txt").not());
}
#[test]
fn test_from_to_option_checks_range() {
let temp_dir = TempDir::new().unwrap();
init_git_repo(&temp_dir).unwrap();
let commit1 =
create_commit(&temp_dir, &[("file1.txt", "line 1\nline 2\n")], "Commit 1").unwrap();
let _commit2 = create_commit(
&temp_dir,
&[
("file2.txt", "line 1 \nline 2\n"), ],
"Commit 2",
)
.unwrap();
let commit3 = create_commit(
&temp_dir,
&[
("file3.txt", "line 1\nline 2"), ],
"Commit 3",
)
.unwrap();
create_commit(
&temp_dir,
&[
("file4.txt", "line 1 \n"), ],
"Commit 4",
)
.unwrap();
let mut cmd = cargo_bin_cmd!("lineguard");
cmd.current_dir(&temp_dir);
cmd.arg("--from").arg(&commit1);
cmd.arg("--to").arg(&commit3);
cmd.arg(".");
cmd.assert()
.code(1) .stdout(predicate::str::contains("file2.txt"))
.stdout(predicate::str::contains("file3.txt"))
.stdout(predicate::str::contains("file4.txt").not());
}
#[test]
fn test_from_without_git_repo_shows_error() {
let temp_dir = TempDir::new().unwrap();
std::fs::write(temp_dir.path().join("file.txt"), "content\n").unwrap();
let mut cmd = cargo_bin_cmd!("lineguard");
cmd.current_dir(&temp_dir);
cmd.arg("--from").arg("HEAD~1");
cmd.arg(".");
cmd.assert()
.failure()
.stderr(predicate::str::contains("not a git repository"));
}
#[test]
fn test_invalid_git_reference_shows_error() {
let temp_dir = TempDir::new().unwrap();
init_git_repo(&temp_dir).unwrap();
create_commit(&temp_dir, &[("file.txt", "content\n")], "Initial commit").unwrap();
let mut cmd = cargo_bin_cmd!("lineguard");
cmd.current_dir(&temp_dir);
cmd.arg("--from").arg("invalid-hash");
cmd.arg(".");
cmd.assert()
.failure()
.stderr(predicate::str::contains("Invalid git reference"));
}
#[test]
fn test_from_option_with_fix_mode() {
let temp_dir = TempDir::new().unwrap();
init_git_repo(&temp_dir).unwrap();
let first_commit = create_commit(
&temp_dir,
&[("unchanged.txt", "line 1\nline 2\n")],
"First commit",
)
.unwrap();
create_commit(
&temp_dir,
&[("changed.txt", "line 1 \nline 2")],
"Add file with issues",
)
.unwrap();
let mut cmd = cargo_bin_cmd!("lineguard");
cmd.current_dir(&temp_dir);
cmd.arg("--from").arg(&first_commit);
cmd.arg("--fix");
cmd.arg(".");
cmd.assert().success();
let content = std::fs::read_to_string(temp_dir.path().join("changed.txt")).unwrap();
assert_eq!(content, "line 1\nline 2\n");
}
#[test]
fn test_from_option_with_json_output() {
let temp_dir = TempDir::new().unwrap();
init_git_repo(&temp_dir).unwrap();
let first_commit =
create_commit(&temp_dir, &[("file1.txt", "content\n")], "First commit").unwrap();
create_commit(&temp_dir, &[("file2.txt", "content \n")], "Second commit").unwrap();
let mut cmd = cargo_bin_cmd!("lineguard");
cmd.current_dir(&temp_dir);
cmd.arg("--from").arg(&first_commit);
cmd.arg("--format").arg("json");
cmd.arg(".");
cmd.assert()
.code(1) .stdout(predicate::str::contains("\"files_checked\": 1"))
.stdout(predicate::str::contains("file2.txt"));
}
#[test]
fn test_verbose_mode_shows_git_range_info() {
let temp_dir = TempDir::new().unwrap();
init_git_repo(&temp_dir).unwrap();
let commit1 = create_commit(
&temp_dir,
&[
("file1.txt", "line 1\nline 2\n"),
("file2.txt", "line 1\nline 2\n"),
],
"Initial commit",
)
.unwrap();
let _commit2 = create_commit(
&temp_dir,
&[
("file3.txt", "line 1\nline 2\n"),
("file4.txt", "line 1 \nline 2"), ],
"Add more files",
)
.unwrap();
let commit3 = create_commit(
&temp_dir,
&[
("file5.txt", "line 1\nline 2\n"),
("file6.txt", "line 1\nline 2"), ],
"Add even more files",
)
.unwrap();
let mut cmd = cargo_bin_cmd!("lineguard");
cmd.current_dir(&temp_dir);
cmd.arg("--verbose");
cmd.arg("--from").arg(&commit1);
cmd.arg("--to").arg(&commit3);
cmd.arg(".");
cmd.assert()
.code(1) .stdout(predicate::str::contains("Git range:"))
.stdout(predicate::str::contains(&commit1[0..7])) .stdout(predicate::str::contains(&commit3[0..7])) .stdout(predicate::str::contains("Changed files: 4"))
.stdout(predicate::str::contains("file3.txt"))
.stdout(predicate::str::contains("file4.txt"))
.stdout(predicate::str::contains("file5.txt"))
.stdout(predicate::str::contains("file6.txt"));
}