use std::io::{self, ErrorKind};
use camino::{Utf8Path, Utf8PathBuf};
use rstest::{fixture, rstest};
use tempfile::TempDir;
use super::{Reading, classify_unreadable, confined_to, nearest_existing};
use crate::select::policy::PathKind;
fn at(path: &str) -> Utf8PathBuf { Utf8PathBuf::from(path) }
fn as_path(directory: &TempDir) -> io::Result<Utf8PathBuf> {
Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).map_err(|path| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("non-UTF-8 temporary path: {}", path.display()),
)
})
}
#[test_macros::allow_fixture_expansion_lints]
#[fixture]
fn temp_root() -> io::Result<TempDir> { tempfile::tempdir() }
fn write(root: &Utf8Path, name: &str, content: &str) -> io::Result<()> {
let path = root.join(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, content)
}
#[rstest]
#[case(ErrorKind::PermissionDenied)]
#[case(ErrorKind::NotADirectory)]
fn a_failure_that_is_not_absence_is_reported_unchanged(#[case] kind: ErrorKind) {
let path = at("/repo/docs/guide.md");
let error = classify_unreadable(&path, io::Error::from(kind))
.expect_err("a file that is present but unreadable is not absent");
assert_eq!(error.kind(), kind, "the cause is reported unchanged");
}
#[rstest]
fn a_read_that_fails_under_a_file_is_not_an_absence(
temp_root: io::Result<TempDir>,
) -> io::Result<()> {
let temp_root = temp_root?;
let root = as_path(&temp_root)?;
let gone = root.join("gone.md");
assert_eq!(
classify_unreadable(&gone, io::Error::from(ErrorKind::NotFound)).ok(),
Some(PathKind::Missing),
"gone.md is gone, and that is the answer the selection has a rule for"
);
write(&root, "blocker", "not a directory\n")?;
let through_a_file = root.join("blocker/guide.md");
let error = classify_unreadable(&through_a_file, io::Error::from(ErrorKind::NotFound))
.expect_err("a path through a file is not an absence");
assert_eq!(
error.kind(),
ErrorKind::NotADirectory,
"the kind Unix reports for it, reported on every platform"
);
Ok(())
}
#[rstest]
fn a_read_that_fails_where_the_whole_path_is_gone_is_an_absence(
temp_root: io::Result<TempDir>,
) -> io::Result<()> {
let temp_root = temp_root?;
let root = as_path(&temp_root)?;
let path = root.join("gone/sub/guide.md");
assert_eq!(
classify_unreadable(&path, io::Error::from(ErrorKind::NotFound)).ok(),
Some(PathKind::Missing),
"a subtree that is gone is absent, not unreachable"
);
Ok(())
}
#[test]
fn a_failure_reading_an_ancestor_is_reported_rather_than_walked_past() {
let docs = at("/repo/docs");
let mut read_paths = Vec::new();
let error = nearest_existing(Some(docs.as_path()), |ancestor| {
read_paths.push(ancestor.to_owned());
if ancestor == docs.as_path() {
Reading::Failed(io::Error::from(ErrorKind::PermissionDenied))
} else {
Reading::Directory
}
})
.expect("an ancestor that cannot be read is a failure, not an absence");
assert_eq!(
error.kind(),
ErrorKind::PermissionDenied,
"the cause is reported unchanged"
);
assert_eq!(
read_paths,
vec![docs],
"the walk stops at the ancestor it could not read rather than climbing to /repo, which is \
a directory and would answer that nothing is missing"
);
}
#[rstest]
fn a_root_that_does_not_exist_confines_nothing(temp_root: io::Result<TempDir>) -> io::Result<()> {
let temp_root = temp_root?;
let root = as_path(&temp_root)?;
let gone = root.join("gone");
assert!(
!confined_to(&gone, &at("/canonical/guide.md"))
.expect("an absent root is not a failure to read it"),
"a root that cannot be resolved confines nothing"
);
Ok(())
}
#[rstest]
fn a_root_that_cannot_be_resolved_is_reported(temp_root: io::Result<TempDir>) -> io::Result<()> {
let temp_root = temp_root?;
let root = as_path(&temp_root)?;
write(&root, "blocker", "not a directory\n")?;
let unreachable = root.join("blocker/sub");
let error = confined_to(&unreachable, &at("/canonical/guide.md"))
.expect_err("a root that cannot be resolved is a failure, not confinement");
assert_eq!(
error.kind(),
ErrorKind::NotADirectory,
"a root behind a file is present, not absent: {error:?}"
);
Ok(())
}