use std::{
cell::RefCell,
collections::{BTreeMap, BTreeSet},
sync::{Arc, Mutex, RwLock},
};
use bytes::BytesMut;
#[cfg(feature = "simulator-real-fs")]
mod real_fs_support {
use bytes::BytesMut;
use scoped_tls::scoped_thread_local;
use std::sync::{Arc, Mutex};
pub struct RealFs;
scoped_thread_local! {
pub(super) static REAL_FS: RealFs
}
pub fn with_real_fs<T>(f: impl FnOnce() -> T) -> T {
REAL_FS.set(&RealFs, f)
}
#[inline]
pub fn is_real_fs() -> bool {
REAL_FS.is_set()
}
pub fn convert_std_file_to_simulator(
std_file: std::fs::File,
path: impl AsRef<std::path::Path>,
read: bool,
write: bool,
) -> std::io::Result<super::sync::File> {
let content = if read {
use std::io::Read;
let mut std_file = std_file;
let mut content = Vec::new();
std_file.read_to_end(&mut content)?;
content
} else {
Vec::new()
};
Ok(super::sync::File {
path: path.as_ref().to_path_buf(),
data: Arc::new(Mutex::new(BytesMut::from(content.as_slice()))),
position: 0,
write,
})
}
#[cfg(feature = "async")]
pub async fn convert_std_file_to_simulator_async(
std_file: std::fs::File,
path: impl AsRef<std::path::Path>,
read: bool,
write: bool,
) -> std::io::Result<super::unsync::File> {
let path_buf = path.as_ref().to_path_buf();
let content = if read {
switchy_async::task::spawn_blocking(move || {
use std::io::Read;
let mut std_file = std_file;
let mut content = Vec::new();
std_file.read_to_end(&mut content)?;
Ok::<Vec<u8>, std::io::Error>(content)
})
.await
.unwrap()?
} else {
Vec::new()
};
Ok(super::unsync::File {
path: path_buf,
data: Arc::new(Mutex::new(BytesMut::from(content.as_slice()))),
position: 0,
write,
})
}
}
#[cfg(not(feature = "simulator-real-fs"))]
mod real_fs_support {
pub fn with_real_fs<T>(f: impl FnOnce() -> T) -> T {
f()
}
#[inline]
#[allow(dead_code)]
pub const fn is_real_fs() -> bool {
false
}
}
pub use real_fs_support::with_real_fs;
thread_local! {
static FILES: RefCell<RwLock<BTreeMap<String, Arc<Mutex<BytesMut>>>>> =
const { RefCell::new(RwLock::new(BTreeMap::new())) };
static DIRECTORIES: RefCell<RwLock<BTreeSet<String>>> =
const { RefCell::new(RwLock::new(BTreeSet::new())) };
}
pub fn reset_fs() {
FILES.with_borrow_mut(|x| x.write().unwrap().clear());
reset_directories();
}
pub fn reset_directories() {
DIRECTORIES.with_borrow_mut(|x| x.write().unwrap().clear());
}
fn get_parent_directories(path: &str) -> Vec<String> {
let mut parents = Vec::new();
let path_buf = std::path::Path::new(path);
let mut current = path_buf.parent();
while let Some(parent) = current {
if let Some(parent_str) = parent.to_str()
&& !parent_str.is_empty()
&& parent_str != "/"
{
parents.push(parent_str.to_string());
}
current = parent.parent();
}
if path != "/" {
parents.push("/".to_string());
}
parents.reverse();
parents
}
fn normalize_path(path: &str) -> String {
let mut components: Vec<&str> = Vec::new();
let is_absolute = path.starts_with('/');
for component in path.split('/') {
match component {
"" | "." => {
}
".." => {
if !components.is_empty() && components.last() != Some(&"..") {
components.pop();
} else if !is_absolute {
components.push("..");
}
}
other => {
components.push(other);
}
}
}
if is_absolute {
if components.is_empty() {
"/".to_string()
} else {
format!("/{}", components.join("/"))
}
} else if components.is_empty() {
".".to_string()
} else {
components.join("/")
}
}
#[must_use]
pub fn exists<P: AsRef<std::path::Path>>(path: P) -> bool {
let Some(path) = path.as_ref().to_str() else {
return false;
};
DIRECTORIES.with_borrow(|dirs| dirs.read().unwrap().contains(path))
|| FILES.with_borrow(|files| files.read().unwrap().contains_key(path))
}
fn get_directory_children(dir_path: &str) -> (Vec<String>, Vec<String>) {
let normalized_dir = if dir_path == "/" {
"/"
} else {
&format!("{dir_path}/")
};
let files = FILES.with_borrow(|files| {
files
.read()
.unwrap()
.keys()
.filter_map(|file_path| {
file_path.strip_prefix(normalized_dir).and_then(|stripped| {
if !stripped.contains('/') && !stripped.is_empty() {
Some(stripped.to_string())
} else {
None
}
})
})
.collect::<Vec<_>>()
});
let subdirs = DIRECTORIES.with_borrow(|dirs| {
dirs.read()
.unwrap()
.iter()
.filter_map(|subdir_path| {
subdir_path
.strip_prefix(normalized_dir)
.and_then(|stripped| {
if !stripped.contains('/') && !stripped.is_empty() {
Some(stripped.to_string())
} else {
None
}
})
})
.collect::<Vec<_>>()
});
(files, subdirs)
}
pub fn init_minimal_fs() -> std::io::Result<()> {
#[cfg(feature = "sync")]
{
sync::create_dir_all("/")?;
sync::create_dir_all("/tmp")?;
sync::create_dir_all("/home")?;
}
Ok(())
}
pub fn init_standard_fs() -> std::io::Result<()> {
#[cfg(feature = "sync")]
{
sync::create_dir_all("/")?;
sync::create_dir_all("/bin")?;
sync::create_dir_all("/etc")?;
sync::create_dir_all("/home")?;
sync::create_dir_all("/lib")?;
sync::create_dir_all("/opt")?;
sync::create_dir_all("/root")?;
sync::create_dir_all("/sbin")?;
sync::create_dir_all("/tmp")?;
sync::create_dir_all("/usr")?;
sync::create_dir_all("/var")?;
sync::create_dir_all("/usr/bin")?;
sync::create_dir_all("/usr/lib")?;
sync::create_dir_all("/usr/local")?;
sync::create_dir_all("/usr/local/bin")?;
sync::create_dir_all("/usr/share")?;
sync::create_dir_all("/var/log")?;
sync::create_dir_all("/var/tmp")?;
sync::create_dir_all("/var/cache")?;
}
Ok(())
}
pub fn init_user_home(username: &str) -> std::io::Result<()> {
let home = format!("/home/{username}");
#[cfg(feature = "sync")]
{
sync::create_dir_all(&home)?;
sync::create_dir_all(format!("{home}/.config"))?;
sync::create_dir_all(format!("{home}/.local"))?;
sync::create_dir_all(format!("{home}/.local/share"))?;
sync::create_dir_all(format!("{home}/.cache"))?;
sync::create_dir_all(format!("{home}/Documents"))?;
sync::create_dir_all(format!("{home}/Downloads"))?;
}
Ok(())
}
#[cfg(all(feature = "simulator-real-fs", feature = "sync", feature = "std"))]
pub fn seed_from_real_fs<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
real_path: P,
sim_path: Q,
) -> std::io::Result<()> {
seed_recursive(real_path.as_ref(), sim_path.as_ref())
}
#[cfg(all(feature = "simulator-real-fs", feature = "sync", feature = "std"))]
fn seed_recursive(real_path: &std::path::Path, sim_path: &std::path::Path) -> std::io::Result<()> {
sync::create_dir_all(sim_path)?;
let entries = with_real_fs(|| crate::standard::sync::read_dir_sorted(real_path))?;
for entry in entries {
let entry_name = entry.file_name();
let real_entry_path = real_path.join(&entry_name);
let sim_entry_path = sim_path.join(&entry_name);
let file_type = entry.file_type()?;
if file_type.is_dir() {
seed_recursive(&real_entry_path, &sim_entry_path)?;
} else if file_type.is_file() {
let content = with_real_fs(|| std::fs::read(&real_entry_path))?;
sync::write(&sim_entry_path, content)?;
}
}
Ok(())
}
macro_rules! path_to_str {
($path:expr) => {{
$path.as_ref().to_str().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "path is invalid str")
})
}};
}
macro_rules! impl_file_sync {
($file:ident $(,)?) => {
impl std::io::Read for $file {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let binding = self.data.lock().unwrap();
let len = binding.len();
let pos = usize::try_from(self.position).unwrap();
let remaining = len - pos;
let read_count = std::cmp::min(remaining, buf.len());
if read_count == 0 {
return Ok(0);
}
let data = &binding[pos..(pos + read_count)];
buf[..read_count].copy_from_slice(data);
self.position += read_count as u64;
drop(binding);
Ok(read_count)
}
}
impl std::io::Write for $file {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
use bytes::BufMut as _;
if !self.write {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"File not opened in write mode",
));
}
let mut binding = self.data.lock().unwrap();
binding.put(buf);
drop(binding);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl std::io::Seek for $file {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.position = match pos {
std::io::SeekFrom::Start(x) => x,
std::io::SeekFrom::End(x) => {
u64::try_from(i64::try_from(self.data.lock().unwrap().len()).unwrap() - x)
.unwrap()
}
std::io::SeekFrom::Current(x) => {
u64::try_from(i64::try_from(self.position).unwrap() + x).unwrap()
}
};
Ok(self.position)
}
}
};
}
#[derive(Debug, Clone)]
pub struct FileType {
is_dir: bool,
is_file: bool,
is_symlink: bool,
}
impl FileType {
#[must_use]
pub const fn is_dir(&self) -> bool {
self.is_dir
}
#[must_use]
pub const fn is_file(&self) -> bool {
self.is_file
}
#[must_use]
pub const fn is_symlink(&self) -> bool {
self.is_symlink
}
}
#[derive(Debug, Clone)]
pub struct Metadata {
pub(crate) len: u64,
pub(crate) is_file: bool,
pub(crate) is_dir: bool,
pub(crate) is_symlink: bool,
}
impl Metadata {
#[must_use]
pub const fn len(&self) -> u64 {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub const fn is_file(&self) -> bool {
self.is_file
}
#[must_use]
pub const fn is_dir(&self) -> bool {
self.is_dir
}
#[must_use]
pub const fn is_symlink(&self) -> bool {
self.is_symlink
}
}
impl From<std::fs::Metadata> for Metadata {
fn from(meta: std::fs::Metadata) -> Self {
Self {
len: meta.len(),
is_file: meta.is_file(),
is_dir: meta.is_dir(),
is_symlink: meta.is_symlink(),
}
}
}
#[cfg(test)]
mod file_type_tests {
use super::FileType;
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_file_type_for_directory() {
let file_type = FileType {
is_dir: true,
is_file: false,
is_symlink: false,
};
assert_eq!(file_type.is_dir(), true);
assert_eq!(file_type.is_file(), false);
assert_eq!(file_type.is_symlink(), false);
}
#[test_log::test]
fn test_file_type_for_regular_file() {
let file_type = FileType {
is_dir: false,
is_file: true,
is_symlink: false,
};
assert_eq!(file_type.is_dir(), false);
assert_eq!(file_type.is_file(), true);
assert_eq!(file_type.is_symlink(), false);
}
#[test_log::test]
fn test_file_type_for_symlink() {
let file_type = FileType {
is_dir: false,
is_file: false,
is_symlink: true,
};
assert_eq!(file_type.is_dir(), false);
assert_eq!(file_type.is_file(), false);
assert_eq!(file_type.is_symlink(), true);
}
#[test_log::test]
fn test_file_type_clone() {
let original = FileType {
is_dir: true,
is_file: false,
is_symlink: false,
};
let cloned = original.clone();
assert_eq!(cloned.is_dir(), original.is_dir());
assert_eq!(cloned.is_file(), original.is_file());
assert_eq!(cloned.is_symlink(), original.is_symlink());
}
}
#[cfg(feature = "sync")]
pub mod sync {
use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use bytes::BytesMut;
use crate::sync::OpenOptions;
use super::{DIRECTORIES, FILES};
pub struct File {
#[cfg_attr(not(feature = "simulator-real-fs"), allow(dead_code))]
pub(crate) path: PathBuf,
pub(crate) data: Arc<Mutex<BytesMut>>,
pub(crate) position: u64,
pub(crate) write: bool,
}
impl File {
pub fn open(path: impl AsRef<Path>) -> std::io::Result<Self> {
OpenOptions::new().read(true).open(path)
}
pub fn create(path: impl AsRef<Path>) -> std::io::Result<Self> {
OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)
}
#[must_use]
pub const fn options() -> OpenOptions {
OpenOptions::new()
}
pub fn metadata(&self) -> std::io::Result<Metadata> {
#[cfg(all(feature = "simulator-real-fs", feature = "std"))]
if super::real_fs_support::is_real_fs() {
return Ok(std::fs::metadata(&self.path)?.into());
}
Ok(Metadata {
len: u64::try_from(self.data.lock().unwrap().len()).unwrap_or(0),
is_file: true,
is_dir: false,
is_symlink: false,
})
}
#[cfg(feature = "async")]
#[must_use]
pub fn into_async(self) -> crate::unsync::File {
crate::unsync::File {
path: self.path,
data: self.data,
position: self.position,
write: self.write,
}
}
}
pub use super::Metadata;
impl_file_sync!(File);
impl OpenOptions {
pub fn open(self, path: impl AsRef<::std::path::Path>) -> ::std::io::Result<File> {
#[cfg(all(feature = "simulator-real-fs", feature = "std"))]
if super::real_fs_support::is_real_fs() {
let std_options: std::fs::OpenOptions = self.clone().into();
let std_file = std_options.open(&path)?;
return super::real_fs_support::convert_std_file_to_simulator(
std_file, &path, self.read, self.write,
);
}
let location = path_to_str!(path)?;
let data = if let Some(data) =
FILES.with_borrow(|x| x.read().unwrap().get(location).cloned())
{
data
} else if self.create {
if let Some(parent) = std::path::Path::new(location).parent()
&& let Some(parent_str) = parent.to_str()
{
let parent_normalized = if parent_str.is_empty() || parent_str == "." {
".".to_string()
} else if parent_str == "/" {
"/".to_string()
} else {
parent_str.trim_end_matches('/').to_string()
};
if parent_normalized != "." && !super::exists(&parent_normalized) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Parent directory not found: {parent_normalized}"),
));
}
}
let data = Arc::new(Mutex::new(BytesMut::new()));
FILES.with_borrow_mut(|x| {
x.write()
.unwrap()
.insert(location.to_string(), data.clone())
});
data
} else {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("File not found at path={location}"),
));
};
if self.truncate {
data.lock().unwrap().clear();
}
Ok(File {
path: path.as_ref().to_path_buf(),
data,
position: 0,
write: self.write,
})
}
}
pub fn read<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<u8>> {
#[cfg(all(feature = "simulator-real-fs", feature = "std"))]
if super::real_fs_support::is_real_fs() {
return std::fs::read(path);
}
let location = path_to_str!(path)?;
let Some(existing) = FILES.with_borrow(|x| x.read().unwrap().get(location).cloned()) else {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("File not found at path={location}"),
));
};
Ok(existing.lock().unwrap().to_vec())
}
pub fn read_to_string<P: AsRef<Path>>(path: P) -> std::io::Result<String> {
#[cfg(all(feature = "simulator-real-fs", feature = "std"))]
if super::real_fs_support::is_real_fs() {
return crate::standard::sync::read_to_string(path);
}
let location = path_to_str!(path)?;
let Some(existing) = FILES.with_borrow(|x| x.read().unwrap().get(location).cloned()) else {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("File not found at path={location}"),
));
};
String::from_utf8(existing.lock().unwrap().to_vec())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> std::io::Result<()> {
use std::io::Write;
#[cfg(all(feature = "simulator-real-fs", feature = "std"))]
if super::real_fs_support::is_real_fs() {
return crate::standard::sync::write(path, contents);
}
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
file.write_all(contents.as_ref())?;
Ok(())
}
pub fn create_dir<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
#[cfg(all(feature = "simulator-real-fs", feature = "std"))]
if super::real_fs_support::is_real_fs() {
return crate::standard::sync::create_dir(path);
}
let path_str = path_to_str!(path)?;
let normalized = if path_str == "/" {
"/".to_string()
} else {
path_str.trim_end_matches('/').to_string()
};
if let Some(parent) = std::path::Path::new(&normalized).parent() {
let parent_str = parent.to_string_lossy().to_string();
if !parent_str.is_empty() && parent_str != "/" {
let parent_exists =
DIRECTORIES.with_borrow(|dirs| dirs.read().unwrap().contains(&parent_str));
if !parent_exists {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Parent directory does not exist: {parent_str}"),
));
}
}
}
DIRECTORIES.with_borrow_mut(|dirs| {
dirs.write().unwrap().insert(normalized);
});
Ok(())
}
pub fn create_dir_all<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
#[cfg(all(feature = "simulator-real-fs", feature = "std"))]
if super::real_fs_support::is_real_fs() {
return crate::standard::sync::create_dir_all(path);
}
let path_str = path_to_str!(path)?;
let normalized = if path_str == "/" {
"/".to_string()
} else {
path_str.trim_end_matches('/').to_string()
};
let mut dirs_to_create = super::get_parent_directories(&normalized);
dirs_to_create.push(normalized);
DIRECTORIES.with_borrow_mut(|dirs| {
let mut dirs_write = dirs.write().unwrap();
for dir in dirs_to_create {
dirs_write.insert(dir);
}
});
Ok(())
}
pub fn remove_dir_all<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
#[cfg(all(feature = "simulator-real-fs", feature = "std"))]
if super::real_fs_support::is_real_fs() {
return crate::standard::sync::remove_dir_all(path);
}
let path_str = path_to_str!(path)?;
let normalized = if path_str == "/" {
"/".to_string()
} else {
path_str.trim_end_matches('/').to_string()
};
if !super::exists(&normalized) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Directory not found: {normalized}"),
));
}
let prefix = if normalized == "/" {
"/"
} else {
&format!("{normalized}/")
};
FILES.with_borrow_mut(|files| {
let mut files_write = files.write().unwrap();
files_write
.retain(|file_path, _| !file_path.starts_with(prefix) && file_path != &normalized);
});
DIRECTORIES.with_borrow_mut(|dirs| {
let mut dirs_write = dirs.write().unwrap();
dirs_write.retain(|dir_path| !dir_path.starts_with(prefix) && dir_path != &normalized);
});
Ok(())
}
pub fn canonicalize<P: AsRef<Path>>(path: P) -> std::io::Result<std::path::PathBuf> {
#[cfg(all(feature = "simulator-real-fs", feature = "std"))]
if super::real_fs_support::is_real_fs() {
return crate::standard::sync::canonicalize(path);
}
let path_str = path_to_str!(path)?;
let normalized = super::normalize_path(path_str);
if !super::exists(&normalized) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Path not found: {normalized}"),
));
}
Ok(std::path::PathBuf::from(normalized))
}
pub fn read_dir_sorted<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<DirEntry>> {
#[cfg(all(feature = "simulator-real-fs", feature = "std"))]
if super::real_fs_support::is_real_fs() {
let std_entries = crate::standard::sync::read_dir_sorted(path)?;
return std_entries
.into_iter()
.map(|x| DirEntry::from_std(&x))
.collect::<std::io::Result<Vec<_>>>();
}
let path_str = path_to_str!(path)?;
let normalized = if path_str == "/" {
"/".to_string()
} else {
path_str.trim_end_matches('/').to_string()
};
if !super::exists(&normalized) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Directory not found: {normalized}"),
));
}
let (files, subdirs) = super::get_directory_children(&normalized);
let mut entries = Vec::new();
for filename in files {
let full_path = if normalized == "/" {
format!("/{filename}")
} else {
format!("{normalized}/{filename}")
};
entries.push(DirEntry::new_file(full_path, filename)?);
}
for dirname in subdirs {
let full_path = if normalized == "/" {
format!("/{dirname}")
} else {
format!("{normalized}/{dirname}")
};
entries.push(DirEntry::new_dir(full_path, dirname)?);
}
entries.sort_by_key(DirEntry::file_name);
Ok(entries)
}
pub fn walk_dir_sorted<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<DirEntry>> {
fn walk_recursive(dir_path: &str) -> std::io::Result<Vec<DirEntry>> {
let mut all_entries = Vec::new();
let (files, subdirs) = super::get_directory_children(dir_path);
for filename in files {
let full_path = if dir_path == "/" {
format!("/{filename}")
} else {
format!("{dir_path}/{filename}")
};
all_entries.push(DirEntry::new_file(full_path, filename)?);
}
for dirname in subdirs {
let full_path = if dir_path == "/" {
format!("/{dirname}")
} else {
format!("{dir_path}/{dirname}")
};
all_entries.push(DirEntry::new_dir(full_path.clone(), dirname)?);
let sub_entries = walk_recursive(&full_path)?;
all_entries.extend(sub_entries);
}
Ok(all_entries)
}
#[cfg(all(feature = "simulator-real-fs", feature = "std"))]
if super::real_fs_support::is_real_fs() {
let std_entries = crate::standard::sync::walk_dir_sorted(path)?;
return std_entries
.into_iter()
.map(|x| DirEntry::from_std(&x))
.collect::<std::io::Result<Vec<_>>>();
}
let path_str = path_to_str!(path)?;
let normalized = if path_str == "/" {
"/".to_string()
} else {
path_str.trim_end_matches('/').to_string()
};
if !super::exists(&normalized) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Directory not found: {normalized}"),
));
}
let mut all_entries = walk_recursive(&normalized)?;
all_entries.sort_by_key(DirEntry::path);
Ok(all_entries)
}
pub struct DirEntry {
path: PathBuf,
file_name: std::ffi::OsString,
file_type_info: super::FileType,
}
impl DirEntry {
pub fn from_std(entry: &std::fs::DirEntry) -> std::io::Result<Self> {
let file_type = entry.file_type()?;
Ok(Self {
path: entry.path(),
file_name: entry.file_name(),
file_type_info: super::FileType {
is_dir: file_type.is_dir(),
is_file: file_type.is_file(),
is_symlink: file_type.is_symlink(),
},
})
}
pub fn new_file(full_path: String, file_name: String) -> std::io::Result<Self> {
Ok(Self {
path: PathBuf::from(full_path),
file_name: std::ffi::OsString::from(file_name),
file_type_info: super::FileType {
is_dir: false,
is_file: true,
is_symlink: false,
},
})
}
pub fn new_dir(full_path: String, dir_name: String) -> std::io::Result<Self> {
Ok(Self {
path: PathBuf::from(full_path),
file_name: std::ffi::OsString::from(dir_name),
file_type_info: super::FileType {
is_dir: true,
is_file: false,
is_symlink: false,
},
})
}
#[must_use]
pub fn path(&self) -> PathBuf {
self.path.clone()
}
#[must_use]
pub fn file_name(&self) -> std::ffi::OsString {
self.file_name.clone()
}
pub fn file_type(&self) -> std::io::Result<super::FileType> {
Ok(self.file_type_info.clone())
}
}
#[cfg(test)]
mod test {
use std::{
io::Read as _,
sync::{Arc, Mutex},
};
use bytes::BytesMut;
use pretty_assertions::assert_eq;
use crate::simulator::FILES;
use super::OpenOptions;
#[switchy_async::test]
async fn can_read_empty_file() {
const FILENAME: &str = "sync::test1";
FILES.with_borrow_mut(|x| {
x.write()
.unwrap()
.insert(FILENAME.to_string(), Arc::new(Mutex::new(BytesMut::new())))
});
let mut file = OpenOptions::new().create(true).open(FILENAME).unwrap();
let mut buf = [0u8; 1024];
let read_count = file.read(&mut buf).unwrap();
assert_eq!(read_count, 0);
}
#[switchy_async::test]
async fn can_read_small_bytes_file() {
const FILENAME: &str = "sync::test2";
FILES.with_borrow_mut(|x| {
x.write().unwrap().insert(
FILENAME.to_string(),
Arc::new(Mutex::new(BytesMut::from(b"hey" as &[u8]))),
)
});
let mut file = OpenOptions::new().create(true).open(FILENAME).unwrap();
let mut buf = [0u8; 1024];
let read_count = file.read(&mut buf).unwrap();
assert_eq!(read_count, 3);
}
#[test_log::test]
fn test_write_without_write_permission() {
use std::io::Write as _;
super::super::reset_fs();
super::create_dir_all("/tmp").unwrap();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.open("/tmp/test_perms.txt")
.unwrap();
file.write_all(b"initial").unwrap();
drop(file);
let mut file = OpenOptions::new()
.read(true)
.open("/tmp/test_perms.txt")
.unwrap();
let result = file.write_all(b"should fail");
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
std::io::ErrorKind::PermissionDenied
);
}
#[test_log::test]
fn test_truncate_existing_file() {
use std::io::Write as _;
super::super::reset_fs();
super::create_dir_all("/tmp").unwrap();
super::write(
"/tmp/truncate_test.txt",
b"initial content that should be removed",
)
.unwrap();
let content = super::read_to_string("/tmp/truncate_test.txt").unwrap();
assert_eq!(content, "initial content that should be removed");
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open("/tmp/truncate_test.txt")
.unwrap();
file.write_all(b"new").unwrap();
drop(file);
let content = super::read_to_string("/tmp/truncate_test.txt").unwrap();
assert_eq!(content, "new");
}
#[test_log::test]
fn test_partial_reads() {
use std::io::Read as _;
super::super::reset_fs();
super::create_dir_all("/tmp").unwrap();
let test_data = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; super::write("/tmp/partial_read.txt", test_data).unwrap();
let mut file = OpenOptions::new()
.read(true)
.open("/tmp/partial_read.txt")
.unwrap();
let mut buf = [0u8; 10];
let mut total_read = Vec::new();
loop {
let count = file.read(&mut buf).unwrap();
if count == 0 {
break;
}
total_read.extend_from_slice(&buf[..count]);
}
assert_eq!(total_read.as_slice(), test_data);
}
#[test_log::test]
fn test_seek_and_read() {
use std::io::{Read as _, Seek as _, SeekFrom};
super::super::reset_fs();
super::create_dir_all("/tmp").unwrap();
super::write("/tmp/seek_test.txt", b"Hello, World!").unwrap();
let mut file = OpenOptions::new()
.read(true)
.open("/tmp/seek_test.txt")
.unwrap();
let pos = file.seek(SeekFrom::Start(7)).unwrap();
assert_eq!(pos, 7);
let mut buf = [0u8; 5];
let count = file.read(&mut buf).unwrap();
assert_eq!(count, 5);
assert_eq!(&buf, b"World");
let pos = file.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(pos, 0);
let mut buf = [0u8; 5];
let count = file.read(&mut buf).unwrap();
assert_eq!(count, 5);
assert_eq!(&buf, b"Hello");
}
#[test_log::test]
fn test_seek_from_end() {
use std::io::{Read as _, Seek as _, SeekFrom};
super::super::reset_fs();
super::create_dir_all("/tmp").unwrap();
super::write("/tmp/seek_end.txt", b"0123456789").unwrap();
let mut file = OpenOptions::new()
.read(true)
.open("/tmp/seek_end.txt")
.unwrap();
let pos = file.seek(SeekFrom::End(0)).unwrap();
assert_eq!(pos, 10, "Seek to end of 10-byte file");
let mut buf = [0u8; 10];
let count = file.read(&mut buf).unwrap();
assert_eq!(count, 0);
}
#[test_log::test]
fn test_seek_from_current() {
use std::io::{Read as _, Seek as _, SeekFrom};
super::super::reset_fs();
super::create_dir_all("/tmp").unwrap();
super::write("/tmp/seek_current.txt", b"0123456789").unwrap();
let mut file = OpenOptions::new()
.read(true)
.open("/tmp/seek_current.txt")
.unwrap();
let mut buf = [0u8; 3];
file.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"012");
let pos = file.seek(SeekFrom::Current(2)).unwrap();
assert_eq!(pos, 5);
file.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"567");
let pos = file.seek(SeekFrom::Current(-4)).unwrap();
assert_eq!(pos, 4);
file.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"456");
}
#[test_log::test]
fn test_seek_past_eof() {
use std::io::{Seek as _, SeekFrom};
super::super::reset_fs();
super::create_dir_all("/tmp").unwrap();
super::write("/tmp/seek_past_eof.txt", b"12345").unwrap();
let mut file = OpenOptions::new()
.read(true)
.open("/tmp/seek_past_eof.txt")
.unwrap();
let pos = file.seek(SeekFrom::Start(100)).unwrap();
assert_eq!(pos, 100);
}
#[test_log::test]
fn test_multiple_handles_same_file() {
use std::io::{Read as _, Write as _};
super::super::reset_fs();
super::create_dir_all("/tmp").unwrap();
super::write("/tmp/shared.txt", b"initial").unwrap();
let mut writer = OpenOptions::new()
.write(true)
.truncate(true)
.open("/tmp/shared.txt")
.unwrap();
let mut reader = OpenOptions::new()
.read(true)
.open("/tmp/shared.txt")
.unwrap();
writer.write_all(b"updated content").unwrap();
drop(writer);
let mut buf = Vec::new();
reader.read_to_end(&mut buf).unwrap();
assert_eq!(buf, b"updated content");
}
#[test_log::test]
fn test_empty_buffer_read() {
super::super::reset_fs();
super::create_dir_all("/tmp").unwrap();
super::write("/tmp/empty_buf.txt", b"content").unwrap();
let mut file = OpenOptions::new()
.read(true)
.open("/tmp/empty_buf.txt")
.unwrap();
let mut buf = [];
let count = file.read(&mut buf).unwrap();
assert_eq!(count, 0);
}
#[test_log::test]
fn test_file_position_after_operations() {
use std::io::{Read as _, Seek as _, SeekFrom, Write as _};
super::super::reset_fs();
super::create_dir_all("/tmp").unwrap();
let mut file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open("/tmp/position_test.txt")
.unwrap();
file.write_all(b"0123456789").unwrap();
let pos = file.stream_position().unwrap();
assert_eq!(pos, 0, "BUG: Write should update position but doesn't");
file.seek(SeekFrom::Start(0)).unwrap();
let mut buf = [0u8; 5];
file.read_exact(&mut buf).unwrap();
let pos = file.stream_position().unwrap();
assert_eq!(pos, 5);
}
#[test_log::test]
#[cfg(all(feature = "sync", feature = "async"))]
fn test_into_async_conversion() {
use std::io::{Seek as _, SeekFrom, Write as _};
super::super::reset_fs();
super::create_dir_all("/tmp").unwrap();
let mut file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open("/tmp/convert_test.txt")
.unwrap();
file.write_all(b"Hello, World!").unwrap();
file.seek(SeekFrom::Start(7)).unwrap();
let position = file.position;
let path = file.path.clone();
let async_file = file.into_async();
assert_eq!(async_file.position, position);
assert_eq!(async_file.path, path);
assert_eq!(async_file.write, true);
}
#[test_log::test]
fn test_remove_empty_directory() {
super::super::reset_fs();
super::create_dir_all("/tmp/empty_dir").unwrap();
assert!(super::super::exists("/tmp/empty_dir"));
super::remove_dir_all("/tmp/empty_dir").unwrap();
assert!(!super::super::exists("/tmp/empty_dir"));
}
#[test_log::test]
fn test_remove_nonexistent_directory() {
super::super::reset_fs();
let result = super::remove_dir_all("/nonexistent");
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
#[test_log::test]
fn test_root_directory_operations() {
super::super::reset_fs();
super::create_dir_all("/").unwrap();
let _entries = super::read_dir_sorted("/").unwrap();
assert!(super::super::exists("/"));
}
#[test_log::test]
fn test_create_file_in_current_directory() {
use std::io::Write as _;
super::super::reset_fs();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.open("./test.txt")
.unwrap();
file.write_all(b"content").unwrap();
drop(file);
let content = super::read_to_string("./test.txt").unwrap();
assert_eq!(content, "content");
}
}
}
#[cfg(feature = "async")]
pub mod unsync {
use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex},
task::Poll,
};
use bytes::BytesMut;
use crate::unsync::OpenOptions;
pub struct File {
pub(crate) path: PathBuf,
pub(crate) data: Arc<Mutex<BytesMut>>,
pub(crate) position: u64,
pub(crate) write: bool,
}
impl File {
#[allow(clippy::future_not_send)]
pub async fn open(path: impl AsRef<Path>) -> std::io::Result<Self> {
OpenOptions::new().read(true).open(path).await
}
#[allow(clippy::future_not_send)]
pub async fn create(path: impl AsRef<Path>) -> std::io::Result<Self> {
OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)
.await
}
#[must_use]
pub const fn options() -> OpenOptions {
OpenOptions::new()
}
#[allow(clippy::unused_async)]
pub async fn metadata(&self) -> std::io::Result<Metadata> {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = self.path.clone();
return switchy_async::task::spawn_blocking(move || {
Ok(std::fs::metadata(&path)?.into())
})
.await
.unwrap();
}
Ok(Metadata {
len: u64::try_from(self.data.lock().unwrap().len()).unwrap_or(0),
is_file: true,
is_dir: false,
is_symlink: false,
})
}
#[cfg(feature = "sync")]
#[must_use]
pub fn into_sync(self) -> crate::sync::File {
crate::sync::File {
path: self.path,
data: self.data,
position: self.position,
write: self.write,
}
}
}
pub use super::Metadata;
impl_file_sync!(File);
impl tokio::io::AsyncRead for File {
fn poll_read(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
use std::io::Read as _;
let dst = buf.initialize_unfilled();
match self.get_mut().read(dst) {
Ok(count) => {
buf.advance(count);
}
Err(e) => return Poll::Ready(Err(e)),
}
Poll::Ready(Ok(()))
}
}
impl tokio::io::AsyncSeek for File {
fn start_seek(
self: std::pin::Pin<&mut Self>,
position: std::io::SeekFrom,
) -> std::io::Result<()> {
use std::io::Seek as _;
self.get_mut().seek(position)?;
Ok(())
}
fn poll_complete(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> Poll<std::io::Result<u64>> {
use std::io::Seek as _;
Poll::Ready(self.get_mut().stream_position())
}
}
impl tokio::io::AsyncWrite for File {
fn poll_write(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
use std::io::Write as _;
Poll::Ready(self.get_mut().write(buf))
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
use std::io::Write as _;
Poll::Ready(self.get_mut().flush())
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Poll::Ready(Ok(()))
}
}
impl OpenOptions {
#[allow(clippy::unused_async, clippy::future_not_send)]
pub async fn open(self, path: impl AsRef<::std::path::Path>) -> ::std::io::Result<File> {
#[cfg(all(feature = "simulator-real-fs", feature = "async",))]
if super::real_fs_support::is_real_fs() {
let path_buf = path.as_ref().to_path_buf();
let options = self.clone();
let std_file = switchy_async::task::spawn_blocking(move || {
let std_options: std::fs::OpenOptions = options.into();
std_options.open(&path_buf)
})
.await
.unwrap()?;
return super::real_fs_support::convert_std_file_to_simulator_async(
std_file, &path, self.read, self.write,
)
.await;
}
Ok(self.into_sync().open(path)?.into_async())
}
}
pub async fn read<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<u8>> {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
return switchy_async::task::spawn_blocking(move || std::fs::read(path))
.await
.unwrap();
}
super::sync::read(path)
}
pub async fn read_to_string<P: AsRef<Path>>(path: P) -> std::io::Result<String> {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
return switchy_async::task::spawn_blocking(move || std::fs::read_to_string(path))
.await
.unwrap();
}
super::sync::read_to_string(path)
}
#[allow(clippy::unused_async)]
pub async fn exists<P: AsRef<Path>>(path: P) -> bool {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
return switchy_async::task::spawn_blocking(move || path.exists())
.await
.unwrap_or(false);
}
super::exists(path)
}
#[allow(clippy::unused_async)]
pub async fn is_file<P: AsRef<Path>>(path: P) -> bool {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
return switchy_async::task::spawn_blocking(move || path.is_file())
.await
.unwrap_or(false);
}
let path_str = path.as_ref().to_string_lossy().to_string();
super::FILES.with_borrow(|files| files.read().unwrap().contains_key(&path_str))
}
#[allow(clippy::unused_async)]
pub async fn is_dir<P: AsRef<Path>>(path: P) -> bool {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
return switchy_async::task::spawn_blocking(move || path.is_dir())
.await
.unwrap_or(false);
}
let path_str = path.as_ref().to_string_lossy().to_string();
super::DIRECTORIES.with_borrow(|dirs| dirs.read().unwrap().contains(&path_str))
}
pub async fn write<P: AsRef<Path> + Send + Sync, C: AsRef<[u8]> + Send>(
path: P,
contents: C,
) -> std::io::Result<()> {
use switchy_async::io::AsyncWriteExt;
#[cfg(all(feature = "simulator-real-fs", feature = "tokio"))]
if super::real_fs_support::is_real_fs() {
return crate::tokio::unsync::write(path, contents).await;
}
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)
.await?;
file.write_all(contents.as_ref()).await?;
Ok(())
}
pub async fn create_dir<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
return switchy_async::task::spawn_blocking(move || std::fs::create_dir(path))
.await
.unwrap();
}
super::sync::create_dir(path)
}
pub async fn create_dir_all<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
return switchy_async::task::spawn_blocking(move || std::fs::create_dir_all(path))
.await
.unwrap();
}
super::sync::create_dir_all(path)
}
pub async fn remove_dir_all<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
return switchy_async::task::spawn_blocking(move || std::fs::remove_dir_all(path))
.await
.unwrap();
}
super::sync::remove_dir_all(path)
}
pub async fn canonicalize<P: AsRef<Path>>(path: P) -> std::io::Result<std::path::PathBuf> {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
return switchy_async::task::spawn_blocking(move || std::fs::canonicalize(path))
.await
.unwrap();
}
super::sync::canonicalize(path)
}
pub struct DirEntry {
path: PathBuf,
file_name: std::ffi::OsString,
file_type_info: super::FileType,
}
impl DirEntry {
pub fn from_std(entry: &std::fs::DirEntry) -> std::io::Result<Self> {
let file_type = entry.file_type()?;
Ok(Self {
path: entry.path(),
file_name: entry.file_name(),
file_type_info: super::FileType {
is_dir: file_type.is_dir(),
is_file: file_type.is_file(),
is_symlink: file_type.is_symlink(),
},
})
}
pub fn new_file(full_path: String, file_name: String) -> std::io::Result<Self> {
Ok(Self {
path: PathBuf::from(full_path),
file_name: std::ffi::OsString::from(file_name),
file_type_info: super::FileType {
is_dir: false,
is_file: true,
is_symlink: false,
},
})
}
pub fn new_dir(full_path: String, dir_name: String) -> std::io::Result<Self> {
Ok(Self {
path: PathBuf::from(full_path),
file_name: std::ffi::OsString::from(dir_name),
file_type_info: super::FileType {
is_dir: true,
is_file: false,
is_symlink: false,
},
})
}
#[must_use]
pub fn path(&self) -> PathBuf {
self.path.clone()
}
#[must_use]
pub fn file_name(&self) -> std::ffi::OsString {
self.file_name.clone()
}
#[allow(clippy::unused_async)]
pub async fn file_type(&self) -> std::io::Result<super::FileType> {
Ok(self.file_type_info.clone())
}
#[allow(clippy::unused_async)]
pub async fn metadata(&self) -> std::io::Result<Metadata> {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = self.path.clone();
return switchy_async::task::spawn_blocking(move || {
Ok(std::fs::metadata(&path)?.into())
})
.await
.unwrap();
}
if self.file_type_info.is_dir() {
Ok(Metadata {
len: 0,
is_file: false,
is_dir: true,
is_symlink: false,
})
} else if self.file_type_info.is_file() {
let path_str = self.path.to_str().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "path is invalid str")
})?;
let len = super::FILES
.with_borrow(|files| {
files
.read()
.unwrap()
.get(path_str)
.map(|data| u64::try_from(data.lock().unwrap().len()).unwrap_or(0))
})
.unwrap_or(0);
Ok(Metadata {
len,
is_file: true,
is_dir: false,
is_symlink: false,
})
} else {
Ok(Metadata {
len: 0,
is_file: false,
is_dir: false,
is_symlink: self.file_type_info.is_symlink(),
})
}
}
}
pub struct ReadDir {
entries: std::vec::IntoIter<DirEntry>,
}
impl ReadDir {
#[allow(clippy::unused_async)]
pub async fn next_entry(&mut self) -> std::io::Result<Option<DirEntry>> {
Ok(self.entries.next())
}
}
#[allow(clippy::unused_async, clippy::needless_collect)]
pub async fn read_dir<P: AsRef<Path>>(path: P) -> std::io::Result<ReadDir> {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
let entries = switchy_async::task::spawn_blocking(move || {
let std_entries = crate::standard::sync::read_dir_sorted(path)?;
std_entries
.into_iter()
.map(|x| DirEntry::from_std(&x))
.collect::<std::io::Result<Vec<_>>>()
})
.await
.unwrap()?;
return Ok(ReadDir {
entries: entries.into_iter(),
});
}
let sync_entries = super::sync::read_dir_sorted(&path)?;
let entries: Vec<DirEntry> = sync_entries
.into_iter()
.map(|e| DirEntry {
path: e.path(),
file_name: e.file_name(),
file_type_info: e.file_type().unwrap(),
})
.collect();
Ok(ReadDir {
entries: entries.into_iter(),
})
}
#[allow(clippy::unused_async)]
pub async fn read_dir_sorted<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<DirEntry>> {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
return switchy_async::task::spawn_blocking(move || {
let std_entries = crate::standard::sync::read_dir_sorted(path)?;
std_entries
.into_iter()
.map(|x| DirEntry::from_std(&x))
.collect::<std::io::Result<Vec<_>>>()
})
.await
.unwrap();
}
let sync_entries = super::sync::read_dir_sorted(&path)?;
Ok(sync_entries
.into_iter()
.map(|e| DirEntry {
path: e.path(),
file_name: e.file_name(),
file_type_info: e.file_type().unwrap(),
})
.collect())
}
#[allow(clippy::unused_async)]
pub async fn walk_dir_sorted<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<DirEntry>> {
#[cfg(all(feature = "simulator-real-fs", feature = "async"))]
if super::real_fs_support::is_real_fs() {
let path = path.as_ref().to_path_buf();
return switchy_async::task::spawn_blocking(move || {
let std_entries = crate::standard::sync::walk_dir_sorted(path)?;
std_entries
.into_iter()
.map(|x| DirEntry::from_std(&x))
.collect::<std::io::Result<Vec<_>>>()
})
.await
.unwrap();
}
let sync_entries = super::sync::walk_dir_sorted(&path)?;
Ok(sync_entries
.into_iter()
.map(|e| DirEntry {
path: e.path(),
file_name: e.file_name(),
file_type_info: e.file_type().unwrap(),
})
.collect())
}
#[cfg(test)]
#[allow(clippy::await_holding_lock)]
mod test {
use std::sync::{Arc, Mutex};
use bytes::BytesMut;
use pretty_assertions::assert_eq;
use tokio::io::AsyncReadExt as _;
use crate::simulator::FILES;
use super::OpenOptions;
#[switchy_async::test]
async fn can_read_empty_file() {
const FILENAME: &str = "unsync::test1";
FILES.with_borrow_mut(|x| {
x.write()
.unwrap()
.insert(FILENAME.to_string(), Arc::new(Mutex::new(BytesMut::new())))
});
let mut file = OpenOptions::new()
.create(true)
.open(FILENAME)
.await
.unwrap();
let mut buf = [0u8; 1024];
let read_count = file.read(&mut buf).await.unwrap();
assert_eq!(read_count, 0);
}
#[switchy_async::test]
async fn can_read_small_bytes_file() {
const FILENAME: &str = "unsync::test2";
FILES.with_borrow_mut(|x| {
x.write().unwrap().insert(
FILENAME.to_string(),
Arc::new(Mutex::new(BytesMut::from(b"hey" as &[u8]))),
)
});
let mut file = OpenOptions::new()
.create(true)
.open(FILENAME)
.await
.unwrap();
let mut buf = [0u8; 1024];
let read_count = file.read(&mut buf).await.unwrap();
assert_eq!(read_count, 3);
}
}
}
#[cfg(test)]
mod real_fs_tests {
use std::io::Write as _;
#[switchy_async::test]
async fn test_simulator_mode_no_real_fs() {
assert!(
!super::real_fs_support::is_real_fs(),
"real_fs should NOT be set in normal test"
);
let content = "test content";
let path = "/simulated/path/file.txt";
super::sync::create_dir_all("/simulated/path").unwrap();
let mut file = crate::sync::OpenOptions::new()
.create(true)
.write(true)
.open(path)
.unwrap();
file.write_all(content.as_bytes()).unwrap();
let read_content = super::sync::read_to_string(path).unwrap();
assert_eq!(read_content, content);
assert!(!std::path::Path::new(path).exists());
}
}
#[cfg(test)]
mod exists_tests {
use super::{exists, reset_fs, sync};
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_exists_returns_false_for_nonexistent_path() {
reset_fs();
assert_eq!(exists("/nonexistent/path"), false);
}
#[test_log::test]
fn test_exists_returns_true_for_directory() {
reset_fs();
sync::create_dir_all("/existing/directory").unwrap();
assert_eq!(exists("/existing/directory"), true);
}
#[test_log::test]
fn test_exists_returns_true_for_file() {
reset_fs();
sync::create_dir_all("/existing").unwrap();
sync::write("/existing/file.txt", b"content").unwrap();
assert_eq!(exists("/existing/file.txt"), true);
}
#[test_log::test]
fn test_exists_with_root_path() {
reset_fs();
sync::create_dir_all("/").unwrap();
assert_eq!(exists("/"), true);
}
}
#[cfg(test)]
mod get_parent_directories_tests {
use super::get_parent_directories;
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_parent_directories_for_deeply_nested_path() {
let parents = get_parent_directories("/a/b/c/d/e");
assert_eq!(parents, vec!["/", "/a", "/a/b", "/a/b/c", "/a/b/c/d"]);
}
#[test_log::test]
fn test_parent_directories_for_single_level() {
let parents = get_parent_directories("/single");
assert_eq!(parents, vec!["/"]);
}
#[test_log::test]
fn test_parent_directories_for_root() {
let parents = get_parent_directories("/");
assert!(parents.is_empty());
}
#[test_log::test]
fn test_parent_directories_preserves_order() {
let parents = get_parent_directories("/usr/local/bin");
assert_eq!(parents, vec!["/", "/usr", "/usr/local"]);
}
}
#[cfg(test)]
mod get_directory_children_tests {
use super::{get_directory_children, reset_fs, sync};
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_children_of_empty_directory() {
reset_fs();
sync::create_dir_all("/empty").unwrap();
let (files, subdirs) = get_directory_children("/empty");
assert!(files.is_empty());
assert!(subdirs.is_empty());
}
#[test_log::test]
fn test_children_with_files_only() {
reset_fs();
sync::create_dir_all("/files_only").unwrap();
sync::write("/files_only/a.txt", b"a").unwrap();
sync::write("/files_only/b.txt", b"b").unwrap();
let (mut files, subdirs) = get_directory_children("/files_only");
files.sort();
assert_eq!(files, vec!["a.txt", "b.txt"]);
assert!(subdirs.is_empty());
}
#[test_log::test]
fn test_children_with_subdirs_only() {
reset_fs();
sync::create_dir_all("/dirs_only/subdir1").unwrap();
sync::create_dir_all("/dirs_only/subdir2").unwrap();
let (files, mut subdirs) = get_directory_children("/dirs_only");
subdirs.sort();
assert!(files.is_empty());
assert_eq!(subdirs, vec!["subdir1", "subdir2"]);
}
#[test_log::test]
fn test_children_mixed_content() {
reset_fs();
sync::create_dir_all("/mixed/sub").unwrap();
sync::write("/mixed/file.txt", b"data").unwrap();
let (files, subdirs) = get_directory_children("/mixed");
assert_eq!(files, vec!["file.txt"]);
assert_eq!(subdirs, vec!["sub"]);
}
#[test_log::test]
fn test_children_of_root_directory() {
reset_fs();
sync::create_dir_all("/root_test").unwrap();
sync::create_dir_all("/another").unwrap();
let (files, mut subdirs) = get_directory_children("/");
subdirs.sort();
assert!(files.is_empty());
assert!(subdirs.contains(&"root_test".to_string()));
assert!(subdirs.contains(&"another".to_string()));
}
#[test_log::test]
fn test_children_excludes_nested_items() {
reset_fs();
sync::create_dir_all("/parent/child").unwrap();
sync::write("/parent/child/nested.txt", b"nested").unwrap();
sync::write("/parent/direct.txt", b"direct").unwrap();
let (files, subdirs) = get_directory_children("/parent");
assert_eq!(files, vec!["direct.txt"]);
assert_eq!(subdirs, vec!["child"]);
assert!(!files.contains(&"nested.txt".to_string()));
}
}
#[cfg(test)]
#[cfg(feature = "async")]
mod async_file_conversion_tests {
use super::{reset_fs, sync};
use pretty_assertions::assert_eq;
use std::io::{Seek as _, SeekFrom, Write as _};
#[test_log::test]
fn test_async_file_into_sync_preserves_state() {
reset_fs();
sync::create_dir_all("/tmp").unwrap();
let mut sync_file = crate::sync::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open("/tmp/async_to_sync.txt")
.unwrap();
sync_file.write_all(b"test data here").unwrap();
sync_file.seek(SeekFrom::Start(5)).unwrap();
let async_file = sync_file.into_async();
assert_eq!(async_file.position, 5);
assert_eq!(async_file.path.to_string_lossy(), "/tmp/async_to_sync.txt");
assert!(async_file.write);
let sync_file_again = async_file.into_sync();
assert_eq!(sync_file_again.position, 5);
assert_eq!(
sync_file_again.path.to_string_lossy(),
"/tmp/async_to_sync.txt"
);
}
}
#[cfg(test)]
#[cfg(feature = "async")]
mod async_operations_tests {
use super::{reset_fs, sync, unsync};
use pretty_assertions::assert_eq;
#[test_log::test(switchy_async::test)]
async fn test_async_write_and_read() {
reset_fs();
sync::create_dir_all("/async_test").unwrap();
unsync::write("/async_test/file.txt", b"async content")
.await
.unwrap();
let content = unsync::read_to_string("/async_test/file.txt")
.await
.unwrap();
assert_eq!(content, "async content");
}
#[test_log::test(switchy_async::test)]
async fn test_async_create_dir_all() {
reset_fs();
unsync::create_dir_all("/async_dirs/nested/deep")
.await
.unwrap();
assert!(super::exists("/async_dirs/nested/deep"));
}
#[test_log::test(switchy_async::test)]
async fn test_async_remove_dir_all() {
reset_fs();
sync::create_dir_all("/to_remove/sub").unwrap();
sync::write("/to_remove/file.txt", b"data").unwrap();
unsync::remove_dir_all("/to_remove").await.unwrap();
assert!(!super::exists("/to_remove"));
assert!(!super::exists("/to_remove/sub"));
assert!(!super::exists("/to_remove/file.txt"));
}
#[test_log::test(switchy_async::test)]
async fn test_async_remove_nonexistent_dir_fails() {
reset_fs();
let result = unsync::remove_dir_all("/does_not_exist").await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
#[test_log::test(switchy_async::test)]
async fn test_async_open_options() {
reset_fs();
sync::create_dir_all("/async_open").unwrap();
let file = crate::unsync::OpenOptions::new()
.create(true)
.write(true)
.open("/async_open/new_file.txt")
.await
.unwrap();
assert!(file.write);
assert_eq!(file.path.to_string_lossy(), "/async_open/new_file.txt");
}
#[test_log::test(switchy_async::test)]
async fn test_async_read_nonexistent_file() {
reset_fs();
let result = unsync::read_to_string("/nonexistent.txt").await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
#[test_log::test(switchy_async::test)]
async fn test_async_write_without_parent_fails() {
reset_fs();
let result = unsync::write("/no/parent/file.txt", b"data").await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
}
#[cfg(test)]
#[cfg(feature = "async")]
mod async_file_io_tests {
use super::{reset_fs, sync};
use pretty_assertions::assert_eq;
use tokio::io::{AsyncReadExt as _, AsyncSeekExt as _, AsyncWriteExt as _};
#[test_log::test(switchy_async::test)]
async fn test_async_read_trait() {
reset_fs();
sync::create_dir_all("/tmp").unwrap();
sync::write("/tmp/async_read.txt", b"Hello, async world!").unwrap();
let mut file = crate::unsync::OpenOptions::new()
.read(true)
.open("/tmp/async_read.txt")
.await
.unwrap();
let mut buf = [0u8; 5];
file.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"Hello");
}
#[test_log::test(switchy_async::test)]
async fn test_async_write_trait() {
reset_fs();
sync::create_dir_all("/tmp").unwrap();
let mut file = crate::unsync::OpenOptions::new()
.create(true)
.write(true)
.open("/tmp/async_write.txt")
.await
.unwrap();
file.write_all(b"async write test").await.unwrap();
file.flush().await.unwrap();
drop(file);
let content = sync::read_to_string("/tmp/async_write.txt").unwrap();
assert_eq!(content, "async write test");
}
#[test_log::test(switchy_async::test)]
async fn test_async_seek_trait() {
reset_fs();
sync::create_dir_all("/tmp").unwrap();
sync::write("/tmp/async_seek.txt", b"0123456789").unwrap();
let mut file = crate::unsync::OpenOptions::new()
.read(true)
.open("/tmp/async_seek.txt")
.await
.unwrap();
let pos = file.seek(std::io::SeekFrom::Start(5)).await.unwrap();
assert_eq!(pos, 5);
let mut buf = [0u8; 5];
file.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"56789");
}
}
pub mod temp_dir {
use std::{
cell::RefCell,
collections::BTreeMap,
ffi::{OsStr, OsString},
path::{Path, PathBuf},
sync::RwLock,
};
struct TempDirState {
cleanup_enabled: bool,
}
thread_local! {
static TEMP_DIRS: RefCell<RwLock<BTreeMap<PathBuf, TempDirState>>> =
const { RefCell::new(RwLock::new(BTreeMap::new())) };
}
pub fn reset_temp_dirs() {
TEMP_DIRS.with_borrow_mut(|x| x.write().unwrap().clear());
}
pub struct TempDir {
path: PathBuf,
cleanup_enabled: bool,
}
impl TempDir {
pub fn new() -> std::io::Result<Self> {
#[cfg(feature = "simulator-real-fs")]
if super::real_fs_support::is_real_fs() {
let real_temp = tempfile::TempDir::new()?;
let path = real_temp.path().to_path_buf();
std::mem::forget(real_temp); return Ok(Self {
path,
cleanup_enabled: true,
});
}
let dir_name = generate_temp_name(None, None, 6);
let path = PathBuf::from("/tmp").join(dir_name);
super::sync::create_dir_all(&path)?;
TEMP_DIRS.with_borrow_mut(|dirs| {
dirs.write().unwrap().insert(
path.clone(),
TempDirState {
cleanup_enabled: true,
},
);
});
Ok(Self {
path,
cleanup_enabled: true,
})
}
pub fn new_in<P: AsRef<Path>>(dir: P) -> std::io::Result<Self> {
#[cfg(feature = "simulator-real-fs")]
if super::real_fs_support::is_real_fs() {
let real_temp = tempfile::TempDir::new_in(dir)?;
let path = real_temp.path().to_path_buf();
std::mem::forget(real_temp);
return Ok(Self {
path,
cleanup_enabled: true,
});
}
let dir_name = generate_temp_name(None, None, 6);
let path = dir.as_ref().join(dir_name);
super::sync::create_dir_all(&path)?;
TEMP_DIRS.with_borrow_mut(|dirs| {
dirs.write().unwrap().insert(
path.clone(),
TempDirState {
cleanup_enabled: true,
},
);
});
Ok(Self {
path,
cleanup_enabled: true,
})
}
pub fn with_prefix<S: AsRef<OsStr>>(prefix: S) -> std::io::Result<Self> {
#[cfg(feature = "simulator-real-fs")]
if super::real_fs_support::is_real_fs() {
let real_temp = tempfile::TempDir::with_prefix(prefix)?;
let path = real_temp.path().to_path_buf();
std::mem::forget(real_temp);
return Ok(Self {
path,
cleanup_enabled: true,
});
}
let dir_name = generate_temp_name(Some(prefix.as_ref()), None, 6);
let path = PathBuf::from("/tmp").join(dir_name);
super::sync::create_dir_all(&path)?;
TEMP_DIRS.with_borrow_mut(|dirs| {
dirs.write().unwrap().insert(
path.clone(),
TempDirState {
cleanup_enabled: true,
},
);
});
Ok(Self {
path,
cleanup_enabled: true,
})
}
pub fn with_suffix<S: AsRef<OsStr>>(suffix: S) -> std::io::Result<Self> {
#[cfg(feature = "simulator-real-fs")]
if super::real_fs_support::is_real_fs() {
let real_temp = tempfile::TempDir::with_suffix(suffix)?;
let path = real_temp.path().to_path_buf();
std::mem::forget(real_temp);
return Ok(Self {
path,
cleanup_enabled: true,
});
}
let dir_name = generate_temp_name(None, Some(suffix.as_ref()), 6);
let path = PathBuf::from("/tmp").join(dir_name);
super::sync::create_dir_all(&path)?;
TEMP_DIRS.with_borrow_mut(|dirs| {
dirs.write().unwrap().insert(
path.clone(),
TempDirState {
cleanup_enabled: true,
},
);
});
Ok(Self {
path,
cleanup_enabled: true,
})
}
pub fn with_prefix_in<S: AsRef<OsStr>, P: AsRef<Path>>(
prefix: S,
dir: P,
) -> std::io::Result<Self> {
#[cfg(feature = "simulator-real-fs")]
if super::real_fs_support::is_real_fs() {
let real_temp = tempfile::TempDir::with_prefix_in(prefix, dir)?;
let path = real_temp.path().to_path_buf();
std::mem::forget(real_temp);
return Ok(Self {
path,
cleanup_enabled: true,
});
}
let dir_name = generate_temp_name(Some(prefix.as_ref()), None, 6);
let path = dir.as_ref().join(dir_name);
super::sync::create_dir_all(&path)?;
TEMP_DIRS.with_borrow_mut(|dirs| {
dirs.write().unwrap().insert(
path.clone(),
TempDirState {
cleanup_enabled: true,
},
);
});
Ok(Self {
path,
cleanup_enabled: true,
})
}
pub fn with_suffix_in<S: AsRef<OsStr>, P: AsRef<Path>>(
suffix: S,
dir: P,
) -> std::io::Result<Self> {
#[cfg(feature = "simulator-real-fs")]
if super::real_fs_support::is_real_fs() {
let real_temp = tempfile::TempDir::with_suffix_in(suffix, dir)?;
let path = real_temp.path().to_path_buf();
std::mem::forget(real_temp);
return Ok(Self {
path,
cleanup_enabled: true,
});
}
let dir_name = generate_temp_name(None, Some(suffix.as_ref()), 6);
let path = dir.as_ref().join(dir_name);
super::sync::create_dir_all(&path)?;
TEMP_DIRS.with_borrow_mut(|dirs| {
dirs.write().unwrap().insert(
path.clone(),
TempDirState {
cleanup_enabled: true,
},
);
});
Ok(Self {
path,
cleanup_enabled: true,
})
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn keep(mut self) -> PathBuf {
self.cleanup_enabled = false;
TEMP_DIRS.with_borrow_mut(|dirs| {
if let Some(state) = dirs.write().unwrap().get_mut(&self.path) {
state.cleanup_enabled = false;
}
});
self.path.clone()
}
#[deprecated = "use TempDir::keep()"]
#[must_use]
pub fn into_path(self) -> PathBuf {
self.keep()
}
pub fn disable_cleanup(&mut self, disable_cleanup: bool) {
self.cleanup_enabled = !disable_cleanup;
TEMP_DIRS.with_borrow_mut(|dirs| {
if let Some(state) = dirs.write().unwrap().get_mut(&self.path) {
state.cleanup_enabled = !disable_cleanup;
}
});
}
pub fn close(mut self) -> std::io::Result<()> {
if !self.cleanup_enabled {
return Ok(());
}
#[cfg(feature = "simulator-real-fs")]
if super::real_fs_support::is_real_fs() {
return std::fs::remove_dir_all(&self.path);
}
TEMP_DIRS.with_borrow_mut(|dirs| {
dirs.write().unwrap().remove(&self.path);
});
super::sync::remove_dir_all(&self.path)?;
self.cleanup_enabled = false;
Ok(())
}
}
impl AsRef<Path> for TempDir {
fn as_ref(&self) -> &Path {
self.path()
}
}
impl std::fmt::Debug for TempDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TempDir")
.field("path", &self.path)
.field("cleanup_enabled", &self.cleanup_enabled)
.finish()
}
}
impl Drop for TempDir {
fn drop(&mut self) {
if self.cleanup_enabled {
#[cfg(feature = "simulator-real-fs")]
if super::real_fs_support::is_real_fs() {
let _ = std::fs::remove_dir_all(&self.path);
return;
}
TEMP_DIRS.with_borrow_mut(|dirs| {
dirs.write().unwrap().remove(&self.path);
});
let _ = super::sync::remove_dir_all(&self.path);
}
}
}
pub struct Builder {
prefix: Option<OsString>,
suffix: Option<OsString>,
rand_bytes: usize,
}
impl Default for Builder {
fn default() -> Self {
Self::new()
}
}
impl Builder {
#[must_use]
pub const fn new() -> Self {
Self {
prefix: None,
suffix: None,
rand_bytes: 6,
}
}
pub fn prefix<S: AsRef<OsStr>>(&mut self, prefix: S) -> &mut Self {
self.prefix = Some(prefix.as_ref().to_os_string());
self
}
pub fn suffix<S: AsRef<OsStr>>(&mut self, suffix: S) -> &mut Self {
self.suffix = Some(suffix.as_ref().to_os_string());
self
}
pub const fn rand_bytes(&mut self, rand: usize) -> &mut Self {
self.rand_bytes = rand;
self
}
pub fn tempdir(&self) -> std::io::Result<TempDir> {
#[cfg(feature = "simulator-real-fs")]
if super::real_fs_support::is_real_fs() {
let mut builder = tempfile::Builder::new();
if let Some(ref prefix) = self.prefix {
builder.prefix(prefix);
}
if let Some(ref suffix) = self.suffix {
builder.suffix(suffix);
}
builder.rand_bytes(self.rand_bytes);
let real_temp = builder.tempdir()?;
let path = real_temp.path().to_path_buf();
std::mem::forget(real_temp);
return Ok(TempDir {
path,
cleanup_enabled: true,
});
}
let dir_name = generate_temp_name(
self.prefix.as_deref(),
self.suffix.as_deref(),
self.rand_bytes,
);
let path = PathBuf::from("/tmp").join(dir_name);
super::sync::create_dir_all(&path)?;
TEMP_DIRS.with_borrow_mut(|dirs| {
dirs.write().unwrap().insert(
path.clone(),
TempDirState {
cleanup_enabled: true,
},
);
});
Ok(TempDir {
path,
cleanup_enabled: true,
})
}
pub fn tempdir_in<P: AsRef<Path>>(&self, dir: P) -> std::io::Result<TempDir> {
#[cfg(feature = "simulator-real-fs")]
if super::real_fs_support::is_real_fs() {
let mut builder = tempfile::Builder::new();
if let Some(ref prefix) = self.prefix {
builder.prefix(prefix);
}
if let Some(ref suffix) = self.suffix {
builder.suffix(suffix);
}
builder.rand_bytes(self.rand_bytes);
let real_temp = builder.tempdir_in(dir)?;
let path = real_temp.path().to_path_buf();
std::mem::forget(real_temp);
return Ok(TempDir {
path,
cleanup_enabled: true,
});
}
let dir_name = generate_temp_name(
self.prefix.as_deref(),
self.suffix.as_deref(),
self.rand_bytes,
);
let path = dir.as_ref().join(dir_name);
super::sync::create_dir_all(&path)?;
TEMP_DIRS.with_borrow_mut(|dirs| {
dirs.write().unwrap().insert(
path.clone(),
TempDirState {
cleanup_enabled: true,
},
);
});
Ok(TempDir {
path,
cleanup_enabled: true,
})
}
}
pub fn tempdir() -> std::io::Result<TempDir> {
TempDir::new()
}
pub fn tempdir_in<P: AsRef<Path>>(dir: P) -> std::io::Result<TempDir> {
TempDir::new_in(dir)
}
fn generate_temp_name(
prefix: Option<&OsStr>,
suffix: Option<&OsStr>,
rand_bytes: usize,
) -> OsString {
let mut name = OsString::new();
if let Some(p) = prefix {
name.push(p);
}
for i in 0..rand_bytes {
#[allow(clippy::cast_possible_truncation)]
let c = char::from(b'a' + (i % 26) as u8);
name.push(c.to_string());
}
if let Some(s) = suffix {
name.push(s);
}
name
}
#[cfg(test)]
mod tests {
use super::*;
use crate::simulator::{exists, reset_fs};
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_generate_temp_name_without_prefix_or_suffix() {
let name = generate_temp_name(None, None, 6);
assert_eq!(name.to_str().unwrap(), "abcdef");
}
#[test_log::test]
fn test_generate_temp_name_with_prefix() {
let name = generate_temp_name(Some(std::ffi::OsStr::new("test-")), None, 4);
assert_eq!(name.to_str().unwrap(), "test-abcd");
}
#[test_log::test]
fn test_generate_temp_name_with_suffix() {
let name = generate_temp_name(None, Some(std::ffi::OsStr::new("-end")), 4);
assert_eq!(name.to_str().unwrap(), "abcd-end");
}
#[test_log::test]
fn test_generate_temp_name_with_both() {
let name = generate_temp_name(
Some(std::ffi::OsStr::new("pre-")),
Some(std::ffi::OsStr::new("-suf")),
3,
);
assert_eq!(name.to_str().unwrap(), "pre-abc-suf");
}
#[test_log::test]
fn test_generate_temp_name_wraps_alphabet() {
let name = generate_temp_name(None, None, 30);
let s = name.to_str().unwrap();
assert!(s.starts_with("abcdefghijklmnopqrstuvwxyzabcd"));
}
#[test_log::test]
fn test_builder_default_values() {
reset_fs();
let builder = Builder::new();
let temp = builder.tempdir().unwrap();
assert!(temp.path().starts_with("/tmp"));
assert!(exists(temp.path()));
}
#[test_log::test]
fn test_builder_with_prefix() {
reset_fs();
let mut builder = Builder::new();
builder.prefix("myprefix-");
let temp = builder.tempdir().unwrap();
let file_name = temp.path().file_name().unwrap().to_str().unwrap();
assert!(file_name.starts_with("myprefix-"));
}
#[test_log::test]
fn test_builder_with_suffix() {
reset_fs();
let mut builder = Builder::new();
builder.suffix("-mysuffix");
let temp = builder.tempdir().unwrap();
let file_name = temp.path().file_name().unwrap().to_str().unwrap();
assert!(file_name.ends_with("-mysuffix"));
}
#[test_log::test]
fn test_builder_with_custom_rand_bytes() {
reset_fs();
let mut builder = Builder::new();
builder.rand_bytes(10);
let temp = builder.tempdir().unwrap();
let file_name = temp.path().file_name().unwrap().to_str().unwrap();
assert!(file_name.contains("abcdefghij"));
}
#[test_log::test]
fn test_builder_tempdir_in() {
reset_fs();
crate::simulator::sync::create_dir_all("/custom").unwrap();
let mut builder = Builder::new();
builder.prefix("test-");
let temp = builder.tempdir_in("/custom").unwrap();
assert!(temp.path().starts_with("/custom"));
assert!(
temp.path()
.file_name()
.unwrap()
.to_str()
.unwrap()
.starts_with("test-")
);
}
#[test_log::test]
fn test_builder_chaining() {
reset_fs();
let temp = Builder::new()
.prefix("start-")
.suffix("-end")
.rand_bytes(3)
.tempdir()
.unwrap();
let file_name = temp.path().file_name().unwrap().to_str().unwrap();
assert_eq!(file_name, "start-abc-end");
}
#[test_log::test]
fn test_disable_cleanup_prevents_deletion() {
reset_fs();
let path = {
let mut temp = TempDir::new().unwrap();
temp.disable_cleanup(true);
temp.path().to_path_buf()
};
assert!(
exists(&path),
"Directory should exist after drop with cleanup disabled"
);
}
#[test_log::test]
fn test_disable_cleanup_can_be_reenabled() {
reset_fs();
let path = {
let mut temp = TempDir::new().unwrap();
temp.disable_cleanup(true);
temp.disable_cleanup(false); temp.path().to_path_buf()
};
assert!(
!exists(&path),
"Directory should be removed when cleanup is re-enabled"
);
}
#[test_log::test]
fn test_tempdir_with_prefix_in() {
reset_fs();
crate::simulator::sync::create_dir_all("/base").unwrap();
let temp = TempDir::with_prefix_in("pfx-", "/base").unwrap();
assert!(temp.path().starts_with("/base"));
assert!(
temp.path()
.file_name()
.unwrap()
.to_str()
.unwrap()
.starts_with("pfx-")
);
}
#[test_log::test]
fn test_tempdir_with_suffix_in() {
reset_fs();
crate::simulator::sync::create_dir_all("/base").unwrap();
let temp = TempDir::with_suffix_in("-sfx", "/base").unwrap();
assert!(temp.path().starts_with("/base"));
assert!(
temp.path()
.file_name()
.unwrap()
.to_str()
.unwrap()
.ends_with("-sfx")
);
}
#[test_log::test]
fn test_tempdir_drop_removes_directory() {
reset_fs();
let path = {
let temp = TempDir::new().unwrap();
let p = temp.path().to_path_buf();
assert!(exists(&p), "Directory should exist before drop");
p
};
assert!(!exists(&path), "Directory should be removed after drop");
}
#[test_log::test]
fn test_tempdir_close_removes_directory() {
reset_fs();
let temp = TempDir::new().unwrap();
let path = temp.path().to_path_buf();
assert!(exists(&path));
temp.close().unwrap();
assert!(!exists(&path), "Directory should be removed after close()");
}
#[test_log::test]
fn test_tempdir_close_with_cleanup_disabled() {
reset_fs();
let mut temp = TempDir::new().unwrap();
let path = temp.path().to_path_buf();
temp.disable_cleanup(true);
temp.close().unwrap();
assert!(
exists(&path),
"Directory should exist after close() with cleanup disabled"
);
}
#[test_log::test]
fn test_tempdir_as_ref() {
reset_fs();
let temp = TempDir::new().unwrap();
let path_ref: &Path = temp.as_ref();
assert_eq!(path_ref, temp.path());
}
#[test_log::test]
fn test_tempdir_debug_format() {
reset_fs();
let temp = TempDir::new().unwrap();
let debug_str = format!("{temp:?}");
assert!(debug_str.contains("TempDir"));
assert!(debug_str.contains("path"));
assert!(debug_str.contains("cleanup_enabled"));
}
#[test_log::test]
fn test_builder_default_impl() {
let builder1 = Builder::new();
let builder2 = Builder::default();
assert!(builder1.prefix.is_none());
assert!(builder2.prefix.is_none());
assert!(builder1.suffix.is_none());
assert!(builder2.suffix.is_none());
assert_eq!(builder1.rand_bytes, builder2.rand_bytes);
}
#[test_log::test]
fn test_reset_temp_dirs_clears_state() {
reset_fs();
let _temp1 = TempDir::new().unwrap();
let _temp2 = TempDir::new().unwrap();
reset_temp_dirs();
}
}
}
#[cfg(test)]
mod init_fs_tests {
use super::{exists, init_minimal_fs, init_standard_fs, init_user_home, reset_fs, sync};
#[test_log::test]
fn test_init_minimal_fs_creates_essential_directories() {
reset_fs();
init_minimal_fs().unwrap();
assert!(exists("/"), "root directory should exist");
assert!(exists("/tmp"), "/tmp directory should exist");
assert!(exists("/home"), "/home directory should exist");
}
#[test_log::test]
fn test_init_standard_fs_creates_fhs_structure() {
reset_fs();
init_standard_fs().unwrap();
assert!(exists("/bin"), "/bin should exist");
assert!(exists("/etc"), "/etc should exist");
assert!(exists("/home"), "/home should exist");
assert!(exists("/lib"), "/lib should exist");
assert!(exists("/opt"), "/opt should exist");
assert!(exists("/root"), "/root should exist");
assert!(exists("/sbin"), "/sbin should exist");
assert!(exists("/tmp"), "/tmp should exist");
assert!(exists("/usr"), "/usr should exist");
assert!(exists("/var"), "/var should exist");
assert!(exists("/usr/bin"), "/usr/bin should exist");
assert!(exists("/usr/lib"), "/usr/lib should exist");
assert!(exists("/usr/local"), "/usr/local should exist");
assert!(exists("/usr/local/bin"), "/usr/local/bin should exist");
assert!(exists("/usr/share"), "/usr/share should exist");
assert!(exists("/var/log"), "/var/log should exist");
assert!(exists("/var/tmp"), "/var/tmp should exist");
assert!(exists("/var/cache"), "/var/cache should exist");
}
#[test_log::test]
fn test_init_user_home_creates_standard_user_directories() {
reset_fs();
init_minimal_fs().unwrap();
init_user_home("testuser").unwrap();
assert!(exists("/home/testuser"), "user home should exist");
assert!(
exists("/home/testuser/.config"),
".config directory should exist"
);
assert!(
exists("/home/testuser/.local"),
".local directory should exist"
);
assert!(
exists("/home/testuser/.local/share"),
".local/share directory should exist"
);
assert!(
exists("/home/testuser/.cache"),
".cache directory should exist"
);
assert!(
exists("/home/testuser/Documents"),
"Documents directory should exist"
);
assert!(
exists("/home/testuser/Downloads"),
"Downloads directory should exist"
);
}
#[test_log::test]
fn test_init_user_home_with_different_usernames() {
reset_fs();
init_minimal_fs().unwrap();
init_user_home("alice").unwrap();
init_user_home("bob").unwrap();
assert!(exists("/home/alice"), "alice home should exist");
assert!(exists("/home/bob"), "bob home should exist");
assert!(
exists("/home/alice/Documents"),
"alice Documents should exist"
);
assert!(exists("/home/bob/Documents"), "bob Documents should exist");
}
#[test_log::test]
fn test_init_standard_fs_allows_listing_usr_subdirs() {
reset_fs();
init_standard_fs().unwrap();
let entries = sync::read_dir_sorted("/usr").unwrap();
let dir_names: Vec<_> = entries.iter().map(sync::DirEntry::file_name).collect();
assert!(
dir_names.iter().any(|n| n == "bin"),
"should contain bin directory"
);
assert!(
dir_names.iter().any(|n| n == "lib"),
"should contain lib directory"
);
assert!(
dir_names.iter().any(|n| n == "local"),
"should contain local directory"
);
assert!(
dir_names.iter().any(|n| n == "share"),
"should contain share directory"
);
}
}
#[cfg(test)]
mod metadata_tests {
use super::{Metadata, reset_fs, sync};
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_metadata_for_empty_file() {
reset_fs();
sync::create_dir_all("/tmp").unwrap();
sync::write("/tmp/empty.txt", b"").unwrap();
let file = sync::File::open("/tmp/empty.txt").unwrap();
let metadata = file.metadata().unwrap();
assert_eq!(metadata.len(), 0, "empty file should have length 0");
assert!(
metadata.is_empty(),
"empty file should return true for is_empty"
);
assert!(metadata.is_file(), "should be a file");
assert!(!metadata.is_dir(), "should not be a directory");
assert!(!metadata.is_symlink(), "should not be a symlink");
}
#[test_log::test]
fn test_metadata_for_file_with_content() {
reset_fs();
sync::create_dir_all("/tmp").unwrap();
sync::write("/tmp/content.txt", b"Hello, World!").unwrap();
let file = sync::File::open("/tmp/content.txt").unwrap();
let metadata = file.metadata().unwrap();
assert_eq!(metadata.len(), 13, "file should have correct length");
assert!(
!metadata.is_empty(),
"non-empty file should return false for is_empty"
);
assert!(metadata.is_file(), "should be a file");
}
#[test_log::test]
fn test_metadata_from_std_fs_metadata() {
let metadata = Metadata {
len: 1024,
is_file: true,
is_dir: false,
is_symlink: false,
};
assert_eq!(metadata.len(), 1024);
assert!(metadata.is_file());
assert!(!metadata.is_dir());
assert!(!metadata.is_symlink());
}
#[test_log::test]
fn test_metadata_for_directory_entry() {
let dir_metadata = Metadata {
len: 0,
is_file: false,
is_dir: true,
is_symlink: false,
};
assert!(dir_metadata.is_dir());
assert!(!dir_metadata.is_file());
assert!(dir_metadata.is_empty());
}
#[test_log::test]
fn test_metadata_for_symlink_entry() {
let symlink_metadata = Metadata {
len: 0,
is_file: false,
is_dir: false,
is_symlink: true,
};
assert!(symlink_metadata.is_symlink());
assert!(!symlink_metadata.is_file());
assert!(!symlink_metadata.is_dir());
}
}
#[cfg(test)]
mod walk_dir_sorted_tests {
use super::{reset_fs, sync};
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_walk_dir_sorted_empty_directory() {
reset_fs();
sync::create_dir_all("/walk_empty").unwrap();
let entries = sync::walk_dir_sorted("/walk_empty").unwrap();
assert!(entries.is_empty(), "empty directory should have no entries");
}
#[test_log::test]
fn test_walk_dir_sorted_flat_directory() {
reset_fs();
sync::create_dir_all("/walk_flat").unwrap();
sync::write("/walk_flat/a.txt", b"a").unwrap();
sync::write("/walk_flat/b.txt", b"b").unwrap();
sync::write("/walk_flat/c.txt", b"c").unwrap();
let entries = sync::walk_dir_sorted("/walk_flat").unwrap();
assert_eq!(entries.len(), 3, "should have 3 files");
let paths: Vec<_> = entries.iter().map(sync::DirEntry::path).collect();
assert_eq!(
paths,
vec![
std::path::PathBuf::from("/walk_flat/a.txt"),
std::path::PathBuf::from("/walk_flat/b.txt"),
std::path::PathBuf::from("/walk_flat/c.txt"),
]
);
}
#[test_log::test]
fn test_walk_dir_sorted_nested_structure() {
reset_fs();
sync::create_dir_all("/walk_nested/dir1").unwrap();
sync::create_dir_all("/walk_nested/dir2").unwrap();
sync::write("/walk_nested/root.txt", b"root").unwrap();
sync::write("/walk_nested/dir1/nested1.txt", b"nested1").unwrap();
sync::write("/walk_nested/dir2/nested2.txt", b"nested2").unwrap();
let entries = sync::walk_dir_sorted("/walk_nested").unwrap();
let paths: Vec<_> = entries.iter().map(sync::DirEntry::path).collect();
assert!(
paths.contains(&std::path::PathBuf::from("/walk_nested/dir1")),
"should contain dir1"
);
assert!(
paths.contains(&std::path::PathBuf::from("/walk_nested/dir2")),
"should contain dir2"
);
assert!(
paths.contains(&std::path::PathBuf::from("/walk_nested/root.txt")),
"should contain root.txt"
);
assert!(
paths.contains(&std::path::PathBuf::from("/walk_nested/dir1/nested1.txt")),
"should contain nested1.txt"
);
assert!(
paths.contains(&std::path::PathBuf::from("/walk_nested/dir2/nested2.txt")),
"should contain nested2.txt"
);
}
#[test_log::test]
fn test_walk_dir_sorted_deeply_nested() {
reset_fs();
sync::create_dir_all("/deep/a/b/c").unwrap();
sync::write("/deep/a/b/c/file.txt", b"deep").unwrap();
let entries = sync::walk_dir_sorted("/deep").unwrap();
let paths: Vec<_> = entries.iter().map(sync::DirEntry::path).collect();
assert!(
paths.contains(&std::path::PathBuf::from("/deep/a")),
"should contain /deep/a"
);
assert!(
paths.contains(&std::path::PathBuf::from("/deep/a/b")),
"should contain /deep/a/b"
);
assert!(
paths.contains(&std::path::PathBuf::from("/deep/a/b/c")),
"should contain /deep/a/b/c"
);
assert!(
paths.contains(&std::path::PathBuf::from("/deep/a/b/c/file.txt")),
"should contain the file"
);
}
#[test_log::test]
fn test_walk_dir_sorted_nonexistent_dir_fails() {
reset_fs();
let result = sync::walk_dir_sorted("/nonexistent_walk");
assert!(result.is_err());
let err = result.err().unwrap();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
#[test_log::test]
fn test_walk_dir_sorted_returns_sorted_paths() {
reset_fs();
sync::create_dir_all("/sorted/z_dir").unwrap();
sync::create_dir_all("/sorted/a_dir").unwrap();
sync::write("/sorted/m_file.txt", b"m").unwrap();
sync::write("/sorted/a_dir/nested.txt", b"nested").unwrap();
let entries = sync::walk_dir_sorted("/sorted").unwrap();
let paths: Vec<_> = entries.iter().map(sync::DirEntry::path).collect();
let mut sorted_paths = paths.clone();
sorted_paths.sort();
assert_eq!(
paths, sorted_paths,
"walk_dir_sorted should return paths in sorted order"
);
}
}
#[cfg(test)]
mod read_dir_sorted_sync_tests {
use super::{reset_fs, sync};
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_read_dir_sorted_empty_directory() {
reset_fs();
sync::create_dir_all("/read_empty").unwrap();
let entries = sync::read_dir_sorted("/read_empty").unwrap();
assert!(entries.is_empty(), "empty directory should have no entries");
}
#[test_log::test]
fn test_read_dir_sorted_files_and_dirs_mixed() {
reset_fs();
sync::create_dir_all("/mixed_content/subdir").unwrap();
sync::write("/mixed_content/file1.txt", b"f1").unwrap();
sync::write("/mixed_content/file2.txt", b"f2").unwrap();
let entries = sync::read_dir_sorted("/mixed_content").unwrap();
assert_eq!(entries.len(), 3, "should have 2 files and 1 directory");
let file_count = entries
.iter()
.filter(|e| e.file_type().unwrap().is_file())
.count();
let dir_count = entries
.iter()
.filter(|e| e.file_type().unwrap().is_dir())
.count();
assert_eq!(file_count, 2, "should have 2 files");
assert_eq!(dir_count, 1, "should have 1 directory");
}
#[test_log::test]
fn test_read_dir_sorted_returns_sorted_by_filename() {
reset_fs();
sync::create_dir_all("/sort_test").unwrap();
sync::write("/sort_test/zebra.txt", b"z").unwrap();
sync::write("/sort_test/apple.txt", b"a").unwrap();
sync::write("/sort_test/mango.txt", b"m").unwrap();
let entries = sync::read_dir_sorted("/sort_test").unwrap();
let filenames: Vec<_> = entries.iter().map(sync::DirEntry::file_name).collect();
assert_eq!(
filenames,
vec![
std::ffi::OsString::from("apple.txt"),
std::ffi::OsString::from("mango.txt"),
std::ffi::OsString::from("zebra.txt"),
]
);
}
#[test_log::test]
fn test_read_dir_sorted_nonexistent_dir_fails() {
reset_fs();
let result = sync::read_dir_sorted("/nonexistent_read");
assert!(result.is_err());
let err = result.err().unwrap();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
#[test_log::test]
fn test_read_dir_sorted_does_not_include_nested() {
reset_fs();
sync::create_dir_all("/parent/child").unwrap();
sync::write("/parent/direct.txt", b"direct").unwrap();
sync::write("/parent/child/nested.txt", b"nested").unwrap();
let entries = sync::read_dir_sorted("/parent").unwrap();
let filenames: Vec<_> = entries.iter().map(sync::DirEntry::file_name).collect();
assert!(
filenames.contains(&std::ffi::OsString::from("direct.txt")),
"should contain direct.txt"
);
assert!(
filenames.contains(&std::ffi::OsString::from("child")),
"should contain child directory"
);
assert!(
!filenames.contains(&std::ffi::OsString::from("nested.txt")),
"should NOT contain nested.txt"
);
}
}
#[cfg(test)]
#[cfg(feature = "async")]
mod async_read_dir_tests {
use super::{reset_fs, sync, unsync};
use pretty_assertions::assert_eq;
#[test_log::test(switchy_async::test)]
async fn test_async_read_dir_iteration() {
reset_fs();
sync::create_dir_all("/async_iter").unwrap();
sync::write("/async_iter/a.txt", b"a").unwrap();
sync::write("/async_iter/b.txt", b"b").unwrap();
let mut read_dir = unsync::read_dir("/async_iter").await.unwrap();
let mut entries = Vec::new();
while let Some(entry) = read_dir.next_entry().await.unwrap() {
entries.push(entry);
}
assert_eq!(entries.len(), 2, "should have 2 entries");
}
#[test_log::test(switchy_async::test)]
async fn test_async_read_dir_empty() {
reset_fs();
sync::create_dir_all("/async_empty").unwrap();
let mut read_dir = unsync::read_dir("/async_empty").await.unwrap();
let entry = read_dir.next_entry().await.unwrap();
assert!(entry.is_none(), "empty directory should return None");
}
#[test_log::test(switchy_async::test)]
async fn test_async_dir_entry_file_type() {
reset_fs();
sync::create_dir_all("/async_type/subdir").unwrap();
sync::write("/async_type/file.txt", b"content").unwrap();
let entries = unsync::read_dir_sorted("/async_type").await.unwrap();
let file_entry = entries
.iter()
.find(|e| e.file_name() == "file.txt")
.unwrap();
let dir_entry = entries.iter().find(|e| e.file_name() == "subdir").unwrap();
let file_type = file_entry.file_type().await.unwrap();
assert!(file_type.is_file(), "file.txt should be a file");
assert!(!file_type.is_dir(), "file.txt should not be a directory");
let dir_type = dir_entry.file_type().await.unwrap();
assert!(dir_type.is_dir(), "subdir should be a directory");
assert!(!dir_type.is_file(), "subdir should not be a file");
}
#[test_log::test(switchy_async::test)]
async fn test_async_dir_entry_metadata() {
reset_fs();
sync::create_dir_all("/async_meta").unwrap();
sync::write("/async_meta/test.txt", b"test content").unwrap();
let entries = unsync::read_dir_sorted("/async_meta").await.unwrap();
let file_entry = entries
.iter()
.find(|e| e.file_name() == "test.txt")
.unwrap();
let metadata = file_entry.metadata().await.unwrap();
assert_eq!(
metadata.len(),
12,
"file should have correct length (12 bytes)"
);
assert!(metadata.is_file(), "should be a file");
}
#[test_log::test(switchy_async::test)]
async fn test_async_dir_entry_path_and_file_name() {
reset_fs();
sync::create_dir_all("/async_paths").unwrap();
sync::write("/async_paths/example.txt", b"data").unwrap();
let entries = unsync::read_dir_sorted("/async_paths").await.unwrap();
let entry = &entries[0];
assert_eq!(
entry.path(),
std::path::PathBuf::from("/async_paths/example.txt")
);
assert_eq!(entry.file_name(), std::ffi::OsString::from("example.txt"));
}
#[test_log::test(switchy_async::test)]
async fn test_async_walk_dir_sorted() {
reset_fs();
sync::create_dir_all("/async_walk/sub").unwrap();
sync::write("/async_walk/root.txt", b"root").unwrap();
sync::write("/async_walk/sub/nested.txt", b"nested").unwrap();
let entries = unsync::walk_dir_sorted("/async_walk").await.unwrap();
let paths: Vec<_> = entries.iter().map(unsync::DirEntry::path).collect();
assert!(
paths.contains(&std::path::PathBuf::from("/async_walk/sub")),
"should contain sub directory"
);
assert!(
paths.contains(&std::path::PathBuf::from("/async_walk/root.txt")),
"should contain root.txt"
);
assert!(
paths.contains(&std::path::PathBuf::from("/async_walk/sub/nested.txt")),
"should contain nested.txt"
);
}
#[test_log::test(switchy_async::test)]
async fn test_async_dir_entry_metadata_for_directory() {
reset_fs();
sync::create_dir_all("/async_dir_meta/subdir").unwrap();
let entries = unsync::read_dir_sorted("/async_dir_meta").await.unwrap();
let dir_entry = &entries[0];
let metadata = dir_entry.metadata().await.unwrap();
assert!(metadata.is_dir(), "should be a directory");
assert_eq!(metadata.len(), 0, "directory should have length 0");
}
}
#[cfg(test)]
mod dir_entry_sync_tests {
use super::{reset_fs, sync};
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_dir_entry_new_file() {
let entry =
sync::DirEntry::new_file("/path/to/file.txt".to_string(), "file.txt".to_string())
.unwrap();
assert_eq!(entry.path(), std::path::PathBuf::from("/path/to/file.txt"));
assert_eq!(entry.file_name(), std::ffi::OsString::from("file.txt"));
assert!(entry.file_type().unwrap().is_file());
assert!(!entry.file_type().unwrap().is_dir());
}
#[test_log::test]
fn test_dir_entry_new_dir() {
let entry = sync::DirEntry::new_dir("/path/to/dir".to_string(), "dir".to_string()).unwrap();
assert_eq!(entry.path(), std::path::PathBuf::from("/path/to/dir"));
assert_eq!(entry.file_name(), std::ffi::OsString::from("dir"));
assert!(entry.file_type().unwrap().is_dir());
assert!(!entry.file_type().unwrap().is_file());
}
#[test_log::test]
fn test_dir_entry_file_type_accessor() {
reset_fs();
sync::create_dir_all("/entry_test/subdir").unwrap();
sync::write("/entry_test/file.txt", b"content").unwrap();
let entries = sync::read_dir_sorted("/entry_test").unwrap();
for entry in entries {
let file_type = entry.file_type().unwrap();
let type_count = [
file_type.is_file(),
file_type.is_dir(),
file_type.is_symlink(),
]
.iter()
.filter(|&&x| x)
.count();
assert_eq!(type_count, 1, "each entry should have exactly one type");
}
}
}
#[cfg(test)]
mod file_create_and_open_tests {
use super::{reset_fs, sync};
use pretty_assertions::assert_eq;
use std::io::{Read as _, Write as _};
#[test_log::test]
fn test_file_create_new_file() {
reset_fs();
sync::create_dir_all("/tmp").unwrap();
let mut file = sync::File::create("/tmp/new_file.txt").unwrap();
file.write_all(b"created content").unwrap();
drop(file);
let content = sync::read_to_string("/tmp/new_file.txt").unwrap();
assert_eq!(content, "created content");
}
#[test_log::test]
fn test_file_create_truncates_existing() {
reset_fs();
sync::create_dir_all("/tmp").unwrap();
sync::write("/tmp/existing.txt", b"original content that is long").unwrap();
let mut file = sync::File::create("/tmp/existing.txt").unwrap();
file.write_all(b"short").unwrap();
drop(file);
let content = sync::read_to_string("/tmp/existing.txt").unwrap();
assert_eq!(content, "short");
}
#[test_log::test]
fn test_file_open_reads_content() {
reset_fs();
sync::create_dir_all("/tmp").unwrap();
sync::write("/tmp/read_test.txt", b"readable content").unwrap();
let mut file = sync::File::open("/tmp/read_test.txt").unwrap();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
assert_eq!(content, "readable content");
}
#[test_log::test]
fn test_file_open_nonexistent_fails() {
reset_fs();
let result = sync::File::open("/nonexistent/file.txt");
assert!(result.is_err());
let err = result.err().unwrap();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
#[test_log::test]
fn test_file_create_without_parent_fails() {
reset_fs();
let result = sync::File::create("/no/parent/file.txt");
assert!(result.is_err());
let err = result.err().unwrap();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
#[test_log::test]
fn test_file_options_returns_open_options() {
let options = sync::File::options();
let _ = options.read(true).write(true).create(true);
}
}
#[cfg(test)]
mod normalize_path_tests {
use super::normalize_path;
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_normalize_absolute_path_with_single_dot() {
assert_eq!(normalize_path("/a/./b"), "/a/b");
assert_eq!(normalize_path("/./a"), "/a");
assert_eq!(normalize_path("/a/."), "/a");
assert_eq!(normalize_path("/././."), "/");
}
#[test_log::test]
fn test_normalize_absolute_path_with_double_dots() {
assert_eq!(normalize_path("/a/b/../c"), "/a/c");
assert_eq!(normalize_path("/a/b/c/../../d"), "/a/d");
assert_eq!(normalize_path("/a/../b"), "/b");
}
#[test_log::test]
fn test_normalize_absolute_path_double_dots_at_root() {
assert_eq!(normalize_path("/.."), "/");
assert_eq!(normalize_path("/../a"), "/a");
assert_eq!(normalize_path("/../../a/b"), "/a/b");
}
#[test_log::test]
fn test_normalize_absolute_path_with_trailing_slashes() {
assert_eq!(normalize_path("/a/b/"), "/a/b");
assert_eq!(normalize_path("/a//b"), "/a/b");
assert_eq!(normalize_path("///a///b///"), "/a/b");
}
#[test_log::test]
fn test_normalize_relative_path_with_single_dot() {
assert_eq!(normalize_path("./a"), "a");
assert_eq!(normalize_path("a/./b"), "a/b");
assert_eq!(normalize_path("."), ".");
}
#[test_log::test]
fn test_normalize_relative_path_with_double_dots() {
assert_eq!(normalize_path("../a"), "../a");
assert_eq!(normalize_path("../../a"), "../../a");
assert_eq!(normalize_path("a/b/../../c"), "c");
assert_eq!(normalize_path("a/../b"), "b");
}
#[test_log::test]
fn test_normalize_relative_path_double_dots_preserved() {
assert_eq!(normalize_path("a/../../b"), "../b");
assert_eq!(normalize_path("../.."), "../..");
}
#[test_log::test]
fn test_normalize_empty_path() {
assert_eq!(normalize_path(""), ".");
}
#[test_log::test]
fn test_normalize_root_path() {
assert_eq!(normalize_path("/"), "/");
}
#[test_log::test]
fn test_normalize_mixed_dots() {
assert_eq!(normalize_path("/a/./b/../c/./d"), "/a/c/d");
assert_eq!(normalize_path("./a/./b/../c"), "a/c");
}
}
#[cfg(test)]
mod canonicalize_tests {
use super::{reset_fs, sync};
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_canonicalize_existing_directory() {
reset_fs();
sync::create_dir_all("/test/path/to/dir").unwrap();
let result = sync::canonicalize("/test/path/to/dir").unwrap();
assert_eq!(result.to_str().unwrap(), "/test/path/to/dir");
}
#[test_log::test]
fn test_canonicalize_existing_file() {
reset_fs();
sync::create_dir_all("/test").unwrap();
sync::write("/test/file.txt", b"content").unwrap();
let result = sync::canonicalize("/test/file.txt").unwrap();
assert_eq!(result.to_str().unwrap(), "/test/file.txt");
}
#[test_log::test]
fn test_canonicalize_path_with_dots() {
reset_fs();
sync::create_dir_all("/a/b/c").unwrap();
let result = sync::canonicalize("/a/b/../b/./c").unwrap();
assert_eq!(result.to_str().unwrap(), "/a/b/c");
}
#[test_log::test]
fn test_canonicalize_path_with_double_slashes() {
reset_fs();
sync::create_dir_all("/test/dir").unwrap();
let result = sync::canonicalize("/test//dir").unwrap();
assert_eq!(result.to_str().unwrap(), "/test/dir");
}
#[test_log::test]
fn test_canonicalize_nonexistent_path_fails() {
reset_fs();
let result = sync::canonicalize("/nonexistent/path");
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
#[test_log::test]
fn test_canonicalize_root() {
reset_fs();
sync::create_dir_all("/").unwrap();
let result = sync::canonicalize("/").unwrap();
assert_eq!(result.to_str().unwrap(), "/");
}
#[test_log::test]
fn test_canonicalize_double_dots_past_root() {
reset_fs();
sync::create_dir_all("/existing").unwrap();
let result = sync::canonicalize("/../existing").unwrap();
assert_eq!(result.to_str().unwrap(), "/existing");
}
}
#[cfg(test)]
mod create_dir_tests {
use super::{exists, reset_fs, sync};
use pretty_assertions::assert_eq;
#[test_log::test]
fn test_create_dir_single_level() {
reset_fs();
sync::create_dir_all("/").unwrap();
sync::create_dir("/toplevel").unwrap();
assert!(exists("/toplevel"));
}
#[test_log::test]
fn test_create_dir_with_existing_parent() {
reset_fs();
sync::create_dir_all("/parent").unwrap();
sync::create_dir("/parent/child").unwrap();
assert!(exists("/parent/child"));
}
#[test_log::test]
fn test_create_dir_without_parent_fails() {
reset_fs();
let result = sync::create_dir("/missing/child");
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
#[test_log::test]
fn test_create_dir_nested_without_parent_fails() {
reset_fs();
sync::create_dir_all("/a").unwrap();
let result = sync::create_dir("/a/b/c");
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
#[test_log::test]
fn test_create_dir_root() {
reset_fs();
sync::create_dir("/").unwrap();
assert!(exists("/"));
}
#[test_log::test]
fn test_create_dir_with_trailing_slash() {
reset_fs();
sync::create_dir_all("/parent").unwrap();
sync::create_dir("/parent/child/").unwrap();
assert!(exists("/parent/child"));
}
#[test_log::test]
fn test_create_dir_idempotent() {
reset_fs();
sync::create_dir_all("/parent").unwrap();
sync::create_dir("/parent/child").unwrap();
sync::create_dir("/parent/child").unwrap();
assert!(exists("/parent/child"));
}
}
#[cfg(test)]
#[cfg(feature = "async")]
mod async_is_file_is_dir_tests {
use super::{reset_fs, sync, unsync};
#[test_log::test(switchy_async::test)]
async fn test_is_file_returns_true_for_file() {
reset_fs();
sync::create_dir_all("/test").unwrap();
sync::write("/test/file.txt", b"content").unwrap();
assert!(unsync::is_file("/test/file.txt").await);
}
#[test_log::test(switchy_async::test)]
async fn test_is_file_returns_false_for_directory() {
reset_fs();
sync::create_dir_all("/test/subdir").unwrap();
assert!(!unsync::is_file("/test/subdir").await);
}
#[test_log::test(switchy_async::test)]
async fn test_is_file_returns_false_for_nonexistent() {
reset_fs();
assert!(!unsync::is_file("/nonexistent/file.txt").await);
}
#[test_log::test(switchy_async::test)]
async fn test_is_dir_returns_true_for_directory() {
reset_fs();
sync::create_dir_all("/test/subdir").unwrap();
assert!(unsync::is_dir("/test/subdir").await);
}
#[test_log::test(switchy_async::test)]
async fn test_is_dir_returns_false_for_file() {
reset_fs();
sync::create_dir_all("/test").unwrap();
sync::write("/test/file.txt", b"content").unwrap();
assert!(!unsync::is_dir("/test/file.txt").await);
}
#[test_log::test(switchy_async::test)]
async fn test_is_dir_returns_false_for_nonexistent() {
reset_fs();
assert!(!unsync::is_dir("/nonexistent/dir").await);
}
#[test_log::test(switchy_async::test)]
async fn test_is_file_with_invalid_path() {
reset_fs();
assert!(!unsync::is_file("").await);
}
#[test_log::test(switchy_async::test)]
async fn test_is_dir_with_root() {
reset_fs();
sync::create_dir_all("/").unwrap();
assert!(unsync::is_dir("/").await);
}
}
#[cfg(test)]
#[cfg(feature = "async")]
mod async_canonicalize_tests {
use super::{reset_fs, sync, unsync};
use pretty_assertions::assert_eq;
#[test_log::test(switchy_async::test)]
async fn test_async_canonicalize_existing_path() {
reset_fs();
sync::create_dir_all("/async/path/here").unwrap();
let result = unsync::canonicalize("/async/path/here").await.unwrap();
assert_eq!(result.to_str().unwrap(), "/async/path/here");
}
#[test_log::test(switchy_async::test)]
async fn test_async_canonicalize_with_dots() {
reset_fs();
sync::create_dir_all("/a/b").unwrap();
let result = unsync::canonicalize("/a/./b/../b").await.unwrap();
assert_eq!(result.to_str().unwrap(), "/a/b");
}
#[test_log::test(switchy_async::test)]
async fn test_async_canonicalize_nonexistent_fails() {
reset_fs();
let result = unsync::canonicalize("/does/not/exist").await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
}
#[cfg(test)]
#[cfg(feature = "async")]
mod async_create_dir_tests {
use super::{exists, reset_fs, sync, unsync};
use pretty_assertions::assert_eq;
#[test_log::test(switchy_async::test)]
async fn test_async_create_dir_with_parent() {
reset_fs();
sync::create_dir_all("/async_parent").unwrap();
unsync::create_dir("/async_parent/child").await.unwrap();
assert!(exists("/async_parent/child"));
}
#[test_log::test(switchy_async::test)]
async fn test_async_create_dir_without_parent_fails() {
reset_fs();
let result = unsync::create_dir("/no_parent/child").await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
}