#![allow(dead_code)]
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
pub struct TestGit {
repo_path: PathBuf,
}
impl TestGit {
pub fn new(repo_path: impl Into<PathBuf>) -> Self {
Self {
repo_path: repo_path.into(),
}
}
pub fn init(&self) -> Result<()> {
Command::new("git")
.arg("init")
.current_dir(&self.repo_path)
.output()
.context("Failed to initialize git repository")?;
Ok(())
}
pub fn config_user(&self) -> Result<()> {
Command::new("git")
.args(["config", "user.email", "test@ccpm.example"])
.current_dir(&self.repo_path)
.output()
.context("Failed to configure git user email")?;
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(&self.repo_path)
.output()
.context("Failed to configure git user name")?;
Ok(())
}
pub fn add_all(&self) -> Result<()> {
Command::new("git")
.args(["add", "."])
.current_dir(&self.repo_path)
.output()
.context("Failed to add files to git")?;
Ok(())
}
pub fn commit(&self, message: &str) -> Result<()> {
Command::new("git")
.args(["commit", "-m", message])
.current_dir(&self.repo_path)
.output()
.context("Failed to create git commit")?;
Ok(())
}
pub fn tag(&self, tag_name: &str) -> Result<()> {
Command::new("git")
.args(["tag", tag_name])
.current_dir(&self.repo_path)
.output()
.context(format!("Failed to create tag: {}", tag_name))?;
Ok(())
}
pub fn create_branch(&self, branch_name: &str) -> Result<()> {
Command::new("git")
.args(["checkout", "-b", branch_name])
.current_dir(&self.repo_path)
.output()
.context(format!("Failed to create branch: {}", branch_name))?;
Ok(())
}
pub fn set_head(&self, branch_name: &str) -> Result<()> {
Command::new("git")
.args([
"symbolic-ref",
"HEAD",
&format!("refs/heads/{}", branch_name),
])
.current_dir(&self.repo_path)
.output()
.context(format!("Failed to set HEAD to branch: {}", branch_name))?;
Ok(())
}
pub fn get_commit_hash(&self) -> Result<String> {
let output = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&self.repo_path)
.output()
.context("Failed to get commit hash")?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub fn get_head_sha(&self) -> Result<String> {
self.get_commit_hash()
}
pub fn clone_to_bare(&self, target_path: &Path) -> Result<()> {
Command::new("git")
.args([
"clone",
"--bare",
self.repo_path.to_str().unwrap(),
target_path.to_str().unwrap(),
])
.output()
.context("Failed to create bare repository")?;
Ok(())
}
}
pub struct TestProject {
_temp_dir: TempDir, project_dir: PathBuf,
cache_dir: PathBuf,
sources_dir: PathBuf,
}
impl TestProject {
pub fn new() -> Result<Self> {
let temp_dir = TempDir::new()?;
let project_dir = temp_dir.path().join("project");
let cache_dir = temp_dir.path().join(".ccpm").join("cache");
let sources_dir = temp_dir.path().join("sources");
fs::create_dir_all(&project_dir)?;
fs::create_dir_all(&cache_dir)?;
fs::create_dir_all(&sources_dir)?;
Ok(Self {
_temp_dir: temp_dir,
project_dir,
cache_dir,
sources_dir,
})
}
pub fn project_path(&self) -> &Path {
&self.project_dir
}
pub fn cache_path(&self) -> &Path {
&self.cache_dir
}
pub fn sources_path(&self) -> &Path {
&self.sources_dir
}
pub fn write_manifest(&self, content: &str) -> Result<()> {
let manifest_path = self.project_dir.join("ccpm.toml");
fs::write(&manifest_path, content)
.with_context(|| format!("Failed to write manifest to {:?}", manifest_path))?;
Ok(())
}
pub fn write_lockfile(&self, content: &str) -> Result<()> {
let lockfile_path = self.project_dir.join("ccpm.lock");
fs::write(&lockfile_path, content)
.with_context(|| format!("Failed to write lockfile to {:?}", lockfile_path))?;
Ok(())
}
pub fn read_lockfile(&self) -> Result<String> {
let lockfile_path = self.project_dir.join("ccpm.lock");
fs::read_to_string(&lockfile_path)
.with_context(|| format!("Failed to read lockfile from {:?}", lockfile_path))
}
pub fn create_local_resource(&self, path: &str, content: &str) -> Result<()> {
let resource_path = self.project_dir.join(path);
if let Some(parent) = resource_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&resource_path, content)?;
Ok(())
}
pub fn create_source_repo(&self, name: &str) -> Result<TestSourceRepo> {
let source_dir = self.sources_dir.join(name);
fs::create_dir_all(&source_dir)?;
let git = TestGit::new(&source_dir);
git.init()?;
git.config_user()?;
Ok(TestSourceRepo {
path: source_dir,
git,
})
}
pub fn run_ccpm(&self, args: &[&str]) -> Result<CommandOutput> {
self.run_ccpm_with_env(args, &[])
}
pub fn run_ccpm_with_env(
&self,
args: &[&str],
env_vars: &[(&str, &str)],
) -> Result<CommandOutput> {
let ccpm_binary = env!("CARGO_BIN_EXE_ccpm");
let mut cmd = Command::new(ccpm_binary);
cmd.args(args)
.current_dir(&self.project_dir)
.env("CCPM_CACHE_DIR", &self.cache_dir)
.env("CCPM_TEST_MODE", "true")
.env("NO_COLOR", "1");
for (key, value) in env_vars {
cmd.env(key, value);
}
let output = cmd.output().context("Failed to run ccpm command")?;
Ok(CommandOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
success: output.status.success(),
code: output.status.code(),
})
}
}
pub struct TestSourceRepo {
pub path: PathBuf,
pub git: TestGit,
}
impl TestSourceRepo {
pub fn add_resource(&self, resource_type: &str, name: &str, content: &str) -> Result<()> {
let resource_dir = self.path.join(resource_type);
fs::create_dir_all(&resource_dir)?;
let file_path = resource_dir.join(format!("{}.md", name));
fs::write(&file_path, content)?;
Ok(())
}
pub fn create_standard_resources(&self) -> Result<()> {
self.add_resource("agents", "test-agent", "# Test Agent\n\nA test agent")?;
self.add_resource(
"snippets",
"test-snippet",
"# Test Snippet\n\nA test snippet",
)?;
self.add_resource(
"commands",
"test-command",
"# Test Command\n\nA test command",
)?;
Ok(())
}
pub fn commit_all(&self, message: &str) -> Result<()> {
self.git.add_all()?;
self.git.commit(message)?;
Ok(())
}
pub fn tag_version(&self, version: &str) -> Result<()> {
self.git.tag(version)?;
Ok(())
}
pub fn file_url(&self) -> String {
let path_str = self.path.display().to_string().replace('\\', "/");
format!("file://{}", path_str)
}
pub fn to_bare_repo(&self, target_path: &Path) -> Result<PathBuf> {
let output = Command::new("git")
.args([
"clone",
"--bare",
self.path.to_str().unwrap(),
target_path.to_str().unwrap(),
])
.output()
.context("Failed to create bare repository")?;
if !output.status.success() {
return Err(anyhow::anyhow!(
"Failed to create bare repository: {}",
String::from_utf8_lossy(&output.stderr)
));
}
let verify_output = Command::new("git")
.args(["tag", "-l"])
.current_dir(target_path)
.output()
.context("Failed to verify bare repository")?;
if !verify_output.status.success() {
return Err(anyhow::anyhow!(
"Bare repository verification failed: {}",
String::from_utf8_lossy(&verify_output.stderr)
));
}
Ok(target_path.to_path_buf())
}
pub fn bare_file_url(&self, sources_dir: &Path) -> Result<String> {
let bare_name = format!(
"{}.git",
self.path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("repo")
);
let bare_path = sources_dir.join(bare_name);
self.to_bare_repo(&bare_path)?;
let path_str = bare_path.display().to_string().replace('\\', "/");
Ok(format!("file://{}", path_str))
}
}
pub struct CommandOutput {
pub stdout: String,
pub stderr: String,
pub success: bool,
pub code: Option<i32>,
}
impl CommandOutput {
pub fn assert_success(&self) -> &Self {
assert!(
self.success,
"Command failed with code {:?}\nStderr: {}",
self.code, self.stderr
);
self
}
pub fn assert_stdout_contains(&self, text: &str) -> &Self {
assert!(
self.stdout.contains(text),
"Expected stdout to contain '{}'\nActual stdout: {}",
text,
self.stdout
);
self
}
}
pub struct FileAssert;
impl FileAssert {
pub fn exists(path: impl AsRef<Path>) {
let path = path.as_ref();
assert!(path.exists(), "Expected file to exist: {}", path.display());
}
pub fn not_exists(path: impl AsRef<Path>) {
let path = path.as_ref();
assert!(
!path.exists(),
"Expected file to not exist: {}",
path.display()
);
}
pub fn contains(path: impl AsRef<Path>, expected: &str) {
let path = path.as_ref();
let content = fs::read_to_string(path)
.unwrap_or_else(|e| panic!("Failed to read file {}: {}", path.display(), e));
assert!(
content.contains(expected),
"Expected file {} to contain '{}'\nActual content: {}",
path.display(),
expected,
content
);
}
pub fn equals(path: impl AsRef<Path>, expected: &str) {
let path = path.as_ref();
let content = fs::read_to_string(path)
.unwrap_or_else(|e| panic!("Failed to read file {}: {}", path.display(), e));
assert_eq!(
content,
expected,
"File {} content mismatch",
path.display()
);
}
}
pub struct DirAssert;
impl DirAssert {
pub fn exists(path: impl AsRef<Path>) {
let path = path.as_ref();
assert!(
path.is_dir(),
"Expected directory to exist: {}",
path.display()
);
}
pub fn contains_file(dir: impl AsRef<Path>, file_name: &str) {
let path = dir.as_ref().join(file_name);
assert!(
path.exists(),
"Expected directory {} to contain file '{}'",
dir.as_ref().display(),
file_name
);
}
pub fn is_empty(path: impl AsRef<Path>) {
let path = path.as_ref();
let entries = fs::read_dir(path)
.unwrap_or_else(|e| panic!("Failed to read directory {}: {}", path.display(), e))
.count();
assert_eq!(
entries,
0,
"Expected directory {} to be empty, but it contains {} entries",
path.display(),
entries
);
}
}