use std::collections::HashMap;
use std::collections::hash_map;
use std::fmt::Display;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::Read;
use std::io::Seek;
use std::io::Write;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use crate::Error;
use crate::Result;
use crate::platform::platform_env::CurrentDirSetter;
macro_rules! fs_error {
($($arg:tt)*) => {
$crate::Error::fs(format!($($arg)*))
};
}
pub trait Fs {
type ReadHandle: Read + Seek;
type WriteHandle: Read + Write + Seek;
type Dir: Iterator<Item = DirEntry>;
fn open_read<P: AsRef<Path>>(&self, path: P) -> Result<Self::ReadHandle>;
fn open_write<P: AsRef<Path>>(&self, path: P) -> Result<Self::WriteHandle> {
self.open_write_options(path, OpenWriteOptions::new().create())
}
fn open_write_options<P: AsRef<Path>>(
&self,
path: P,
options: OpenWriteOptions,
) -> Result<Self::WriteHandle>;
fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
let error_path = path.as_ref().to_path_buf();
let mut file = self.open_read(path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)
.map_err(fs_error_at(&error_path))?;
Ok(buffer)
}
fn read_to_string<P: AsRef<Path>>(&self, path: P) -> Result<String> {
let error_path = path.as_ref().to_path_buf();
String::from_utf8(self.read(path)?).map_err(fs_error_at(&error_path))
}
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&self, path: P, data: C) -> Result<()> {
let error_path = path.as_ref().to_path_buf();
let mut file =
self.open_write_options(path, OpenWriteOptions::new().create().truncate())?;
file.write_all(data.as_ref())
.map_err(fs_error_at(&error_path))?;
Ok(())
}
fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<Self::Dir>;
fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> Result;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct OpenWriteOptions {
pub append: bool,
pub truncate: bool,
pub create: bool,
pub create_new: bool,
}
impl OpenWriteOptions {
pub const fn new() -> Self {
OpenWriteOptions {
append: false,
truncate: false,
create: false,
create_new: false,
}
}
pub const fn append(mut self) -> Self {
self.append = true;
self
}
pub const fn create(mut self) -> Self {
self.create = true;
self
}
pub const fn create_new(mut self) -> Self {
self.create_new = true;
self
}
pub const fn truncate(mut self) -> Self {
self.truncate = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
File,
Dir,
}
impl FileType {
pub fn is_dir(&self) -> bool {
matches!(self, FileType::Dir)
}
pub fn is_file(&self) -> bool {
matches!(self, FileType::File)
}
}
#[derive(Debug, Clone)]
pub struct DirEntry {
pub(crate) path: PathBuf,
pub(crate) file_type: FileType,
}
impl DirEntry {
pub(crate) fn new(path: PathBuf, file_type: FileType) -> Option<Self> {
path.file_name().and_then(|name| name.to_str())?;
Some(Self { path, file_type })
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn file_name(&self) -> &str {
self.path
.file_name()
.and_then(|n| n.to_str())
.expect("DirEntry path has no UTF-8 file name component")
}
pub fn file_type(&self) -> FileType {
self.file_type
}
}
#[derive(Debug, Clone, Copy)]
pub enum BorrowState {
Read(u64),
Write,
}
pub struct FileHandle {
handles: Arc<Mutex<HashMap<PathBuf, BorrowState>>>,
path: PathBuf,
file: File,
}
pub(crate) fn lock_handles<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(|e| {
mutex.clear_poison();
e.into_inner()
})
}
impl Drop for FileHandle {
fn drop(&mut self) {
let mut handles = lock_handles(&self.handles);
if let Some(state) = handles.get_mut(&self.path) {
match state {
BorrowState::Read(count) => {
*count = count.saturating_sub(1);
if *count == 0 {
handles.remove(&self.path);
}
}
BorrowState::Write => {
handles.remove(&self.path);
}
}
}
}
}
pub struct FileReadHandle(FileHandle);
impl FileReadHandle {
fn acquire(
handles: Arc<Mutex<HashMap<PathBuf, BorrowState>>>,
path: PathBuf,
file: File,
) -> Result<Self> {
{
let mut handles = lock_handles(&handles);
let entry = handles.entry(path.clone());
match entry {
hash_map::Entry::Occupied(mut open) => match open.get() {
BorrowState::Read(count) => {
open.insert(BorrowState::Read(count + 1));
}
BorrowState::Write => {
return Err(fs_error!(
"File could not be opened, it is already borrowed for writing: {}",
path.display()
));
}
},
hash_map::Entry::Vacant(_) => {
entry.insert_entry(BorrowState::Read(1));
}
}
}
Ok(Self(FileHandle {
handles,
path,
file,
}))
}
}
impl Read for FileReadHandle {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.file.read(buf)
}
}
impl Seek for FileReadHandle {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.0.file.seek(pos)
}
}
pub struct FileWriteHandle(FileHandle);
impl FileWriteHandle {
fn acquire(
handles: Arc<Mutex<HashMap<PathBuf, BorrowState>>>,
path: PathBuf,
options: &mut OpenOptions,
) -> Result<Self> {
let file;
{
let mut handles = lock_handles(&handles);
let entry = handles.entry(path.clone());
match entry {
hash_map::Entry::Occupied(open) => match open.get() {
BorrowState::Read(count) => {
return Err(fs_error!(
"File could not be opened, it is already borrowed for reading {count} times: {}",
path.display()
));
}
BorrowState::Write => {
return Err(fs_error!(
"File could not be opened, it is already borrowed for writing: {}",
path.display()
));
}
},
hash_map::Entry::Vacant(_) => {
file = options.open(&path).map_err(fs_error_at(&path))?;
entry.insert_entry(BorrowState::Write);
}
}
}
Ok(Self(FileHandle {
handles,
path,
file,
}))
}
}
impl Read for FileWriteHandle {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.file.read(buf)
}
}
impl Seek for FileWriteHandle {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.0.file.seek(pos)
}
}
impl Write for FileWriteHandle {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.file.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.0.file.flush()
}
}
pub struct FsImpl {
pub(crate) current_dir: PathBuf,
handles: Arc<Mutex<HashMap<PathBuf, BorrowState>>>,
}
impl FsImpl {
pub fn new(current_dir: PathBuf) -> Self {
FsImpl {
current_dir,
handles: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl CurrentDirSetter for FsImpl {
fn set_current_dir(&mut self, path: PathBuf) {
self.current_dir = path;
}
}
impl Fs for FsImpl {
type ReadHandle = FileReadHandle;
type WriteHandle = FileWriteHandle;
type Dir = std::vec::IntoIter<DirEntry>;
fn open_read<P: AsRef<Path>>(&self, path: P) -> Result<Self::ReadHandle> {
let normalized_path = normalize_path(&self.current_dir, path);
if std::fs::metadata(&normalized_path).is_ok_and(|metadata| metadata.is_dir()) {
return Err(fs_error!(
"File could not be opened, path is a directory: {}",
normalized_path.display()
));
}
let file = File::open(&normalized_path).map_err(fs_error_at(&normalized_path))?;
FileReadHandle::acquire(self.handles.clone(), normalized_path, file)
}
fn open_write_options<P: AsRef<Path>>(
&self,
path: P,
options: OpenWriteOptions,
) -> Result<Self::WriteHandle> {
let normalized_path = normalize_path(&self.current_dir, path);
FileWriteHandle::acquire(
self.handles.clone(),
normalized_path,
File::options()
.read(true)
.write(true)
.create(options.create)
.create_new(options.create_new)
.append(options.append)
.truncate(options.truncate),
)
}
fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<Self::Dir> {
let normalized_path = normalize_path(&self.current_dir, path);
let to_entry = |path: PathBuf, file_type: std::fs::FileType| {
file_type
.is_dir()
.then_some(FileType::Dir)
.or(file_type.is_file().then_some(FileType::File))
.and_then(|file_type| DirEntry::new(path, file_type))
};
let entries: Vec<DirEntry> = std::fs::read_dir(&normalized_path)
.and_then(|dir| {
dir.filter_map(|result| {
result
.and_then(|entry| entry.file_type().map(|t| to_entry(entry.path(), t)))
.transpose()
})
.collect()
})
.map_err(fs_error_at(&normalized_path))?;
Ok(entries.into_iter())
}
fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> Result {
let normalized_path = normalize_path(&self.current_dir, path);
std::fs::create_dir_all(&normalized_path).map_err(fs_error_at(&normalized_path))
}
}
pub(crate) fn fs_error_at<T: Display>(path: &Path) -> impl FnOnce(T) -> Error + '_ {
move |error| fs_error!("{error}: {}", path.display())
}
pub(crate) fn normalize_path<C: Into<PathBuf>, P: AsRef<Path>>(current_dir: C, path: P) -> PathBuf {
if path.as_ref().is_relative() {
cannonicalize_path(current_dir.into(), path)
} else {
cannonicalize_path(PathBuf::new(), path)
}
}
fn cannonicalize_path<P: AsRef<Path>>(mut base: PathBuf, path: P) -> PathBuf {
for component in path.as_ref().components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
base.pop();
}
Component::RootDir | Component::Prefix(_) => {
base.push(component);
}
Component::Normal(part) => {
base.push(part);
}
}
}
base
}
#[cfg(test)]
mod tests {
use std::io::SeekFrom;
use super::*;
struct TestFile {
path: PathBuf,
}
impl TestFile {
pub fn new(path: PathBuf) -> Self {
let _ = std::fs::remove_file(&path);
Self { path }
}
}
impl Drop for TestFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
struct TestDir {
path: PathBuf,
}
impl TestDir {
pub fn new(path: PathBuf) -> Self {
let _ = std::fs::remove_dir_all(&path);
Self { path }
}
}
impl Drop for TestDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
fn assert_os_error(error: Error, path: &Path) {
let suffix = format!(": {}", path.display());
let os_message = error.description().strip_suffix(&suffix);
assert!(
os_message.is_some_and(|message| !message.is_empty()),
"error should be the OS message followed by the path, got: {error}"
);
}
mod fs_impl {
use super::*;
#[test]
fn fs_impl_should_enforce_one_writer_or_many_readers() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let normalized_path = normalize_path(&fs.current_dir, "Cargo.toml");
let read1 = fs.open_read("Cargo.toml").unwrap();
let read2 = fs.open_read("Cargo.toml").unwrap();
let err = fs.open_write("Cargo.toml").err().unwrap().to_string();
assert_eq!(
err,
fs_error!(
"File could not be opened, it is already borrowed for reading 2 times: {}",
normalized_path.display()
)
.to_string()
);
drop(read1);
drop(read2);
let write_handle = fs.open_write("Cargo.toml").unwrap();
let err = fs.open_read("Cargo.toml").err().unwrap().to_string();
assert_eq!(
err,
fs_error!(
"File could not be opened, it is already borrowed for writing: {}",
normalized_path.display()
)
.to_string()
);
assert!(fs.read("Cargo.toml").is_err());
assert!(fs.write("Cargo.toml", "data").is_err());
let err = fs.open_write("Cargo.toml").err().unwrap().to_string();
assert_eq!(
err,
fs_error!(
"File could not be opened, it is already borrowed for writing: {}",
normalized_path.display()
)
.to_string()
);
drop(write_handle);
let _read = fs.open_read("Cargo.toml").unwrap();
}
mod current_dir_setter {
use super::*;
#[test]
fn set_current_dir_should_update_current_directory() {
let mut fs = FsImpl::new(PathBuf::new());
let new_dir = PathBuf::from("/tmp");
fs.set_current_dir(new_dir.clone());
assert_eq!(fs.current_dir, new_dir);
}
}
mod fs {
use super::*;
mod open_read {
use super::*;
#[test]
fn open_read_should_return_readable_handle() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let mut file = fs.open_read("Cargo.toml").unwrap();
let len = file.seek(SeekFrom::End(0)).unwrap() as usize;
assert!(len > 0);
file.seek(SeekFrom::Start(0)).unwrap();
let mut content = vec![0_u8; len];
let read = file.read(&mut content).unwrap();
assert_eq!(read, len);
}
#[test]
fn open_read_should_fail_for_nonexistent_file() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.open_read("nonexistent_file.txt").err().unwrap(),
&normalize_path(&fs.current_dir, "nonexistent_file.txt"),
);
}
#[test]
fn open_read_dir_should_fail() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_eq!(
fs.open_read("src").err().unwrap().to_string(),
fs_error!(
"File could not be opened, path is a directory: {}",
fs.current_dir.join("src").display()
)
.to_string()
);
}
#[test]
fn open_read_should_fail_when_file_is_write_borrowed() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let _write_handle = fs.open_write("Cargo.toml").unwrap();
assert!(fs.open_read("Cargo.toml").is_err());
}
}
mod open_write {
use super::*;
#[test]
fn open_write_should_return_writable_handle_to_new_or_existent_file() {
let _test_file = TestFile::new(
std::env::current_dir()
.unwrap()
.join("fs_test_open_write.tmp"),
);
let fs = FsImpl::new(std::env::current_dir().unwrap());
let content = b"content";
let mut file = fs.open_write("fs_test_open_write.tmp").unwrap();
let written = file.write(content).unwrap();
assert_eq!(written, content.len());
drop(file);
let mut file = fs.open_write("fs_test_open_write.tmp").unwrap();
let len = file.seek(SeekFrom::End(0)).unwrap() as usize;
assert_eq!(len, content.len());
file.seek(SeekFrom::Start(0)).unwrap();
let mut read_content = vec![0_u8; content.len()];
let read = file.read(&mut read_content).unwrap();
assert_eq!(read, len);
assert_eq!(read_content, content);
}
}
mod open_write_options {
use super::*;
#[test]
fn open_write_options_append_should_only_append_to_existing_content() {
let _test_file =
TestFile::new(std::env::current_dir().unwrap().join("fs_test_append.tmp"));
let fs = FsImpl::new(std::env::current_dir().unwrap());
let content = b"content";
fs.write("fs_test_append.tmp", content).unwrap();
let mut file = fs
.open_write_options("fs_test_append.tmp", OpenWriteOptions::new().append())
.unwrap();
file.seek(SeekFrom::Start(0)).unwrap();
let written = file.write(content).unwrap();
assert_eq!(written, content.len());
let current = file.stream_position().unwrap() as usize;
assert_eq!(current, 2 * written);
file.flush().unwrap();
}
#[test]
fn open_write_options_truncate_should_clear_existing_content() {
let _test_file = TestFile::new(
std::env::current_dir()
.unwrap()
.join("fs_test_truncate.tmp"),
);
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.write("fs_test_truncate.tmp", "content").unwrap();
let mut file = fs
.open_write_options(
"fs_test_truncate.tmp",
OpenWriteOptions::new().truncate(),
)
.unwrap();
let len = file.seek(SeekFrom::End(0)).unwrap() as usize;
assert_eq!(len, 0);
}
#[test]
fn open_write_options_create_new_should_fail_on_existing_file() {
let _test_file = TestFile::new(
std::env::current_dir()
.unwrap()
.join("fs_test_create_new.tmp"),
);
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.write("fs_test_create_new.tmp", "content").unwrap();
assert_os_error(
fs.open_write_options(
"fs_test_create_new.tmp",
OpenWriteOptions::new().create_new(),
)
.err()
.unwrap(),
&normalize_path(&fs.current_dir, "fs_test_create_new.tmp"),
);
}
#[test]
fn open_write_options_append_with_truncate_should_be_rejected() {
let _test_file = TestFile::new(
std::env::current_dir()
.unwrap()
.join("fs_test_reject_append_truncate.tmp"),
);
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert!(
fs.open_write_options(
"fs_test_reject_append_truncate.tmp",
OpenWriteOptions::new().append().truncate()
)
.is_err()
);
assert!(
fs.open_write_options(
"fs_test_reject_append_truncate.tmp",
OpenWriteOptions::new().append().truncate().create()
)
.is_err()
);
assert!(
fs.open_write_options(
"fs_test_reject_append_truncate.tmp",
OpenWriteOptions::new().append().truncate().create_new()
)
.is_ok()
);
assert!(
fs.open_write_options(
"fs_test_reject_append_truncate.tmp",
OpenWriteOptions::new().append().truncate().create_new()
)
.is_err()
);
}
}
mod read {
use super::*;
#[test]
fn read_should_return_file_contents_as_bytes() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let path = fs
.current_dir
.join("Cargo.toml")
.to_string_lossy()
.to_string();
let result = fs.read(path.as_str()).unwrap();
assert!(!result.is_empty());
}
}
mod read_to_string {
use super::*;
#[test]
fn read_to_string_should_return_file_contents_as_string() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let result = fs.read_to_string("./src/../Cargo.toml").unwrap();
assert!(!result.is_empty());
}
#[test]
fn read_to_string_should_fail_for_nonexistent_file() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert!(fs.read_to_string("Cargo.toml2").is_err());
}
}
mod write {
use super::*;
#[test]
fn write_should_create_file_and_write_content() {
let _test_file = TestFile::new(
std::env::current_dir()
.unwrap()
.join("fs_test_write_create.tmp"),
);
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.write("fs_test_write_create.tmp", "hello").unwrap();
assert_eq!(
fs.read_to_string("fs_test_write_create.tmp").unwrap(),
"hello"
);
}
#[test]
fn write_should_overwrite_existing_content() {
let _test_file = TestFile::new(
std::env::current_dir()
.unwrap()
.join("fs_test_write_overwrite.tmp"),
);
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.write("fs_test_write_overwrite.tmp", "hello").unwrap();
fs.write("fs_test_write_overwrite.tmp", "a").unwrap();
assert_eq!(
fs.read_to_string("fs_test_write_overwrite.tmp").unwrap(),
"a"
);
}
#[test]
fn write_should_fail_when_file_is_write_borrowed() {
let _test_file = TestFile::new(
std::env::current_dir()
.unwrap()
.join("fs_test_write_while_write_borrowed.tmp"),
);
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.write("fs_test_write_while_write_borrowed.tmp", "hello")
.unwrap();
let _write_handle = fs
.open_write("fs_test_write_while_write_borrowed.tmp")
.unwrap();
assert!(
fs.write("fs_test_write_while_write_borrowed.tmp", "world")
.is_err()
);
}
#[test]
fn write_should_fail_when_file_is_read_borrowed() {
let _test_file = TestFile::new(
std::env::current_dir()
.unwrap()
.join("fs_test_write_while_read_borrowed.tmp"),
);
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.write("fs_test_write_while_read_borrowed.tmp", "hello")
.unwrap();
let _read_handle = fs
.open_read("fs_test_write_while_read_borrowed.tmp")
.unwrap();
fs.write("fs_test_write_while_read_borrowed.tmp", "world")
.unwrap_err();
assert_eq!(
fs.read_to_string("fs_test_write_while_read_borrowed.tmp")
.unwrap(),
"hello"
);
}
}
mod read_dir {
use super::*;
#[test]
fn read_dir_should_list_directory_contents() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let entries: Vec<DirEntry> = fs.read_dir("src").unwrap().collect();
let names: Vec<String> =
entries.iter().map(|e| e.file_name().to_string()).collect();
assert!(names.contains(&"fs.rs".to_string()));
assert!(names.contains(&"lib.rs".to_string()));
}
#[test]
fn read_dir_should_distinguish_files_and_directories() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let entries: Vec<DirEntry> = fs.read_dir("src").unwrap().collect();
let fs_entry = entries.iter().find(|e| e.file_name() == "fs.rs").unwrap();
assert!(
fs_entry.file_type().is_file(),
"expected fs.rs to be a file"
);
let platform_entry = entries
.iter()
.find(|e| e.file_name() == "platform")
.unwrap();
assert!(
platform_entry.file_type().is_dir(),
"expected 'platform' to be a directory"
);
}
#[test]
fn read_dir_should_fail_for_nonexistent_path() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.read_dir("nonexistent_dir").err().unwrap(),
&normalize_path(&fs.current_dir, "nonexistent_dir"),
);
}
#[cfg(unix)]
#[test]
fn read_dir_should_skip_symlinks() {
let dir = std::env::current_dir()
.unwrap()
.join("fs_test_read_dir_symlink.tmp");
let _test_dir = TestDir::new(dir.clone());
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.create_dir_all("fs_test_read_dir_symlink.tmp").unwrap();
fs.write("fs_test_read_dir_symlink.tmp/a.txt", "aaa")
.unwrap();
std::os::unix::fs::symlink(dir.join("a.txt"), dir.join("link")).unwrap();
let names: Vec<String> = fs
.read_dir("fs_test_read_dir_symlink.tmp")
.unwrap()
.map(|e| e.file_name().to_string())
.collect();
assert_eq!(names, vec!["a.txt"]);
}
#[cfg(unix)]
#[test]
fn read_dir_should_skip_non_utf8_names() {
use std::os::unix::ffi::OsStrExt;
let dir = std::env::current_dir()
.unwrap()
.join("fs_test_read_dir_non_utf8.tmp");
let _test_dir = TestDir::new(dir.clone());
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.create_dir_all("fs_test_read_dir_non_utf8.tmp").unwrap();
fs.write("fs_test_read_dir_non_utf8.tmp/a.txt", "aaa")
.unwrap();
std::fs::write(dir.join(std::ffi::OsStr::from_bytes(b"\xff.txt")), "bbb")
.unwrap();
let names: Vec<String> = fs
.read_dir("fs_test_read_dir_non_utf8.tmp")
.unwrap()
.map(|e| e.file_name().to_string())
.collect();
assert_eq!(names, vec!["a.txt"]);
}
}
mod create_dir_all {
use super::*;
#[test]
fn create_dir_all_should_create_directory_and_parents() {
let _test_dir = TestDir::new(
std::env::current_dir()
.unwrap()
.join("fs_test_create_dir_all.tmp"),
);
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.create_dir_all("fs_test_create_dir_all.tmp/a/b").unwrap();
assert!(
std::env::current_dir()
.unwrap()
.join("fs_test_create_dir_all.tmp/a/b")
.is_dir()
);
}
#[test]
fn create_dir_all_should_succeed_if_directory_already_exists() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.create_dir_all("src").unwrap();
assert!(
fs.current_dir.join("src").is_dir(),
"src should still be a directory"
);
}
#[test]
fn create_dir_all_should_fail_if_file_exists_at_path() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.create_dir_all("Cargo.toml").err().unwrap(),
&normalize_path(&fs.current_dir, "Cargo.toml"),
);
}
}
}
}
mod file_handle {
use super::*;
mod drop {
use super::*;
#[test]
fn drop_should_not_panic_when_path_not_in_handles() {
let handle = FileHandle {
handles: Arc::new(Mutex::new(HashMap::new())),
path: PathBuf::from("Cargo.toml"),
file: std::fs::File::open("Cargo.toml").unwrap(),
};
drop(handle);
}
}
}
mod file_read_handle {
use super::*;
#[test]
fn file_read_handle_should_be_usable_as_struct_field() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
struct S<Handle: Read + Seek> {
file: Handle,
}
let mut s = S {
file: fs.open_read("Cargo.toml").unwrap(),
};
let mut buffer = vec![0_u8; 1];
let read = s.file.read(&mut buffer).unwrap();
assert_eq!(read, buffer.len());
}
}
mod lock_handles {
use super::*;
#[test]
fn lock_handles_should_recover_from_poison_and_return_guard() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let poisoned_handles = fs.handles.clone();
let _ = std::panic::catch_unwind(|| {
std::thread::spawn(move || {
let _guard = poisoned_handles.lock().unwrap();
panic!("Poison the mutex");
})
.join()
.ok()
});
fs.open_read("Cargo.toml").unwrap();
fs.open_write("Cargo.toml").unwrap();
}
}
mod fs_error {
use crate::ErrorKind;
#[test]
fn fs_error_should_format_arguments_into_fs_error() {
let error = fs_error!("{} {}", "some", "error");
assert_eq!(error.kind(), ErrorKind::Fs);
assert_eq!(error.description(), "some error");
}
}
mod fs_error_at {
use super::*;
#[test]
fn fs_error_at_should_map_into_fs_error_for_path() {
let path = Path::new("/some/file.txt");
let result: std::result::Result<(), &str> = Err("fs error");
assert_eq!(
result.map_err(fs_error_at(path)).unwrap_err().to_string(),
fs_error!("fs error: {}", path.display()).to_string()
);
}
}
mod file_type {
use super::*;
mod is_dir {
use super::*;
#[test]
fn is_dir_should_return_true_for_dir() {
assert!(FileType::Dir.is_dir());
assert!(!FileType::File.is_dir());
}
}
mod is_file {
use super::*;
#[test]
fn is_file_should_return_true_for_file() {
assert!(FileType::File.is_file());
assert!(!FileType::Dir.is_file());
}
}
}
mod dir_entry {
use super::*;
#[test]
fn dir_entry_should_expose_path_file_name_and_file_type() {
let entry = DirEntry {
path: PathBuf::from("/some/dir/file.txt"),
file_type: FileType::File,
};
assert_eq!(entry.path(), Path::new("/some/dir/file.txt"));
assert_eq!(entry.file_name(), "file.txt");
assert_eq!(entry.file_type(), FileType::File);
}
}
}