use anyhow::{Context, Result};
use futures::future::try_join_all;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
pub fn ensure_dir(path: &Path) -> Result<()> {
let safe_path = crate::utils::platform::windows_long_path(path);
if !safe_path.exists() {
fs::create_dir_all(&safe_path).with_context(|| {
let platform_help = if crate::utils::platform::is_windows() {
"On Windows: Check that the path length is < 260 chars or that long path support is enabled"
} else {
"Check directory permissions and path validity"
};
format!("Failed to create directory: {}\\n\\n{}", path.display(), platform_help)
})?;
} else if !safe_path.is_dir() {
return Err(anyhow::anyhow!("Path exists but is not a directory: {}", path.display()));
}
Ok(())
}
pub fn safe_write(path: &Path, content: &str) -> Result<()> {
atomic_write(path, content.as_bytes())
}
pub fn atomic_write(path: &Path, content: &[u8]) -> Result<()> {
use std::io::Write;
let safe_path = crate::utils::platform::windows_long_path(path);
if let Some(parent) = safe_path.parent() {
ensure_dir(parent)?;
}
let temp_path = safe_path.with_extension("tmp");
{
let mut file = fs::File::create(&temp_path).with_context(|| {
let platform_help = if crate::utils::platform::is_windows() {
"On Windows: Check file permissions, path length, and that directory exists"
} else {
"Check file permissions and that directory exists"
};
format!("Failed to create temp file: {}\\n\\n{}", temp_path.display(), platform_help)
})?;
file.write_all(content)
.with_context(|| format!("Failed to write to temp file: {}", temp_path.display()))?;
file.sync_all().with_context(|| "Failed to sync file to disk")?;
}
fs::rename(&temp_path, &safe_path)
.with_context(|| format!("Failed to rename temp file to: {}", safe_path.display()))?;
Ok(())
}
pub fn copy_dir(src: &Path, dst: &Path) -> Result<()> {
ensure_dir(dst)?;
for entry in
fs::read_dir(src).with_context(|| format!("Failed to read directory: {}", src.display()))?
{
let entry = entry?;
let file_type = entry.file_type()?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if file_type.is_dir() {
copy_dir(&src_path, &dst_path)?;
} else if file_type.is_file() {
fs::copy(&src_path, &dst_path).with_context(|| {
format!("Failed to copy file from {} to {}", src_path.display(), dst_path.display())
})?;
}
}
Ok(())
}
pub fn remove_dir_all(path: &Path) -> Result<()> {
if path.exists() {
fs::remove_dir_all(path)
.with_context(|| format!("Failed to remove directory: {}", path.display()))?;
}
Ok(())
}
#[must_use]
pub fn normalize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::CurDir => {} std::path::Component::ParentDir => {
components.pop(); }
c => components.push(c),
}
}
components.iter().collect()
}
#[must_use]
pub fn is_safe_path(base: &Path, path: &Path) -> bool {
let normalized_base = normalize_path(base);
let normalized_path = if path.is_absolute() {
normalize_path(path)
} else {
normalize_path(&base.join(path))
};
normalized_path.starts_with(normalized_base)
}
pub fn find_files(dir: &Path, pattern: &str) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
find_files_recursive(dir, pattern, &mut files)?;
Ok(files)
}
fn find_files_recursive(dir: &Path, pattern: &str, files: &mut Vec<PathBuf>) -> Result<()> {
if !dir.is_dir() {
return Ok(());
}
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
find_files_recursive(&path, pattern, files)?;
} else if path.is_file()
&& let Some(name) = path.file_name()
&& name.to_string_lossy().contains(pattern)
{
files.push(path);
}
}
Ok(())
}
pub fn dir_size(path: &Path) -> Result<u64> {
let mut size = 0;
for entry in fs::read_dir(path)? {
let entry = entry?;
let metadata = entry.metadata()?;
if metadata.is_dir() {
size += dir_size(&entry.path())?;
} else {
size += metadata.len();
}
}
Ok(size)
}
pub async fn get_directory_size(path: &Path) -> Result<u64> {
let path = path.to_path_buf();
tokio::task::spawn_blocking(move || dir_size(&path))
.await
.context("Failed to join directory size calculation task")?
}
pub fn ensure_parent_dir(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
ensure_dir(parent)?;
}
Ok(())
}
pub fn ensure_dir_exists(path: &Path) -> Result<()> {
ensure_dir(path)
}
pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
copy_dir(src, dst)
}
pub fn find_project_root(start: &Path) -> Result<PathBuf> {
let mut current = start.canonicalize().unwrap_or_else(|_| start.to_path_buf());
loop {
if current.join("agpm.toml").exists() {
return Ok(current);
}
if !current.pop() {
return Err(anyhow::anyhow!(
"No agpm.toml found in current directory or any parent directory"
));
}
}
}
pub fn get_global_config_path() -> Result<PathBuf> {
let home = crate::utils::platform::get_home_dir()?;
Ok(home.join(".config").join("agpm").join("config.toml"))
}
pub struct TempDir {
path: PathBuf,
}
impl TempDir {
pub fn new(prefix: &str) -> Result<Self> {
let temp_dir = std::env::temp_dir();
let unique_name = format!("agpm_{}_{}", prefix, uuid::Uuid::new_v4());
let path = temp_dir.join(unique_name);
ensure_dir(&path)?;
Ok(Self {
path,
})
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = remove_dir_all(&self.path);
}
}
pub fn calculate_checksum(path: &Path) -> Result<String> {
let content = fs::read(path)
.with_context(|| format!("Failed to read file for checksum: {}", path.display()))?;
let mut hasher = Sha256::new();
hasher.update(&content);
let result = hasher.finalize();
Ok(hex::encode(result))
}
pub async fn calculate_checksums_parallel(paths: &[PathBuf]) -> Result<Vec<(PathBuf, String)>> {
if paths.is_empty() {
return Ok(Vec::new());
}
let mut tasks = Vec::new();
for (index, path) in paths.iter().enumerate() {
let path = path.clone();
let task = tokio::task::spawn_blocking(move || {
calculate_checksum(&path).map(|checksum| (index, path, checksum))
});
tasks.push(task);
}
let results = try_join_all(tasks).await.context("Failed to join checksum calculation tasks")?;
let mut successes = Vec::new();
let mut errors = Vec::new();
for result in results {
match result {
Ok((index, path, checksum)) => successes.push((index, path, checksum)),
Err(e) => errors.push(e),
}
}
if !errors.is_empty() {
let error_msgs: Vec<String> =
errors.into_iter().map(|error| format!(" {error}")).collect();
return Err(anyhow::anyhow!(
"Failed to calculate checksums for {} files:\n{}",
error_msgs.len(),
error_msgs.join("\n")
));
}
successes.sort_by_key(|(index, _, _)| *index);
let ordered_results: Vec<(PathBuf, String)> =
successes.into_iter().map(|(_, path, checksum)| (path, checksum)).collect();
Ok(ordered_results)
}
pub async fn copy_files_parallel(sources_and_destinations: &[(PathBuf, PathBuf)]) -> Result<()> {
if sources_and_destinations.is_empty() {
return Ok(());
}
let mut tasks = Vec::new();
for (src, dst) in sources_and_destinations {
let src = src.clone();
let dst = dst.clone();
let task = tokio::task::spawn_blocking(move || {
if let Some(parent) = dst.parent() {
ensure_dir(parent)?;
}
fs::copy(&src, &dst).with_context(|| {
format!("Failed to copy file from {} to {}", src.display(), dst.display())
})?;
Ok::<_, anyhow::Error>((src, dst))
});
tasks.push(task);
}
let results = try_join_all(tasks).await.context("Failed to join file copy tasks")?;
let mut errors = Vec::new();
for result in results {
if let Err(e) = result {
errors.push(e);
}
}
if !errors.is_empty() {
let error_msgs: Vec<String> =
errors.into_iter().map(|error| format!(" {error}")).collect();
return Err(anyhow::anyhow!(
"Failed to copy {} files:\n{}",
error_msgs.len(),
error_msgs.join("\n")
));
}
Ok(())
}
pub async fn atomic_write_multiple(files: &[(PathBuf, Vec<u8>)]) -> Result<()> {
if files.is_empty() {
return Ok(());
}
let mut tasks = Vec::new();
for (path, content) in files {
let path = path.clone();
let content = content.clone();
let task =
tokio::task::spawn_blocking(move || atomic_write(&path, &content).map(|()| path));
tasks.push(task);
}
let results = try_join_all(tasks).await.context("Failed to join atomic write tasks")?;
let mut errors = Vec::new();
for result in results {
if let Err(e) = result {
errors.push(e);
}
}
if !errors.is_empty() {
let error_msgs: Vec<String> =
errors.into_iter().map(|error| format!(" {error}")).collect();
return Err(anyhow::anyhow!(
"Failed to write {} files:\n{}",
error_msgs.len(),
error_msgs.join("\n")
));
}
Ok(())
}
pub async fn copy_dirs_parallel(sources_and_destinations: &[(PathBuf, PathBuf)]) -> Result<()> {
if sources_and_destinations.is_empty() {
return Ok(());
}
let mut tasks = Vec::new();
for (src, dst) in sources_and_destinations {
let src = src.clone();
let dst = dst.clone();
let task = tokio::task::spawn_blocking(move || copy_dir(&src, &dst).map(|()| (src, dst)));
tasks.push(task);
}
let results = try_join_all(tasks).await.context("Failed to join directory copy tasks")?;
let mut errors = Vec::new();
for result in results {
if let Err(e) = result {
errors.push(e);
}
}
if !errors.is_empty() {
let error_msgs: Vec<String> =
errors.into_iter().map(|error| format!(" {error}")).collect();
return Err(anyhow::anyhow!(
"Failed to copy {} directories:\n{}",
error_msgs.len(),
error_msgs.join("\n")
));
}
Ok(())
}
pub async fn read_files_parallel(paths: &[PathBuf]) -> Result<Vec<(PathBuf, String)>> {
if paths.is_empty() {
return Ok(Vec::new());
}
let mut tasks = Vec::new();
for (index, path) in paths.iter().enumerate() {
let path = path.clone();
let task = tokio::task::spawn_blocking(move || {
fs::read_to_string(&path).map(|content| (index, path, content))
});
tasks.push(task);
}
let results = try_join_all(tasks).await.context("Failed to join file read tasks")?;
let mut successes = Vec::new();
let mut errors = Vec::new();
for result in results {
match result {
Ok((index, path, content)) => successes.push((index, path, content)),
Err(e) => errors.push(e),
}
}
if !errors.is_empty() {
let error_msgs: Vec<String> =
errors.into_iter().map(|error| format!(" {error}")).collect();
return Err(anyhow::anyhow!(
"Failed to read {} files:\n{}",
error_msgs.len(),
error_msgs.join("\n")
));
}
successes.sort_by_key(|(index, _, _)| *index);
let ordered_results: Vec<(PathBuf, String)> =
successes.into_iter().map(|(_, path, content)| (path, content)).collect();
Ok(ordered_results)
}
pub fn read_text_file(path: &Path) -> Result<String> {
fs::read_to_string(path).with_context(|| format!("Failed to read file: {}", path.display()))
}
pub fn write_text_file(path: &Path, content: &str) -> Result<()> {
safe_write(path, content).with_context(|| format!("Failed to write file: {}", path.display()))
}
pub fn read_json_file<T>(path: &Path) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let content = read_text_file(path)?;
serde_json::from_str(&content)
.with_context(|| format!("Failed to parse JSON from file: {}", path.display()))
}
pub fn write_json_file<T>(path: &Path, data: &T, pretty: bool) -> Result<()>
where
T: serde::Serialize,
{
let json = if pretty {
serde_json::to_string_pretty(data)?
} else {
serde_json::to_string(data)?
};
write_text_file(path, &json)
.with_context(|| format!("Failed to write JSON file: {}", path.display()))
}
pub fn read_toml_file<T>(path: &Path) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let content = read_text_file(path)?;
toml::from_str(&content)
.with_context(|| format!("Failed to parse TOML from file: {}", path.display()))
}
pub fn write_toml_file<T>(path: &Path, data: &T) -> Result<()>
where
T: serde::Serialize,
{
let toml = toml::to_string_pretty(data)
.with_context(|| format!("Failed to serialize data to TOML for: {}", path.display()))?;
write_text_file(path, &toml)
.with_context(|| format!("Failed to write TOML file: {}", path.display()))
}
pub fn read_yaml_file<T>(path: &Path) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let content = read_text_file(path)?;
serde_yaml::from_str(&content)
.with_context(|| format!("Failed to parse YAML from file: {}", path.display()))
}
pub fn write_yaml_file<T>(path: &Path, data: &T) -> Result<()>
where
T: serde::Serialize,
{
let yaml = serde_yaml::to_string(data)
.with_context(|| format!("Failed to serialize data to YAML for: {}", path.display()))?;
write_text_file(path, &yaml)
.with_context(|| format!("Failed to write YAML file: {}", path.display()))
}
pub fn create_temp_file(prefix: &str, content: &str) -> Result<tempfile::TempPath> {
let temp_file = tempfile::Builder::new().prefix(prefix).suffix(".tmp").tempfile()?;
let path = temp_file.into_temp_path();
write_text_file(&path, content)?;
Ok(path)
}
pub fn file_exists_and_readable(path: &Path) -> bool {
path.exists() && path.is_file() && fs::metadata(path).is_ok()
}
pub fn get_modified_time(path: &Path) -> Result<std::time::SystemTime> {
let metadata = fs::metadata(path)
.with_context(|| format!("Failed to get metadata for: {}", path.display()))?;
metadata
.modified()
.with_context(|| format!("Failed to get modification time for: {}", path.display()))
}
pub fn compare_file_times(path1: &Path, path2: &Path) -> Result<std::cmp::Ordering> {
let time1 = get_modified_time(path1)?;
let time2 = get_modified_time(path2)?;
Ok(time1.cmp(&time2))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_ensure_dir() {
let temp = tempdir().unwrap();
let test_dir = temp.path().join("test_dir");
assert!(!test_dir.exists());
ensure_dir(&test_dir).unwrap();
assert!(test_dir.exists());
assert!(test_dir.is_dir());
}
#[test]
fn test_normalize_path() {
let path = Path::new("/foo/./bar/../baz");
let normalized = normalize_path(path);
assert_eq!(normalized, PathBuf::from("/foo/baz"));
}
#[test]
fn test_is_safe_path() {
let base = Path::new("/home/user/project");
assert!(is_safe_path(base, Path::new("subdir/file.txt")));
assert!(is_safe_path(base, Path::new("./subdir/file.txt")));
assert!(!is_safe_path(base, Path::new("../other/file.txt")));
assert!(!is_safe_path(base, Path::new("/etc/passwd")));
}
#[test]
fn test_safe_write() {
let temp = tempdir().unwrap();
let file_path = temp.path().join("test.txt");
safe_write(&file_path, "test content").unwrap();
let content = std::fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "test content");
}
#[test]
fn test_safe_write_creates_parent_dirs() {
let temp = tempdir().unwrap();
let file_path = temp.path().join("subdir").join("test.txt");
safe_write(&file_path, "test content").unwrap();
assert!(file_path.exists());
let content = std::fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "test content");
}
#[test]
fn test_copy_dir() {
let temp = tempdir().unwrap();
let src = temp.path().join("src");
let dst = temp.path().join("dst");
ensure_dir(&src).unwrap();
ensure_dir(&src.join("subdir")).unwrap();
std::fs::write(src.join("file1.txt"), "content1").unwrap();
std::fs::write(src.join("subdir/file2.txt"), "content2").unwrap();
copy_dir(&src, &dst).unwrap();
assert!(dst.join("file1.txt").exists());
assert!(dst.join("subdir/file2.txt").exists());
let content1 = std::fs::read_to_string(dst.join("file1.txt")).unwrap();
assert_eq!(content1, "content1");
let content2 = std::fs::read_to_string(dst.join("subdir/file2.txt")).unwrap();
assert_eq!(content2, "content2");
}
#[test]
fn test_remove_dir_all() {
let temp = tempdir().unwrap();
let dir = temp.path().join("to_remove");
ensure_dir(&dir).unwrap();
std::fs::write(dir.join("file.txt"), "content").unwrap();
assert!(dir.exists());
remove_dir_all(&dir).unwrap();
assert!(!dir.exists());
}
#[test]
fn test_remove_dir_all_nonexistent() {
let temp = tempdir().unwrap();
let dir = temp.path().join("nonexistent");
remove_dir_all(&dir).unwrap();
}
#[test]
fn test_find_files() {
let temp = tempdir().unwrap();
let root = temp.path();
std::fs::write(root.join("test.rs"), "").unwrap();
std::fs::write(root.join("main.rs"), "").unwrap();
ensure_dir(&root.join("src")).unwrap();
std::fs::write(root.join("src/lib.rs"), "").unwrap();
std::fs::write(root.join("src/test.txt"), "").unwrap();
let files = find_files(root, ".rs").unwrap();
assert_eq!(files.len(), 3);
let files = find_files(root, "test").unwrap();
assert_eq!(files.len(), 2);
}
#[test]
fn test_dir_size() {
let temp = tempdir().unwrap();
let dir = temp.path();
std::fs::write(dir.join("file1.txt"), "12345").unwrap();
std::fs::write(dir.join("file2.txt"), "123456789").unwrap();
ensure_dir(&dir.join("subdir")).unwrap();
std::fs::write(dir.join("subdir/file3.txt"), "abc").unwrap();
let size = dir_size(dir).unwrap();
assert_eq!(size, 17); }
#[test]
fn test_temp_dir() {
let temp_dir = TempDir::new("test").unwrap();
let path = temp_dir.path().to_path_buf();
assert!(path.exists());
assert!(path.is_dir());
std::fs::write(path.join("test.txt"), "test").unwrap();
assert!(path.join("test.txt").exists());
drop(temp_dir);
assert!(!path.exists());
}
#[test]
fn test_ensure_parent_dir() {
let temp = tempdir().unwrap();
let file_path = temp.path().join("parent").join("child").join("file.txt");
ensure_parent_dir(&file_path).unwrap();
assert!(file_path.parent().unwrap().exists());
}
#[test]
fn test_ensure_dir_exists() {
let temp = tempdir().unwrap();
let test_dir = temp.path().join("test_dir_alias");
assert!(!test_dir.exists());
ensure_dir_exists(&test_dir).unwrap();
assert!(test_dir.exists());
}
#[test]
fn test_copy_dir_all() {
let temp = tempdir().unwrap();
let src = temp.path().join("src_alias");
let dst = temp.path().join("dst_alias");
ensure_dir(&src).unwrap();
std::fs::write(src.join("file.txt"), "content").unwrap();
copy_dir_all(&src, &dst).unwrap();
assert!(dst.join("file.txt").exists());
}
#[test]
fn test_find_project_root() {
let temp = tempdir().unwrap();
let project = temp.path().join("project");
let subdir = project.join("src").join("subdir");
ensure_dir(&subdir).unwrap();
std::fs::write(project.join("agpm.toml"), "[sources]").unwrap();
let root = find_project_root(&subdir).unwrap();
assert_eq!(root.canonicalize().unwrap(), project.canonicalize().unwrap());
}
#[test]
fn test_find_project_root_not_found() {
let temp = tempdir().unwrap();
let result = find_project_root(temp.path());
assert!(result.is_err());
}
#[test]
fn test_get_global_config_path() {
let config_path = get_global_config_path().unwrap();
assert!(config_path.to_string_lossy().contains(".config"));
assert!(config_path.to_string_lossy().contains("agpm"));
}
#[test]
fn test_calculate_checksum() {
let temp = tempdir().unwrap();
let file = temp.path().join("checksum_test.txt");
std::fs::write(&file, "test content").unwrap();
let checksum = calculate_checksum(&file).unwrap();
assert!(!checksum.is_empty());
assert_eq!(checksum.len(), 64); }
#[tokio::test]
async fn test_calculate_checksums_parallel() {
let temp = tempdir().unwrap();
let file1 = temp.path().join("file1.txt");
let file2 = temp.path().join("file2.txt");
std::fs::write(&file1, "content1").unwrap();
std::fs::write(&file2, "content2").unwrap();
let paths = vec![file1.clone(), file2.clone()];
let results = calculate_checksums_parallel(&paths).await.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].0, file1);
assert_eq!(results[1].0, file2);
assert!(!results[0].1.is_empty());
assert!(!results[1].1.is_empty());
}
#[tokio::test]
async fn test_calculate_checksums_parallel_empty() {
let results = calculate_checksums_parallel(&[]).await.unwrap();
assert!(results.is_empty());
}
#[tokio::test]
async fn test_copy_files_parallel() {
let temp = tempdir().unwrap();
let src1 = temp.path().join("src1.txt");
let src2 = temp.path().join("src2.txt");
let dst1 = temp.path().join("dst").join("dst1.txt");
let dst2 = temp.path().join("dst").join("dst2.txt");
std::fs::write(&src1, "content1").unwrap();
std::fs::write(&src2, "content2").unwrap();
let pairs = vec![(src1.clone(), dst1.clone()), (src2.clone(), dst2.clone())];
copy_files_parallel(&pairs).await.unwrap();
assert!(dst1.exists());
assert!(dst2.exists());
assert_eq!(std::fs::read_to_string(&dst1).unwrap(), "content1");
assert_eq!(std::fs::read_to_string(&dst2).unwrap(), "content2");
}
#[tokio::test]
async fn test_atomic_write_multiple() {
let temp = tempdir().unwrap();
let file1 = temp.path().join("atomic1.txt");
let file2 = temp.path().join("atomic2.txt");
let files =
vec![(file1.clone(), b"content1".to_vec()), (file2.clone(), b"content2".to_vec())];
atomic_write_multiple(&files).await.unwrap();
assert!(file1.exists());
assert!(file2.exists());
assert_eq!(std::fs::read_to_string(&file1).unwrap(), "content1");
assert_eq!(std::fs::read_to_string(&file2).unwrap(), "content2");
}
#[tokio::test]
async fn test_copy_dirs_parallel() {
let temp = tempdir().unwrap();
let src1 = temp.path().join("src1");
let src2 = temp.path().join("src2");
let dst1 = temp.path().join("dst1");
let dst2 = temp.path().join("dst2");
ensure_dir(&src1).unwrap();
ensure_dir(&src2).unwrap();
std::fs::write(src1.join("file1.txt"), "content1").unwrap();
std::fs::write(src2.join("file2.txt"), "content2").unwrap();
let pairs = vec![(src1.clone(), dst1.clone()), (src2.clone(), dst2.clone())];
copy_dirs_parallel(&pairs).await.unwrap();
assert!(dst1.join("file1.txt").exists());
assert!(dst2.join("file2.txt").exists());
}
#[tokio::test]
async fn test_read_files_parallel() {
let temp = tempdir().unwrap();
let file1 = temp.path().join("read1.txt");
let file2 = temp.path().join("read2.txt");
std::fs::write(&file1, "content1").unwrap();
std::fs::write(&file2, "content2").unwrap();
let paths = vec![file1.clone(), file2.clone()];
let results = read_files_parallel(&paths).await.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].0, file1);
assert_eq!(results[0].1, "content1");
assert_eq!(results[1].0, file2);
assert_eq!(results[1].1, "content2");
}
#[test]
fn test_ensure_dir_on_file() {
let temp = tempdir().unwrap();
let file_path = temp.path().join("file.txt");
std::fs::write(&file_path, "content").unwrap();
let result = ensure_dir(&file_path);
assert!(result.is_err());
}
#[tokio::test]
async fn test_parallel_operations_empty() {
let result = calculate_checksums_parallel(&[]).await;
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
let result = copy_files_parallel(&[]).await;
assert!(result.is_ok());
let result = atomic_write_multiple(&[]).await;
assert!(result.is_ok());
let result = copy_dirs_parallel(&[]).await;
assert!(result.is_ok());
let result = read_files_parallel(&[]).await;
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_atomic_write_basic() {
let temp = tempdir().unwrap();
let file = temp.path().join("atomic.txt");
atomic_write(&file, b"test content").unwrap();
assert_eq!(std::fs::read_to_string(&file).unwrap(), "test content");
}
#[test]
fn test_atomic_write_overwrites() {
let temp = tempdir().unwrap();
let file = temp.path().join("atomic.txt");
atomic_write(&file, b"initial").unwrap();
assert_eq!(std::fs::read_to_string(&file).unwrap(), "initial");
atomic_write(&file, b"updated").unwrap();
assert_eq!(std::fs::read_to_string(&file).unwrap(), "updated");
}
#[test]
fn test_atomic_write_creates_parent() {
let temp = tempdir().unwrap();
let file = temp.path().join("deep").join("nested").join("atomic.txt");
atomic_write(&file, b"nested content").unwrap();
assert!(file.exists());
assert_eq!(std::fs::read_to_string(&file).unwrap(), "nested content");
}
#[test]
fn test_safe_copy_file() {
let temp = tempdir().unwrap();
let src = temp.path().join("source.txt");
let dst = temp.path().join("dest.txt");
std::fs::write(&src, "test content").unwrap();
std::fs::copy(&src, &dst).unwrap();
assert_eq!(std::fs::read_to_string(&dst).unwrap(), "test content");
}
#[test]
fn test_copy_with_parent_creation() {
let temp = tempdir().unwrap();
let src = temp.path().join("source.txt");
let dst = temp.path().join("subdir").join("dest.txt");
std::fs::write(&src, "test content").unwrap();
ensure_parent_dir(&dst).unwrap();
std::fs::copy(&src, &dst).unwrap();
assert!(dst.exists());
assert_eq!(std::fs::read_to_string(&dst).unwrap(), "test content");
}
#[test]
fn test_copy_nonexistent_source() {
let temp = tempdir().unwrap();
let src = temp.path().join("nonexistent.txt");
let dst = temp.path().join("dest.txt");
let result = std::fs::copy(&src, &dst);
assert!(result.is_err());
}
#[test]
fn test_normalize_path_complex() {
assert_eq!(normalize_path(Path::new("/")), PathBuf::from("/"));
assert_eq!(normalize_path(Path::new("/foo/bar")), PathBuf::from("/foo/bar"));
assert_eq!(normalize_path(Path::new("/foo/./bar")), PathBuf::from("/foo/bar"));
assert_eq!(normalize_path(Path::new("/foo/../bar")), PathBuf::from("/bar"));
assert_eq!(normalize_path(Path::new("/foo/bar/..")), PathBuf::from("/foo"));
assert_eq!(normalize_path(Path::new("foo/./bar")), PathBuf::from("foo/bar"));
assert_eq!(normalize_path(Path::new("./foo/bar")), PathBuf::from("foo/bar"));
}
#[test]
fn test_is_safe_path_edge_cases() {
let base = Path::new("/home/user/project");
assert!(is_safe_path(base, Path::new("")));
assert!(is_safe_path(base, Path::new(".")));
assert!(is_safe_path(base, Path::new("./nested/./path")));
assert!(!is_safe_path(base, Path::new("..")));
assert!(!is_safe_path(base, Path::new("../../etc")));
assert!(!is_safe_path(base, Path::new("/absolute/path")));
if cfg!(windows) {
assert!(!is_safe_path(base, Path::new("C:\\Windows")));
}
}
#[test]
fn test_safe_write_readonly_parent() {
if std::env::var("CI").is_ok() {
return;
}
let temp = tempdir().unwrap();
let readonly_dir = temp.path().join("readonly");
ensure_dir(&readonly_dir).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&readonly_dir).unwrap().permissions();
perms.set_mode(0o555); std::fs::set_permissions(&readonly_dir, perms).unwrap();
let file = readonly_dir.join("test.txt");
let result = safe_write(&file, "test");
assert!(result.is_err());
let mut perms = std::fs::metadata(&readonly_dir).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&readonly_dir, perms).unwrap();
}
}
#[test]
#[cfg(unix)]
fn test_remove_dir_all_symlink() {
let temp = tempdir().unwrap();
let target = temp.path().join("target");
let link = temp.path().join("link");
ensure_dir(&target).unwrap();
std::fs::write(target.join("important.txt"), "data").unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
remove_dir_all(&link).unwrap();
assert!(target.exists());
assert!(target.join("important.txt").exists());
}
#[test]
fn test_find_files_with_patterns() {
let temp = tempdir().unwrap();
let root = temp.path();
std::fs::write(root.join("README.md"), "").unwrap();
std::fs::write(root.join("test.MD"), "").unwrap(); std::fs::write(root.join("file.txt"), "").unwrap();
ensure_dir(&root.join("hidden")).unwrap();
std::fs::write(root.join("hidden/.secret.md"), "").unwrap();
let files = find_files(root, ".md").unwrap();
assert_eq!(files.len(), 2);
let files = find_files(root, ".MD").unwrap();
assert_eq!(files.len(), 1);
let files = find_files(root, "test").unwrap();
assert_eq!(files.len(), 1);
let files = find_files(root, "secret").unwrap();
assert_eq!(files.len(), 1);
}
#[test]
fn test_dir_size_edge_cases() {
let temp = tempdir().unwrap();
let empty_dir = temp.path().join("empty");
ensure_dir(&empty_dir).unwrap();
assert_eq!(dir_size(&empty_dir).unwrap(), 0);
let nonexistent = temp.path().join("nonexistent");
let result = dir_size(&nonexistent);
assert!(result.is_err());
#[cfg(unix)]
{
let dir = temp.path().join("with_symlink");
ensure_dir(&dir).unwrap();
std::fs::write(dir.join("file.txt"), "12345").unwrap();
let target = temp.path().join("target");
std::fs::write(&target, "123456789").unwrap();
std::os::unix::fs::symlink(&target, dir.join("link")).unwrap();
let size = dir_size(&dir).unwrap();
assert!(size >= 5);
assert!(size < 1_000_000);
}
}
#[test]
fn test_temp_dir_custom_prefix() {
let temp1 = TempDir::new("prefix1").unwrap();
let temp2 = TempDir::new("prefix2").unwrap();
assert!(temp1.path().to_string_lossy().contains("prefix1"));
assert!(temp2.path().to_string_lossy().contains("prefix2"));
let path1 = temp1.path().to_path_buf();
let path2 = temp2.path().to_path_buf();
assert_ne!(path1, path2);
assert!(path1.exists());
assert!(path2.exists());
}
#[test]
fn test_ensure_parent_dir_edge_cases() {
let temp = tempdir().unwrap();
let root_file = if cfg!(windows) {
PathBuf::from("C:\\file.txt")
} else {
PathBuf::from("/file.txt")
};
ensure_parent_dir(&root_file).unwrap();
let current_file = PathBuf::from("file.txt");
ensure_parent_dir(¤t_file).unwrap();
let existing = temp.path().join("file.txt");
ensure_parent_dir(&existing).unwrap();
ensure_parent_dir(&existing).unwrap(); }
#[test]
fn test_calculate_checksum_edge_cases() {
let temp = tempdir().unwrap();
let empty = temp.path().join("empty.txt");
std::fs::write(&empty, "").unwrap();
let checksum = calculate_checksum(&empty).unwrap();
assert_eq!(checksum.len(), 64);
assert_eq!(checksum, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
let nonexistent = temp.path().join("nonexistent.txt");
let result = calculate_checksum(&nonexistent);
assert!(result.is_err());
let large = temp.path().join("large.txt");
let large_content = vec![b'a'; 1024 * 1024];
std::fs::write(&large, &large_content).unwrap();
let checksum = calculate_checksum(&large).unwrap();
assert_eq!(checksum.len(), 64);
}
#[tokio::test]
async fn test_calculate_checksums_parallel_errors() {
let temp = tempdir().unwrap();
let valid = temp.path().join("valid.txt");
let invalid = temp.path().join("invalid.txt");
std::fs::write(&valid, "content").unwrap();
let paths = vec![valid.clone(), invalid.clone()];
let result = calculate_checksums_parallel(&paths).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_copy_files_parallel_errors() {
let temp = tempdir().unwrap();
let src = temp.path().join("nonexistent.txt");
let dst = temp.path().join("dest.txt");
let pairs = vec![(src, dst)];
let result = copy_files_parallel(&pairs).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_atomic_write_multiple_partial_failure() {
let temp = tempdir().unwrap();
let valid_path = temp.path().join("valid.txt");
let invalid_base = temp.path().join("not_a_directory.txt");
std::fs::write(&invalid_base, "this is a file").unwrap();
let invalid_path = invalid_base.join("impossible_file.txt");
let files =
vec![(valid_path.clone(), b"content".to_vec()), (invalid_path, b"fail".to_vec())];
let result = atomic_write_multiple(&files).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_read_files_parallel_mixed() {
let temp = tempdir().unwrap();
let valid = temp.path().join("valid.txt");
let invalid = temp.path().join("invalid.txt");
std::fs::write(&valid, "content").unwrap();
let paths = vec![valid, invalid];
let result = read_files_parallel(&paths).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_copy_dirs_parallel_errors() {
let temp = tempdir().unwrap();
let src = temp.path().join("nonexistent");
let dst = temp.path().join("dest");
let pairs = vec![(src, dst)];
let result = copy_dirs_parallel(&pairs).await;
assert!(result.is_err());
}
#[test]
fn test_copy_dir_with_permissions() {
let temp = tempdir().unwrap();
let src = temp.path().join("src");
let dst = temp.path().join("dst");
ensure_dir(&src).unwrap();
std::fs::write(src.join("file.txt"), "content").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(src.join("file.txt")).unwrap().permissions();
perms.set_mode(0o644);
std::fs::set_permissions(src.join("file.txt"), perms).unwrap();
}
copy_dir(&src, &dst).unwrap();
assert!(dst.join("file.txt").exists());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::metadata(dst.join("file.txt")).unwrap().permissions();
assert_eq!(perms.mode() & 0o777, 0o644);
}
}
#[test]
fn test_find_project_root_multiple_markers() {
let temp = tempdir().unwrap();
let root = temp.path().join("project");
let subproject = root.join("subproject");
let deep = subproject.join("src");
ensure_dir(&deep).unwrap();
std::fs::write(root.join("agpm.toml"), "[sources]").unwrap();
std::fs::write(subproject.join("agpm.toml"), "[sources]").unwrap();
let found = find_project_root(&deep).unwrap();
assert_eq!(found.canonicalize().unwrap(), subproject.canonicalize().unwrap());
}
#[test]
fn test_get_cache_dir_from_config() {
let cache_dir = crate::config::get_cache_dir().unwrap();
assert!(!cache_dir.as_os_str().is_empty());
if std::env::var("AGPM_CACHE_DIR").is_err() {
assert!(cache_dir.to_string_lossy().contains("agpm"));
}
}
}