Loading page…
Rust walkthroughs
Loading page…
predicates::path::exists simplify filesystem assertions compared to manual std::fs::metadata checks?predicates::path::exists provides a declarative, composable predicate for testing filesystem existence that abstracts away the manual pattern of calling std::fs::metadata and matching on its Result to determine if a path exists. Rather than writing the boilerplate of checking for Ok(_) (exists) versus Err(e) where e.kind() == std::io::ErrorKind::NotFound (doesn't exist), you express the assertion directly as predicates::path::exists().eval(&path) which returns a simple boolean. This becomes particularly valuable in test assertions where you want readable failure messages—assert!(predicates::path::exists().eval(&path)) produces a generic boolean failure, but the predicates crate integrates with assertion frameworks to automatically generate descriptions like "expected path to exist but it does not" rather than just "assertion failed: false is not true".
use std::fs;
use std::io;
use std::path::Path;
fn path_exists_manual(path: &Path) -> bool {
match fs::metadata(path) {
Ok(_) => true,
Err(e) if e.kind() == io::ErrorKind::NotFound => false,
Err(_) => false, // Other errors: permission denied, etc.
}
}
fn main() {
let existing_path = Path::new("/etc/hosts");
let nonexistent_path = Path::new("/nonexistent/file/path");
println!("{} exists: {}", existing_path.display(), path_exists_manual(existing_path));
println!("{} exists: {}", nonexistent_path.display(), path_exists_manual(nonexistent_path));
// The manual approach requires:
// 1. Calling metadata()
// 2. Matching on the Result
// 3. Distinguishing NotFound from other errors
// 4. Deciding how to handle non-NotFound errors
}The manual approach requires understanding io::ErrorKind and making decisions about error handling.
use predicates::path::exists;
use std::path::Path;
fn main() {
let existing_path = Path::new("/etc/hosts");
let nonexistent_path = Path::new("/nonexistent/file/path");
// Simple boolean evaluation
let predicate = exists();
println!("{} exists: {}", existing_path.display(), predicate.eval(&existing_path));
println!("{} exists: {}", nonexistent_path.display(), predicate.eval(&nonexistent_path));
// The predicate handles:
// - Calling metadata internally
// - Proper error handling
// - Clean boolean result
}exists() encapsulates the entire existence-checking logic in a reusable predicate.
use std::fs;
use std::io;
use std::path::Path;
use predicates::path::exists;
fn main() {
let path = Path::new("/etc/hosts");
// MANUAL APPROACH
// Check if path exists
fn check_exists_manual(path: &Path) -> bool {
fs::metadata(path).is_ok()
}
// But this has issues:
// - Doesn't distinguish "not found" from other errors
// - Noisy and repetitive
// BETTER MANUAL APPROACH
fn check_exists_proper(path: &Path) -> bool {
match fs::metadata(path) {
Ok(_) => true,
Err(e) if e.kind() == io::ErrorKind::NotFound => false,
Err(e) => {
// What do we do here? Log? Panic? Return false?
eprintln!("Error checking path: {}", e);
false
}
}
}
// PREDICATE APPROACH
fn check_exists_predicate(path: &Path) -> bool {
exists().eval(path)
}
// Single line, clear intent, proper error handling
println!("Manual: {}", check_exists_manual(path));
println!("Proper manual: {}", check_exists_proper(path));
println!("Predicate: {}", check_exists_predicate(path));
}The predicate approach reduces boilerplate and handles edge cases automatically.
use predicates::path::exists;
use predicates::prelude::*;
use std::path::Path;
// The predicates crate integrates with assertion frameworks
// to provide readable failure messages
fn main() {
let temp_dir = std::env::temp_dir();
let existing_path = temp_dir.join("does_this_exist_unlikely_name_xyz");
// In tests, you'd typically use:
// Option 1: Using with assertion libraries
let result = exists().eval(&existing_path);
if !result {
println!("Assertion failed: path {:?} does not exist", existing_path);
}
// Option 2: Using the predicate's native display
let predicate = exists();
println!("Predicate description: {}", predicate);
// Option 3: Using into_bool for direct comparison
// (eval returns bool directly, so this is straightforward)
let path = Path::new("/etc/hosts");
assert!(
exists().eval(path),
"Expected {:?} to exist but it does not",
path
);
println!("Assertion passed for {:?}", path);
// The predicate approach gives you:
// 1. Clear intent (exists())
// 2. Automatic proper error handling
// 3. Reusable across tests
}In tests, predicates provide clear intent and automatic failure messages.
use predicates::path::*;
use predicates::prelude::*;
use std::path::Path;
fn main() {
let path = Path::new("/etc/hosts");
// exists() - path exists (file or directory)
println!("exists: {}", exists().eval(path));
// missing() - path does NOT exist
println!("missing: {}", missing().eval(Path::new("/nonexistent")));
// is_file() - path exists and is a file
println!("is_file: {}", is_file().eval(path));
// is_dir() - path exists and is a directory
println!("is_dir: {}", is_dir().eval(Path::new("/etc")));
// is_symlink() - path is a symbolic link
println!("is_symlink: {}", is_symlink().eval(path));
// Each predicate is composable and reusable
let file_exists = exists().and(is_file());
println!("file_exists: {}", file_exists.eval(path));
}The predicates crate provides a family of path-related predicates.
use predicates::path::{exists, is_file, is_dir};
use predicates::prelude::*;
use std::path::Path;
fn main() {
// Predicates compose with boolean logic
// AND composition
let exists_and_is_file = exists().and(is_file());
println!("Is existing file: {}", exists_and_is_file.eval(Path::new("/etc/hosts")));
// OR composition
let file_or_dir = is_file().or(is_dir());
println!("Is file or dir: {}", file_or_dir.eval(Path::new("/etc")));
// NOT composition
let not_directory = PredicateBoolExt::not(is_dir());
println!("Not a directory: {}", not_directory.eval(Path::new("/etc/hosts")));
// Complex composition
let exists_but_not_dir = exists().and(PredicateBoolExt::not(is_dir()));
println!("Exists but not directory: {}", exists_but_not_dir.eval(Path::new("/etc/hosts")));
// This composability is difficult with manual checks
}Predicates compose naturally, making complex conditions readable.
use std::fs;
use std::io;
use std::path::Path;
// Composing manual checks requires verbose functions
fn path_exists_and_is_file(path: &Path) -> bool {
match fs::metadata(path) {
Ok(metadata) => metadata.is_file(),
Err(e) if e.kind() == io::ErrorKind::NotFound => false,
Err(_) => false,
}
}
fn path_exists_and_is_dir(path: &Path) -> bool {
match fs::metadata(path) {
Ok(metadata) => metadata.is_dir(),
Err(e) if e.kind() == io::ErrorKind::NotFound => false,
Err(_) => false,
}
}
fn path_exists_but_not_dir(path: &Path) -> bool {
match fs::metadata(path) {
Ok(metadata) => !metadata.is_dir(),
Err(e) if e.kind() == io::ErrorKind::NotFound => false,
Err(_) => false,
}
}
fn main() {
let path = Path::new("/etc/hosts");
println!("Exists and is file: {}", path_exists_and_is_file(path));
println!("Exists and is dir: {}", path_exists_and_is_dir(path));
println!("Exists but not dir: {}", path_exists_but_not_dir(path));
// Each condition requires a new function
// No natural composition
// Repetitive error handling
}Manual composition requires repetitive error handling for each condition.
use std::fs;
use std::io;
use std::path::Path;
use predicates::path::exists;
fn main() {
// Manual approach: you must decide what to do with errors
fn exists_manual(path: &Path) -> Result<bool, io::Error> {
match fs::metadata(path) {
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e), // Propagate other errors
}
}
// Common shortcut (loses error information)
fn exists_simple(path: &Path) -> bool {
fs::metadata(path).is_ok()
}
// Predicate approach: handles errors consistently
// Returns false for any error (NotFound, PermissionDenied, etc.)
fn exists_predicate(path: &Path) -> bool {
exists().eval(path)
}
// If you need to distinguish error types,
// use manual approach or combine:
fn check_path(path: &Path) -> Result<bool, io::Error> {
match fs::metadata(path) {
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
// Handle permission issues
eprintln!("Permission denied: {}", path.display());
Ok(false) // or propagate: Err(e)
}
Err(e) => Err(e),
}
}
let path = Path::new("/etc/hosts");
println!("Manual result: {:?}", exists_manual(path));
println!("Simple result: {}", exists_simple(path));
println!("Predicate result: {}", exists_predicate(path));
}Predicates handle errors consistently but return only boolean; manual checks allow error propagation.
use std::fs;
use std::path::PathBuf;
use predicates::path::{exists, missing, is_file};
fn create_temp_file(dir: &std::path::Path, name: &str) -> PathBuf {
let path = dir.join(name);
fs::write(&path, "content").expect("Failed to create file");
path
}
fn main() {
let temp_dir = std::env::temp_dir();
let test_file = temp_dir.join("test_predicate_example.txt");
// Clean up if it exists
let _ = fs::remove_file(&test_file);
// Verify file doesn't exist
assert!(
missing().eval(&test_file),
"File should not exist before creation"
);
// Create the file
fs::write(&test_file, "test content").expect("Failed to write");
// Verify file exists
assert!(
exists().eval(&test_file),
"File should exist after creation"
);
// Verify it's a file (not a directory)
assert!(
is_file().eval(&test_file),
"Path should be a file"
);
// Clean up
fs::remove_file(&test_file).expect("Failed to clean up");
// Verify cleanup
assert!(
missing().eval(&test_file),
"File should not exist after cleanup"
);
println!("All assertions passed!");
}Predicates make test assertions readable and self-documenting.
use std::fs;
use std::path::PathBuf;
use predicates::path::{exists, is_file, is_dir, missing};
use predicates::prelude::*;
// Imagine this is a real test module
struct TestContext {
temp_dir: PathBuf,
}
impl TestContext {
fn new() -> Self {
let temp_dir = std::env::temp_dir().join("test_predicate_example");
fs::create_dir_all(&temp_dir).ok();
Self { temp_dir }
}
fn create_file(&self, name: &str) -> PathBuf {
let path = self.temp_dir.join(name);
fs::write(&path, "content").expect("Failed to create file");
path
}
fn create_dir(&self, name: &str) -> PathBuf {
let path = self.temp_dir.join(name);
fs::create_dir_all(&path).expect("Failed to create directory");
path
}
}
impl Drop for TestContext {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.temp_dir);
}
}
#[test]
fn test_file_operations() {
let ctx = TestContext::new();
let file_path = ctx.create_file("test.txt");
let dir_path = ctx.create_dir("subdir");
// With predicates: clear, readable assertions
assert!(exists().eval(&file_path), "File should exist");
assert!(is_file().eval(&file_path), "Should be a file");
assert!(exists().eval(&dir_path), "Directory should exist");
assert!(is_dir().eval(&dir_path), "Should be a directory");
// Clean up file
fs::remove_file(&file_path).expect("Failed to remove file");
assert!(missing().eval(&file_path), "File should be removed");
println!("All tests passed!");
}
fn main() {
test_file_operations();
}In real tests, predicates make assertions self-documenting.
use std::fs;
use std::path::PathBuf;
use predicates::path::exists;
use predicates::str::contains;
fn main() {
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join("test_content.txt");
fs::write(&file_path, "Hello, World! This is test content.").unwrap();
// Path predicate for existence
assert!(exists().eval(&file_path), "File should exist");
// Content predicate (requires reading the file)
let content = fs::read_to_string(&file_path).unwrap();
assert!(contains("Hello").eval(&content), "File should contain 'Hello'");
// Combined check: file exists and has content
fn file_exists_with_content(path: &std::path::Path, expected: &str) -> bool {
exists().eval(path) && {
fs::read_to_string(path)
.map(|content| content.contains(expected))
.unwrap_or(false)
}
}
assert!(
file_exists_with_content(&file_path, "World"),
"File should exist and contain 'World'"
);
fs::remove_file(&file_path).ok();
}Path predicates can be combined with other predicate types for comprehensive checks.
use std::fs;
use std::path::Path;
use predicates::path::exists;
fn main() {
// Both approaches ultimately call fs::metadata()
// Performance is equivalent
// Manual approach
fn check_many_manual(paths: &[&Path]) -> Vec<bool> {
paths.iter()
.map(|p| fs::metadata(p).is_ok())
.collect()
}
// Predicate approach
fn check_many_predicate(paths: &[&Path]) -> Vec<bool> {
paths.iter()
.map(|p| exists().eval(*p))
.collect()
}
// The predicate has minimal overhead (just wrapping the call)
// Choose based on readability and composability, not performance
let paths: Vec<&Path> = vec![
Path::new("/etc/hosts"),
Path::new("/etc/passwd"),
Path::new("/nonexistent"),
];
let manual_results = check_many_manual(&paths);
let predicate_results = check_many_predicate(&paths);
println!("Manual: {:?}", manual_results);
println!("Predicate: {:?}", predicate_results);
}Performance is comparable; the predicate is a thin wrapper around metadata().
use std::fs;
use std::io;
use std::path::Path;
use predicates::path::{exists, missing, is_file, is_dir};
fn main() {
let path = Path::new("/etc/hosts");
// Check: does path exist?
// Manual (simple)
let _ = fs::metadata(path).is_ok();
// Manual (proper)
let _ = match fs::metadata(path) {
Ok(_) => true,
Err(e) if e.kind() == io::ErrorKind::NotFound => false,
Err(_) => false,
};
// Predicate
let _ = exists().eval(path);
// Check: does path NOT exist?
// Manual
let _ = !fs::metadata(path).is_ok(); // Wrong for errors
let _ = matches!(fs::metadata(path), Err(e) if e.kind() == io::ErrorKind::NotFound);
// Predicate
let _ = missing().eval(path);
// Check: is path an existing file?
// Manual
let _ = match fs::metadata(path) {
Ok(m) => m.is_file(),
Err(_) => false,
};
// Predicate
let _ = is_file().eval(path);
// Check: is path an existing directory?
// Manual
let _ = match fs::metadata(path) {
Ok(m) => m.is_dir(),
Err(_) => false,
};
// Predicate
let _ = is_dir().eval(path);
println!("Syntax comparison complete");
}Predicates provide shorter, clearer syntax for common filesystem checks.
Approach comparison:
| Aspect | Manual metadata | predicates::path |
|--------|-------------------|-------------------|
| Verbosity | More boilerplate | Concise |
| Intent clarity | Requires reading implementation | Self-documenting |
| Error handling | Must handle explicitly | Handled automatically |
| Composability | Manual function composition | Native .and(), .or(), .not() |
| Reusability | Copy-paste or helper functions | Predicate objects |
| Test integration | Custom assertions | Predicate-friendly |
Available predicates:
| Predicate | Meaning | Returns true when |
|-----------|---------|-------------------|
| exists() | Path exists | metadata(path).is_ok() |
| missing() | Path doesn't exist | metadata(path) returns NotFound |
| is_file() | Path is a file | Exists and is_file() |
| is_dir() | Path is a directory | Exists and is_dir() |
| is_symlink() | Path is a symlink | symlink_metadata().is_ok() and is symlink |
When to use each:
| Use manual when | Use predicates when | |-----------------|--------------------| | Need error details | Writing test assertions | | Custom error handling | Checking simple existence | | Non-boolean result needed | Composing conditions | | Performance-critical inner loops | Code clarity matters | | Need to propagate errors | Reusable checks across codebase |
Key insight: predicates::path::exists and its related predicates shift filesystem checks from imperative operations to declarative assertions. The manual approach—fs::metadata(path).is_ok()—works but requires the reader to understand that metadata returns Err(NotFound) for missing paths and that is_ok() treats all errors as "doesn't exist." The predicate exists().eval(path) expresses the intent directly and handles the NotFound versus other-error distinction appropriately. More importantly, predicates compose naturally: exists().and(is_file()) is immediately readable while the equivalent manual code requires nested matches or helper functions. In tests especially, this readability translates to maintainability—you can glance at assert!(is_file().eval(path)) and understand exactly what's being checked, while assert!(fs::metadata(path).map(|m| m.is_file()).unwrap_or(false)) requires mental parsing. The trade-off is that predicates return only boolean results; if you need to distinguish permission-denied from not-found or propagate errors up the call stack, manual metadata checks give you that control. For the common case of simple existence checks in tests and assertions, predicates provide clearer code with no downsides.