Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ lint-clippy: ## Run rustdoc and Clippy with warnings denied
RUSTFLAGS="$${RUSTFLAGS:+$$RUSTFLAGS }-D warnings $(POLONIUS_FLAGS)" $(CARGO) clippy $(CLIPPY_FLAGS)

lint-whitaker: ## Run the Whitaker Dylint suite with warnings denied
RUSTFLAGS="$${RUSTFLAGS:+$$RUSTFLAGS }-D warnings $(POLONIUS_FLAGS)" $(WHITAKER) --all --no-deps --package netsuke-build -- --all-targets --all-features
DYLINT_TOML="$$(cat dylint.toml)" RUSTFLAGS="$${RUSTFLAGS:+$$RUSTFLAGS }-D warnings $(POLONIUS_FLAGS)" $(WHITAKER) --all --no-deps --package netsuke-build -- --all-targets --all-features
# Run from the crate directory as well so Whitaker loads the narrow
# `test_support::fs` exemption from test_support/dylint.toml.
cd test_support && DYLINT_TOML="$$(cat dylint.toml)" RUSTFLAGS="$${RUSTFLAGS:+$$RUSTFLAGS }-D warnings $(POLONIUS_FLAGS)" $(WHITAKER) --all --no-deps --package test_support -- --all-targets --all-features
Expand Down
27 changes: 23 additions & 4 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1712,10 +1712,13 @@ is not obvious from the name:
absent or unreadable path returns `false` rather than surfacing the
underlying metadata error. Fixture code must use this wrapper for directory
predicates rather than calling `std::fs::metadata(...).is_dir()` or
`Path::is_dir` directly. `test_support/src/manifest.rs` is an existing caller:
`ensure_manifest_exists` uses it both to reject a directory where a manifest
file is expected, and to accept a destination directory that is already
present.
`Path::is_dir` directly.
- `PathState` and `inspect_path(path) -> io::Result<PathState>` provide a
fallible target-state probe. `PathState::Absent` means metadata returned
`NotFound`, `PathState::Directory` means the target is a directory, and
`PathState::NonDirectory` means it exists but is not a directory. The probe
follows symlinks, so a dangling symlink is `Absent` even when its directory
entry exists; metadata errors other than `NotFound` are propagated.
- `try_is_file(path) -> io::Result<bool>` is the fallible counterpart to the
boolean predicates: `Ok(true)` when the path is a regular file, `Ok(false)`
when it is absent (`NotFound` is folded into the boolean result), and `Err`
Expand Down Expand Up @@ -1749,6 +1752,22 @@ ambient boundary stays where the lint expects it. Prefer that shape — pass in
what the operation needs and keep the handle here — over widening an exclusion
to a module that wants a raw `File`.

### `test_support::ensure_manifest_exists`

`test_support::ensure_manifest_exists` (`test_support/src/manifest.rs`) never
overwrites an existing non-directory target. If another actor creates a
non-directory target after the initial existence check but before persistence,
no-clobber persistence leaves that target unchanged and returns its path, which
satisfies the existence contract. An existing directory, including one created
at the controlled pre-persist point, returns `io::ErrorKind::IsADirectory`.

When a manifest is missing, its generated contents are written to a temporary
file staged in the destination directory before persistence. The tests inject
the pre-persist action to cover controlled creation orderings; they do not claim
to model arbitrary scheduler or filesystem interleavings. The fallible
`test_support::fs::inspect_path` probe treats `NotFound` as absence and
propagates every other metadata error.

### Shared Makefile contract helpers

`tests/support/makefile.rs` is a shared module for integration tests that
Expand Down
48 changes: 48 additions & 0 deletions test_support/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ use std::io;
use std::path::Path;
use std::time::SystemTime;

/// The state observed when inspecting a filesystem path.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathState {
/// The path does not exist.
Absent,
/// The path exists and is a directory.
Directory,
/// The path exists and is not a directory.
NonDirectory,
}

/// Write `contents` to `path`, creating or truncating the file.
///
/// # Errors
Expand Down Expand Up @@ -117,6 +128,43 @@ pub fn is_dir(path: impl AsRef<Path>) -> bool {
fs::metadata(path).is_ok_and(|metadata| metadata.is_dir())
}

