use std::path::PathBuf;
use pi_async_fs::{
CreateTargetEvidence, FileNamespace, LocalFileNamespace,
};
use pi_result::ErrorKind;
#[test]
fn test_create_dir_is_lazy_and_strictly_rejects_existing_targets() {
let temporary = tempfile::tempdir().expect("temporary directory");
let namespace = LocalFileNamespace::new();
let target = temporary.path().join("lazy");
let future = namespace.create_dir(&target);
std::fs::create_dir(&target).expect("external creation before first poll");
let failure = async_global_executor::block_on(future)
.expect_err("an existing target must be rejected");
assert_eq!(failure.error().current_context(), &ErrorKind::AlreadyExists);
assert_eq!(
failure.target_evidence(),
&CreateTargetEvidence::NotCreatedByOperation
);
assert!(target.is_dir());
}
#[test]
fn test_create_dir_creates_only_one_level_and_reports_precommit_failures() {
let temporary = tempfile::tempdir().expect("temporary directory");
let namespace = LocalFileNamespace::new();
let target = temporary.path().join("single");
async_global_executor::block_on(namespace.create_dir(&target))
.expect("single directory creation");
assert!(target.is_dir());
let nested = temporary.path().join("missing-parent").join("child");
let failure = async_global_executor::block_on(namespace.create_dir(&nested))
.expect_err("strict creation must not create an ancestor");
assert_eq!(failure.error().current_context(), &ErrorKind::NotFound);
assert_eq!(
failure.target_evidence(),
&CreateTargetEvidence::NotCreatedByOperation
);
assert!(!temporary.path().join("missing-parent").exists());
}
#[test]
fn test_create_dir_rejects_every_existing_final_entry_without_modifying_it() {
let temporary = tempfile::tempdir().expect("temporary directory");
let namespace = LocalFileNamespace::new();
let directory = temporary.path().join("directory");
let file = temporary.path().join("file");
std::fs::create_dir(&directory).expect("existing directory");
std::fs::write(&file, b"preserve").expect("existing file");
for target in [&directory, &file] {
let failure = async_global_executor::block_on(
namespace.create_dir(target),
)
.expect_err("every existing target kind must be rejected");
assert_eq!(failure.error().current_context(), &ErrorKind::AlreadyExists);
assert_eq!(
failure.target_evidence(),
&CreateTargetEvidence::NotCreatedByOperation
);
}
assert_eq!(std::fs::read(&file).expect("preserved file"), b"preserve");
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let link = temporary.path().join("link");
let dangling = temporary.path().join("dangling");
symlink(&directory, &link).expect("directory symlink");
symlink(temporary.path().join("absent"), &dangling)
.expect("dangling symlink");
for target in [&link, &dangling] {
let failure = async_global_executor::block_on(
namespace.create_dir(target),
)
.expect_err("a final symlink is an existing entry");
assert_eq!(
failure.error().current_context(),
&ErrorKind::AlreadyExists
);
assert_eq!(
failure.target_evidence(),
&CreateTargetEvidence::NotCreatedByOperation
);
}
assert!(std::fs::symlink_metadata(&link)
.expect("link remains")
.file_type()
.is_symlink());
assert!(std::fs::symlink_metadata(&dangling)
.expect("dangling link remains")
.file_type()
.is_symlink());
}
}
#[test]
fn test_create_dir_converges_across_clones_and_parent_aliases() {
let temporary = tempfile::tempdir().expect("temporary directory");
let real_parent = temporary.path().join("real");
std::fs::create_dir(&real_parent).expect("real parent");
#[cfg(unix)]
let targets = {
use std::os::unix::fs::symlink;
let alias = temporary.path().join("alias");
symlink(&real_parent, &alias).expect("parent alias");
vec![real_parent.join("child"), alias.join("child")]
};
#[cfg(not(unix))]
let targets = vec![real_parent.join("child"), real_parent.join("child")];
let namespace = LocalFileNamespace::new();
let results = async_global_executor::block_on(async move {
let tasks = targets
.into_iter()
.enumerate()
.map(|(index, target)| {
let namespace = if index == 0 {
namespace.clone()
} else {
LocalFileNamespace::new()
};
async_global_executor::spawn(async move {
namespace.create_dir(&target).await
})
})
.collect::<Vec<_>>();
let mut results = Vec::with_capacity(tasks.len());
for task in tasks {
results.push(task.await);
}
results
});
assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1);
for failure in results.into_iter().filter_map(Result::err) {
assert!(matches!(
failure.error().current_context(),
ErrorKind::AlreadyExists | ErrorKind::Conflict
));
assert_eq!(
failure.target_evidence(),
&CreateTargetEvidence::NotCreatedByOperation
);
}
assert!(real_parent.join("child").is_dir());
}
#[test]
fn test_create_dir_all_is_lazy_idempotent_and_obeys_inclusive_target() {
let temporary = tempfile::tempdir().expect("temporary directory");
let namespace = LocalFileNamespace::new();
let target = temporary.path().join("one").join("two").join("three");
let future = namespace.create_dir_all(&target);
assert!(!temporary.path().join("one").exists());
async_global_executor::block_on(future).expect("recursive creation");
assert!(target.is_dir());
async_global_executor::block_on(namespace.create_dir_all(&target))
.expect("an existing directory tree is an idempotent success");
async_global_executor::block_on(
namespace.create_dir_all(&PathBuf::new()),
)
.expect("an empty local path denotes the existing relative base");
}
#[test]
fn test_create_dir_all_reports_confirmed_partial_ancestors_without_rollback() {
let temporary = tempfile::tempdir().expect("temporary directory");
let namespace = LocalFileNamespace::new();
let blocker = temporary.path().join("blocker");
std::fs::write(&blocker, b"preserve").expect("blocking file");
let created = temporary.path().join("created");
let target = created.join("..").join("blocker").join("child");
let failure = async_global_executor::block_on(
namespace.create_dir_all(&target),
)
.expect_err("a file in the requested hierarchy must stop creation");
assert_eq!(failure.error().current_context(), &ErrorKind::AlreadyExists);
assert_eq!(failure.confirmed_created(), std::slice::from_ref(&created));
assert!(failure.uncertain_targets().is_empty());
assert!(created.is_dir());
assert_eq!(std::fs::read(&blocker).expect("blocker remains"), b"preserve");
assert!(!blocker.join("child").exists());
}
#[test]
fn test_create_dir_all_accepts_directory_symlinks_but_rejects_other_objects() {
let temporary = tempfile::tempdir().expect("temporary directory");
let namespace = LocalFileNamespace::new();
let file = temporary.path().join("file");
std::fs::write(&file, b"preserve").expect("blocking file");
let failure = async_global_executor::block_on(
namespace.create_dir_all(&file),
)
.expect_err("a final regular file cannot satisfy directory semantics");
assert_eq!(failure.error().current_context(), &ErrorKind::AlreadyExists);
assert!(failure.confirmed_created().is_empty());
assert!(failure.uncertain_targets().is_empty());
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let real = temporary.path().join("real");
let link = temporary.path().join("link");
std::fs::create_dir(&real).expect("real directory");
symlink(&real, &link).expect("directory symlink");
let child = link.join("child");
async_global_executor::block_on(namespace.create_dir_all(&child))
.expect("an existing directory symlink follows normal path rules");
assert!(real.join("child").is_dir());
assert!(std::fs::symlink_metadata(&link)
.expect("link remains")
.file_type()
.is_symlink());
}
}
#[test]
fn test_create_dir_all_concurrent_calls_converge_without_hiding_active_conflicts() {
let temporary = tempfile::tempdir().expect("temporary directory");
let target = temporary.path().join("shared").join("deep").join("leaf");
let namespace = LocalFileNamespace::new();
let results = async_global_executor::block_on(async move {
let tasks = (0..16)
.map(|_| {
let namespace = namespace.clone();
let target = target.clone();
async_global_executor::spawn(async move {
namespace.create_dir_all(&target).await
})
})
.collect::<Vec<_>>();
let mut results = Vec::with_capacity(tasks.len());
for task in tasks {
results.push(task.await);
}
(target, results)
});
let target = results.0;
assert!(target.is_dir());
assert!(results.1.iter().any(Result::is_ok));
for failure in results.1.into_iter().filter_map(Result::err) {
assert_eq!(failure.error().current_context(), &ErrorKind::Conflict);
assert!(failure.uncertain_targets().is_empty());
for created in failure.confirmed_created() {
assert!(created.is_dir());
assert!(target.starts_with(created));
}
}
async_global_executor::block_on(
LocalFileNamespace::new().create_dir_all(&target),
)
.expect("retry after all active reservations are released is idempotent");
}