use std::io;
use std::path::{Path, PathBuf};
pub async fn blocking<F, T>(f: F) -> io::Result<T>
where
F: FnOnce() -> io::Result<T> + Send + 'static,
T: Send + 'static,
{
match crate::runtime::task::spawn_blocking(f).await {
Ok(result) => result,
Err(e) => Err(io::Error::other(format!(
"filesystem worker did not complete: {e}"
))),
}
}
pub async fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
let path = path.as_ref().to_path_buf();
blocking(move || std::fs::read_to_string(path)).await
}
pub async fn write(path: impl AsRef<Path>, contents: impl Into<Vec<u8>>) -> io::Result<()> {
let path = path.as_ref().to_path_buf();
let contents = contents.into();
blocking(move || std::fs::write(path, contents)).await
}
pub async fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> {
let from = from.as_ref().to_path_buf();
let to = to.as_ref().to_path_buf();
blocking(move || std::fs::copy(from, to)).await
}
pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
let from = from.as_ref().to_path_buf();
let to = to.as_ref().to_path_buf();
blocking(move || std::fs::rename(from, to)).await
}
pub async fn canonicalize(path: impl AsRef<Path>) -> io::Result<PathBuf> {
let path = path.as_ref().to_path_buf();
blocking(move || std::fs::canonicalize(path)).await
}
#[cfg(test)]
mod tests {
use super::*;
#[epics_macros_rs::epics_test]
async fn a_round_trip_goes_through_the_seam() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("seam.txt");
write(&path, "hello").await.unwrap();
assert_eq!(read_to_string(&path).await.unwrap(), "hello");
}
#[epics_macros_rs::epics_test]
async fn a_missing_file_keeps_its_error_kind() {
let dir = tempfile::tempdir().unwrap();
let e = read_to_string(dir.path().join("absent")).await.unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::NotFound);
}
#[epics_macros_rs::epics_test]
async fn rename_and_copy_move_the_bytes() {
let dir = tempfile::tempdir().unwrap();
let a = dir.path().join("a");
let b = dir.path().join("b");
let c = dir.path().join("c");
write(&a, "payload").await.unwrap();
rename(&a, &b).await.unwrap();
assert!(!a.exists());
copy(&b, &c).await.unwrap();
assert_eq!(read_to_string(&c).await.unwrap(), "payload");
}
#[epics_macros_rs::epics_test]
async fn canonicalize_resolves_to_an_absolute_path() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("real");
write(&path, "x").await.unwrap();
assert!(canonicalize(&path).await.unwrap().is_absolute());
}
#[epics_macros_rs::epics_test]
async fn a_composite_closure_returns_its_own_error() {
let e = blocking(|| std::fs::read_to_string("/definitely/not/here"))
.await
.unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::NotFound);
}
#[test]
fn the_seam_does_not_name_the_crate_it_replaces() {
let src = include_str!("fs.rs");
let code = src
.split_once("use std::io;")
.expect("the module still starts its code with the imports")
.1;
let production = code
.split_once(concat!("#[cfg", "(test)]"))
.expect("the test module is still the tail of this file")
.0;
assert!(!production.contains(concat!("tokio", "::fs")));
}
}