/// Inspect whether `path` is absent, a directory, or another target.
///
/// Only [`io::ErrorKind::NotFound`] becomes [`PathState::Absent`]. All other
/// metadata failures are returned so callers do not mistake an unreadable path
/// for an absent one.
/// [`std::fs::metadata`] follows symbolic links, so a dangling symbolic link is
/// reported as [`PathState::Absent`] even though its directory entry exists.
///
/// # Errors
///
/// Propagates the underlying metadata failure for any error other than
/// [`io::ErrorKind::NotFound`].
///
/// # Examples
///
/// ```
/// use test_support::fs::{PathState, inspect_path};
///
/// let dir = tempfile::tempdir().expect("create tempdir");
/// let file = dir.path().join("file");
/// test_support::fs::write(&file, "contents").expect("write file");
/// assert_eq!(inspect_path(&file).expect("inspect file"), PathState::NonDirectory);
/// assert_eq!(inspect_path(dir.path()).expect("inspect directory"), PathState::Directory);
/// assert_eq!(
/// inspect_path(dir.path().join("absent")).expect("inspect absent path"),
/// PathState::Absent
/// );
/// ```
pub fn inspect_path(path: impl AsRef<Path>) -> io::Result<PathState> {
match fs::metadata(path) {
Ok(metadata) if metadata.is_dir() => Ok(PathState::Directory),
Ok(_) => Ok(PathState::NonDirectory),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(PathState::Absent),
Err(error) => Err(error),
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Return `true` when `path` is a regular file, surfacing unexpected errors.
///
/// Unlike `Path::is_file`, an I/O failure other than `NotFound` is propagated
Expand Down
43 changes: 35 additions & 8 deletions test_support/src/fs_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
//! `module_max_lines` cap; included from there via `#[path]` so the tests
//! stay a child module of `fs`.

use super::{create_dir_all, try_is_file, write};
use super::{PathState, create_dir_all, inspect_path, try_is_file, write};
use rstest::{fixture, rstest};
use std::io;
use std::path::Path;

/// Temporary workspace for the filesystem helper tests.
///
Expand All @@ -21,6 +22,24 @@ fn temp_dir() -> TempDir {
tempfile::tempdir()
}

fn assert_traversal_through_file_propagates_non_not_found<T>(
temp_dir: TempDir,
inspect: impl FnOnce(&Path) -> io::Result<T>,
) -> anyhow::Result<()> {
let temp = temp_dir?;
let file = temp.path().join("regular-file");
write(&file, b"fixture")?;

let Err(error) = inspect(&file.join("child")) else {
anyhow::bail!("traversing through a regular file should fail");
};
anyhow::ensure!(
error.kind() != io::ErrorKind::NotFound,
"traversal through a file should not be reported as absence, got {error:?}"
);
Ok(())
}

#[rstest]
fn try_is_file_reports_a_regular_file_as_a_file(temp_dir: TempDir) -> anyhow::Result<()> {
let temp = temp_dir?;
Expand Down Expand Up @@ -58,20 +77,28 @@ fn try_is_file_reports_a_directory_as_not_a_file(temp_dir: TempDir) -> anyhow::R

#[rstest]
fn try_is_file_propagates_errors_other_than_not_found(temp_dir: TempDir) -> anyhow::Result<()> {
assert_traversal_through_file_propagates_non_not_found(temp_dir, |path| try_is_file(path))
}

#[rstest]
fn inspect_path_distinguishes_absent_directory_and_non_directory(
temp_dir: TempDir,
) -> anyhow::Result<()> {
let temp = temp_dir?;
let file = temp.path().join("regular-file");
write(&file, b"fixture")?;

let Err(error) = try_is_file(file.join("child")) else {
anyhow::bail!("traversing through a regular file should fail");
};
anyhow::ensure!(
error.kind() != io::ErrorKind::NotFound,
"traversal through a file should not be reported as absence, got {error:?}"
);
anyhow::ensure!(inspect_path(temp.path())? == PathState::Directory);
anyhow::ensure!(inspect_path(&file)? == PathState::NonDirectory);
anyhow::ensure!(inspect_path(temp.path().join("absent"))? == PathState::Absent);
Ok(())
}

#[rstest]
fn inspect_path_propagates_errors_other_than_not_found(temp_dir: TempDir) -> anyhow::Result<()> {
assert_traversal_through_file_propagates_non_not_found(temp_dir, |path| inspect_path(path))
}

#[rstest]
fn create_dir_all_accepts_an_existing_directory(temp_dir: TempDir) -> io::Result<()> {
let temp = temp_dir?;
Expand Down
Loading
Loading