use std::process::Command;
use tempfile::TempDir;
fn get_grubble_bin() -> String {
if let Ok(output) = Command::new("which").arg("grubble").output() {
if output.status.success() {
return String::from_utf8_lossy(&output.stdout).trim().to_string();
}
}
let cargo_target = std::env::var("CARGO_MANIFEST_DIR")
.ok()
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().unwrap())
.join("target")
.join("debug")
.join("grubble");
if cargo_target.exists() {
return cargo_target.to_string_lossy().to_string();
}
let cargo_target_release = std::env::var("CARGO_MANIFEST_DIR")
.ok()
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().unwrap())
.join("target")
.join("release")
.join("grubble");
if cargo_target_release.exists() {
return cargo_target_release.to_string_lossy().to_string();
}
panic!("Could not find grubble binary. Build with 'cargo build' first.");
}
fn setup_test_repo() -> (TempDir, Command) {
let temp_dir = TempDir::new().unwrap();
Command::new("git")
.args(["init"])
.current_dir(&temp_dir)
.output()
.expect("Failed to init git");
Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(&temp_dir)
.output()
.expect("Failed to set git email");
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(&temp_dir)
.output()
.expect("Failed to set git name");
Command::new("git")
.args(["commit", "--allow-empty", "-m", "chore: initial commit"])
.current_dir(&temp_dir)
.output()
.expect("Failed to create initial commit");
Command::new("git")
.args(["tag", "v1.0.0"])
.current_dir(&temp_dir)
.output()
.expect("Failed to create tag");
let mut cmd = Command::new(get_grubble_bin());
cmd.current_dir(&temp_dir);
(temp_dir, cmd)
}
#[test]
fn test_bump_type_no_commits() {
let (_dir, mut cmd) = setup_test_repo();
cmd.arg("--bump-type");
let output = cmd.output().expect("Failed to run grubble");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "none");
}
#[test]
fn test_bump_type_patch() {
let (dir, mut cmd) = setup_test_repo();
Command::new("git")
.args(["commit", "--allow-empty", "-m", "fix: resolve bug"])
.current_dir(&dir)
.output()
.expect("Failed to create fix commit");
cmd.arg("--bump-type");
let output = cmd.output().expect("Failed to run grubble");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "patch");
}
#[test]
fn test_bump_type_minor() {
let (dir, mut cmd) = setup_test_repo();
Command::new("git")
.args(["commit", "--allow-empty", "-m", "feat: add new feature"])
.current_dir(&dir)
.output()
.expect("Failed to create feat commit");
cmd.arg("--bump-type");
let output = cmd.output().expect("Failed to run grubble");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "minor");
}
#[test]
fn test_bump_type_major() {
let (dir, mut cmd) = setup_test_repo();
Command::new("git")
.args(["commit", "--allow-empty", "-m", "feat!: breaking change"])
.current_dir(&dir)
.output()
.expect("Failed to create breaking commit");
cmd.arg("--bump-type");
let output = cmd.output().expect("Failed to run grubble");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "major");
}
#[test]
fn test_dry_run_no_bump_exit_code() {
let (_dir, mut cmd) = setup_test_repo();
cmd.arg("--dry-run");
let output = cmd.output().expect("Failed to run grubble");
assert_eq!(output.status.code(), Some(0));
}
#[test]
fn test_dry_run_bump_needed_exit_code() {
let (dir, mut cmd) = setup_test_repo();
Command::new("git")
.args(["commit", "--allow-empty", "-m", "fix: resolve bug"])
.current_dir(&dir)
.output()
.expect("Failed to create fix commit");
cmd.arg("--dry-run");
let output = cmd.output().expect("Failed to run grubble");
assert_eq!(output.status.code(), Some(0));
}
#[test]
fn test_dry_run_does_not_modify_files() {
let (dir, mut cmd) = setup_test_repo();
Command::new("git")
.args(["commit", "--allow-empty", "-m", "fix: resolve bug"])
.current_dir(&dir)
.output()
.expect("Failed to create fix commit");
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nversion = \"1.0.0\"\n",
)
.unwrap();
cmd.arg("--dry-run");
cmd.arg("--preset");
cmd.arg("rust");
let output = cmd.output().expect("Failed to run grubble");
assert_eq!(output.status.code(), Some(0));
let cargo_content = std::fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
assert!(cargo_content.contains("version = \"1.0.0\""));
}
#[test]
fn test_dry_run_does_not_create_tags() {
let (dir, mut cmd) = setup_test_repo();
Command::new("git")
.args(["commit", "--allow-empty", "-m", "fix: resolve bug"])
.current_dir(&dir)
.output()
.expect("Failed to create fix commit");
cmd.arg("--dry-run");
let output = cmd.output().expect("Failed to run grubble");
assert_eq!(output.status.code(), Some(0));
let tags_output = Command::new("git")
.args(["tag", "-l"])
.current_dir(&dir)
.output()
.expect("Failed to list tags");
let tags = String::from_utf8_lossy(&tags_output.stdout);
assert_eq!(tags.trim(), "v1.0.0"); }
#[test]
fn test_dry_run_verbose_output() {
let (dir, mut cmd) = setup_test_repo();
Command::new("git")
.args(["commit", "--allow-empty", "-m", "fix: resolve bug"])
.current_dir(&dir)
.output()
.expect("Failed to create fix commit");
Command::new("git")
.args(["commit", "--allow-empty", "-m", "feat: add feature"])
.current_dir(&dir)
.output()
.expect("Failed to create feat commit");
cmd.arg("--dry-run");
let output = cmd.output().expect("Failed to run grubble");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Current version"));
assert!(stdout.contains("Version bump"));
}
#[test]
fn test_normal_run_no_bump_exit_code() {
let (_dir, mut cmd) = setup_test_repo();
let output = cmd.output().expect("Failed to run grubble");
assert_eq!(output.status.code(), Some(0));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!stderr.starts_with("Error:"));
}
#[test]
fn test_raw_no_further_bump_exit_code() {
let (dir, mut cmd) = setup_test_repo();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nversion = \"1.0.0\"\n",
)
.unwrap();
cmd.arg("--raw");
cmd.arg("--preset");
cmd.arg("rust");
let output = cmd.output().expect("Failed to run grubble");
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "1.0.0");
}
#[test]
fn test_error_exit_code() {
let (dir, mut cmd) = setup_test_repo();
cmd.arg("--preset");
cmd.arg("rust");
Command::new("git")
.args(["commit", "--allow-empty", "-m", "fix: something"])
.current_dir(&dir)
.output()
.expect("Failed to create fix commit");
let output = cmd.output().expect("Failed to run grubble");
assert_ne!(output.status.code(), Some(0));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Error:"),
"expected error on stderr, got: {}",
stderr
);
}
#[test]
fn test_raw_with_rust_preset_reads_cargo_toml() {
let (dir, mut cmd) = setup_test_repo();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nversion = \"0.1.0\"\n",
)
.unwrap();
cmd.arg("--raw");
cmd.arg("--preset");
cmd.arg("rust");
let output = cmd.output().expect("Failed to run grubble");
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "0.1.0");
}
#[test]
fn test_raw_with_node_preset_reads_package_json() {
let (dir, mut cmd) = setup_test_repo();
std::fs::write(
dir.path().join("package.json"),
"{\"name\": \"demo\", \"version\": \"2.3.4\"}\n",
)
.unwrap();
cmd.arg("--raw");
cmd.arg("--preset");
cmd.arg("node");
let output = cmd.output().expect("Failed to run grubble");
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "2.3.4");
}
#[test]
fn test_raw_with_git_preset_unchanged() {
let (_dir, mut cmd) = setup_test_repo();
cmd.arg("--raw");
cmd.arg("--preset");
cmd.arg("git");
let output = cmd.output().expect("Failed to run grubble");
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "1.0.0");
}
#[test]
fn test_bump_type_json_output() {
let (dir, mut cmd) = setup_test_repo();
Command::new("git")
.args(["commit", "--allow-empty", "-m", "feat: add thing"])
.current_dir(&dir)
.output()
.expect("Failed to create feat commit");
cmd.arg("--bump-type");
cmd.arg("--output");
cmd.arg("json");
let output = cmd.output().expect("Failed to run grubble");
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
let stdout = stdout.trim();
let parsed: serde_json::Value = serde_json::from_str(stdout)
.unwrap_or_else(|e| panic!("stdout is not valid JSON '{}': {}", stdout, e));
assert_eq!(parsed["bump_type"], "minor");
assert!(parsed["current_version"].is_string());
assert!(parsed["triggering_commits"].is_array());
assert!(parsed["unknown_commits"].is_array());
}
#[test]
fn test_raw_json_output() {
let (dir, mut cmd) = setup_test_repo();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nversion = \"1.0.0\"\n",
)
.unwrap();
cmd.arg("--raw");
cmd.arg("--preset");
cmd.arg("rust");
cmd.arg("--output");
cmd.arg("json");
let output = cmd.output().expect("Failed to run grubble");
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
let stdout = stdout.trim();
let parsed: serde_json::Value = serde_json::from_str(stdout)
.unwrap_or_else(|e| panic!("stdout is not valid JSON '{}': {}", stdout, e));
assert_eq!(parsed["version"], "1.0.0");
assert_eq!(parsed["preset"], "rust");
}
#[test]
fn test_json_output_invalid_with_dry_run() {
let (_dir, mut cmd) = setup_test_repo();
cmd.arg("--dry-run");
cmd.arg("--output");
cmd.arg("json");
let output = cmd.output().expect("Failed to run grubble");
assert_ne!(output.status.code(), Some(0));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Invalid configuration") || stderr.contains("--output json"),
"expected validation error on stderr, got: {}",
stderr
);
}
#[test]
fn test_json_output_invalid_with_normal_run() {
let (_dir, mut cmd) = setup_test_repo();
cmd.arg("--output");
cmd.arg("json");
let output = cmd.output().expect("Failed to run grubble");
assert_ne!(output.status.code(), Some(0));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Invalid configuration") || stderr.contains("--output json"),
"expected validation error on stderr, got: {}",
stderr
);
}