use std::env;
use std::path::Path;
use std::process::{Command, Stdio};
use tempfile::{tempdir, TempDir};
pub fn hermetic_git_env() {
env::set_var("GIT_CONFIG_NOSYSTEM", "true");
env::set_var("GIT_CONFIG_GLOBAL", "/dev/null");
env::set_var("GIT_AUTHOR_NAME", "testuser");
env::set_var("GIT_AUTHOR_EMAIL", "testuser@example.com");
env::set_var("GIT_COMMITTER_NAME", "testuser");
env::set_var("GIT_COMMITTER_EMAIL", "testuser@example.com");
}
#[must_use]
pub fn hermetic_git_env_vars() -> [(&'static str, &'static str); 6] {
[
("GIT_CONFIG_NOSYSTEM", "true"),
("GIT_CONFIG_GLOBAL", "/dev/null"),
("GIT_AUTHOR_NAME", "testuser"),
("GIT_AUTHOR_EMAIL", "testuser@example.com"),
("GIT_COMMITTER_NAME", "testuser"),
("GIT_COMMITTER_EMAIL", "testuser@example.com"),
]
}
pub fn run_git_command(args: &[&str], dir: &Path) {
assert!(Command::new("git")
.args(args)
.envs([
("GIT_CONFIG_NOSYSTEM", "true"),
("GIT_CONFIG_GLOBAL", "/dev/null"),
("GIT_AUTHOR_NAME", "testuser"),
("GIT_AUTHOR_EMAIL", "testuser@example.com"),
("GIT_COMMITTER_NAME", "testuser"),
("GIT_COMMITTER_EMAIL", "testuser@example.com"),
])
.current_dir(dir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("Failed to spawn git command")
.success());
}
pub fn init_repo(dir: &Path) {
run_git_command(&["init", "--initial-branch", "master"], dir);
run_git_command(&["commit", "--allow-empty", "-m", "Initial commit"], dir);
}
#[must_use]
pub fn dir_with_repo() -> TempDir {
let tempdir = tempdir().unwrap();
init_repo(tempdir.path());
tempdir
}
pub fn init_repo_simple() {
assert!(Command::new("git")
.arg("init")
.output()
.expect("Failed to init git repo")
.status
.success());
}
pub fn empty_commit() {
assert!(Command::new("git")
.args(["commit", "--allow-empty", "-m", "test commit"])
.output()
.expect("Failed to create empty commit")
.status
.success());
}
pub fn init_repo_with_file(dir: &Path) {
run_git_command(&["init", "--initial-branch", "master"], dir);
std::fs::write(dir.join("test.txt"), "test content").expect("Failed to create test file");
run_git_command(&["add", "test.txt"], dir);
run_git_command(&["commit", "-m", "Initial commit"], dir);
}
#[must_use]
pub fn dir_with_repo_and_file() -> TempDir {
let tempdir = tempdir().unwrap();
init_repo_with_file(tempdir.path());
tempdir
}
pub fn with_isolated_home<F, R>(f: F) -> R
where
F: FnOnce(&Path) -> R,
{
let temp_dir = TempDir::new().unwrap();
let original_home = env::var("HOME").ok();
let original_xdg = env::var("XDG_CONFIG_HOME").ok();
env::set_var("HOME", temp_dir.path());
env::remove_var("XDG_CONFIG_HOME");
let result = f(temp_dir.path());
if let Some(home) = original_home {
env::set_var("HOME", home);
} else {
env::remove_var("HOME");
}
if let Some(xdg) = original_xdg {
env::set_var("XDG_CONFIG_HOME", xdg);
}
result
}
pub fn with_isolated_cwd_git<F, R>(f: F) -> R
where
F: FnOnce(&Path) -> R,
{
hermetic_git_env();
let temp_dir = dir_with_repo();
let _guard = DirGuard::new(temp_dir.path());
f(temp_dir.path())
}
pub fn with_isolated_test_setup<F, R>(f: F) -> R
where
F: FnOnce(&Path, &Path) -> R,
{
with_isolated_home(|home_path| with_isolated_cwd_git(|git_dir| f(git_dir, home_path)))
}
pub fn write_gitperfconfig(dir: &Path, content: &str) {
let config_path = dir.join(".gitperfconfig");
std::fs::write(&config_path, content).expect("Failed to write .gitperfconfig");
}
pub struct DirGuard {
original_dir: std::path::PathBuf,
}
impl DirGuard {
#[must_use]
pub fn new(new_dir: &Path) -> Self {
let original_dir = env::current_dir().expect("Failed to get current directory");
env::set_current_dir(new_dir).expect("Failed to change directory");
DirGuard { original_dir }
}
}
impl Drop for DirGuard {
fn drop(&mut self) {
let _ = env::set_current_dir(&self.original_dir);
}
}
#[must_use]
pub fn setup_test_env_with_config(config_content: &str) -> (TempDir, DirGuard) {
hermetic_git_env();
let temp_dir = dir_with_repo();
write_gitperfconfig(temp_dir.path(), config_content);
let guard = DirGuard::new(temp_dir.path());
(temp_dir, guard)
}
#[cfg(test)]
mod tests {
use super::*;
use std::env::set_current_dir;
#[test]
fn test_hermetic_git_env() {
hermetic_git_env();
assert_eq!(env::var("GIT_CONFIG_NOSYSTEM").unwrap(), "true");
assert_eq!(env::var("GIT_CONFIG_GLOBAL").unwrap(), "/dev/null");
assert_eq!(env::var("GIT_AUTHOR_NAME").unwrap(), "testuser");
assert_eq!(
env::var("GIT_AUTHOR_EMAIL").unwrap(),
"testuser@example.com"
);
}
#[test]
fn test_dir_with_repo() {
let repo_dir = dir_with_repo();
set_current_dir(repo_dir.path()).expect("Failed to change dir");
let output = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.output()
.expect("Failed to run git command");
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "true");
}
#[test]
fn test_init_repo() {
let tempdir = tempdir().unwrap();
init_repo(tempdir.path());
set_current_dir(tempdir.path()).expect("Failed to change dir");
let output = Command::new("git")
.args(["rev-list", "--count", "HEAD"])
.output()
.expect("Failed to run git command");
assert!(output.status.success());
let count = String::from_utf8_lossy(&output.stdout)
.trim()
.parse::<i32>()
.unwrap();
assert_eq!(count, 1);
}
#[test]
fn test_hermetic_git_env_vars() {
let env_vars = hermetic_git_env_vars();
assert_eq!(env_vars.len(), 6);
assert_eq!(env_vars[0], ("GIT_CONFIG_NOSYSTEM", "true"));
assert_eq!(env_vars[1], ("GIT_CONFIG_GLOBAL", "/dev/null"));
assert_eq!(env_vars[2], ("GIT_AUTHOR_NAME", "testuser"));
assert_eq!(env_vars[3], ("GIT_AUTHOR_EMAIL", "testuser@example.com"));
assert_eq!(env_vars[4], ("GIT_COMMITTER_NAME", "testuser"));
assert_eq!(env_vars[5], ("GIT_COMMITTER_EMAIL", "testuser@example.com"));
for (key, value) in &env_vars {
assert!(
!key.is_empty(),
"Environment variable key should not be empty"
);
assert!(
!value.is_empty(),
"Environment variable value for {} should not be empty",
key
);
}
}
#[test]
fn test_init_repo_simple() {
let tempdir = tempdir().unwrap();
let original_dir = env::current_dir().unwrap();
set_current_dir(tempdir.path()).expect("Failed to change to temp dir");
hermetic_git_env();
init_repo_simple();
let output = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.envs(hermetic_git_env_vars())
.output()
.expect("Failed to run git command");
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "true");
set_current_dir(original_dir).expect("Failed to restore directory");
}
#[test]
fn test_empty_commit() {
let tempdir = tempdir().unwrap();
let original_dir = env::current_dir().unwrap();
hermetic_git_env();
init_repo(tempdir.path());
set_current_dir(tempdir.path()).expect("Failed to change to temp dir");
let before = Command::new("git")
.args(["rev-list", "--count", "HEAD"])
.envs(hermetic_git_env_vars())
.output()
.expect("Failed to run git command");
assert!(
before.status.success(),
"Failed to get initial commit count"
);
let count_before = String::from_utf8_lossy(&before.stdout)
.trim()
.parse::<i32>()
.expect("Failed to parse initial commit count");
hermetic_git_env();
empty_commit();
let after = Command::new("git")
.args(["rev-list", "--count", "HEAD"])
.envs(hermetic_git_env_vars())
.output()
.expect("Failed to run git command");
assert!(after.status.success(), "Failed to get final commit count");
let count_after = String::from_utf8_lossy(&after.stdout)
.trim()
.parse::<i32>()
.expect("Failed to parse final commit count");
assert_eq!(
count_after,
count_before + 1,
"Should have created exactly one new commit"
);
set_current_dir(original_dir).expect("Failed to restore directory");
}
#[test]
fn test_dir_guard_restores_directory() {
let original_dir = env::current_dir().unwrap();
let tempdir = tempdir().unwrap();
{
let _guard = DirGuard::new(tempdir.path());
assert_eq!(
env::current_dir().unwrap(),
tempdir.path().canonicalize().unwrap()
);
}
assert_eq!(env::current_dir().unwrap(), original_dir);
}
#[test]
fn test_hermetic_git_env_sets_all_vars() {
env::remove_var("GIT_CONFIG_NOSYSTEM");
env::remove_var("GIT_CONFIG_GLOBAL");
env::remove_var("GIT_AUTHOR_NAME");
env::remove_var("GIT_AUTHOR_EMAIL");
env::remove_var("GIT_COMMITTER_NAME");
env::remove_var("GIT_COMMITTER_EMAIL");
hermetic_git_env();
assert_eq!(env::var("GIT_CONFIG_NOSYSTEM").unwrap(), "true");
assert_eq!(env::var("GIT_CONFIG_GLOBAL").unwrap(), "/dev/null");
assert_eq!(env::var("GIT_AUTHOR_NAME").unwrap(), "testuser");
assert_eq!(
env::var("GIT_AUTHOR_EMAIL").unwrap(),
"testuser@example.com"
);
assert_eq!(env::var("GIT_COMMITTER_NAME").unwrap(), "testuser");
assert_eq!(
env::var("GIT_COMMITTER_EMAIL").unwrap(),
"testuser@example.com"
);
}
}