diff --git a/Cargo.toml b/Cargo.toml index 35deaae..e017a77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ path-clean = "1.0" reqwest = { version = "0.12", features = [ "json" ] } url = "2.5" tempfile = "3.10" +tokio = { version = "1.41", features = [ "sync" ] } [dev-dependencies] assert2 = "0.3" diff --git a/src/fs/dir.rs b/src/fs/dir.rs index 69d39a0..8121ca1 100644 --- a/src/fs/dir.rs +++ b/src/fs/dir.rs @@ -1,7 +1,7 @@ // use crate::fs::{DirItem, DirItemIterator, Result}; -use super::{DirHandle, Error, FileHandle, PathHandle, PathMarker}; +use super::{DirHandle, Error, PathHandle, PathMarker}; impl DirHandle { /// Creates a new, empty directory at the path @@ -93,19 +93,6 @@ impl DirHandle { std::fs::remove_dir_all(self.as_pathbuf()).map_err(Error::Io) } } -impl TryFrom> for FileHandle { - type Error = crate::fs::Error; - - fn try_from(path: PathHandle) -> std::result::Result { - match path.as_file() { - Ok(Some(dir)) => Ok(dir.clone()), - Ok(None) => Err(crate::fs::Error::NotADirectory { - path: path.as_pathbuf(), - }), - Err(err) => Err(err), - } - } -} impl TryFrom> for DirHandle { type Error = crate::fs::Error; diff --git a/src/fs/dir_item.rs b/src/fs/dir_item.rs index c3bad62..daad67d 100644 --- a/src/fs/dir_item.rs +++ b/src/fs/dir_item.rs @@ -7,7 +7,7 @@ use std::{ use super::Error; /// Represents an item in a directory -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum DirItem { File(PathBuf), Dir(PathBuf), diff --git a/src/fs/file.rs b/src/fs/file.rs index 99ba5ba..cd4b8d7 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -1,7 +1,7 @@ // use crate::fs::Result; -use super::{reader::Reader, Error, FileHandle}; +use super::{reader::Reader, Error, FileHandle, PathHandle, PathMarker}; impl FileHandle { /// Returns a [Reader] for the file. @@ -99,3 +99,16 @@ impl FileHandle { std::fs::hard_link(self.as_pathbuf(), dest.as_pathbuf()).map_err(Error::Io) } } +impl TryFrom> for FileHandle { + type Error = crate::fs::Error; + + fn try_from(path: PathHandle) -> std::result::Result { + match path.as_file() { + Ok(Some(dir)) => Ok(dir.clone()), + Ok(None) => Err(crate::fs::Error::NotAFile { + path: path.as_pathbuf(), + }), + Err(err) => Err(err), + } + } +} diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 3906eae..df3a4f5 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -98,3 +98,25 @@ pub fn new(base: impl Into) -> FileSystem { pub fn temp() -> Result { temp::TempFileSystem::new() } + +#[cfg(test)] +mod tests { + use super::*; + + fn is_normal() {} + + #[test] + fn normal_types() { + is_normal::(); + is_normal::(); + is_normal::(); + is_normal::(); + is_normal::>(); + is_normal::(); + is_normal::(); + is_normal::(); + is_normal::(); + is_normal::(); + is_normal::>(); + } +} diff --git a/src/fs/path.rs b/src/fs/path.rs index eb44442..aa2ea8c 100644 --- a/src/fs/path.rs +++ b/src/fs/path.rs @@ -12,24 +12,24 @@ use super::{DirHandle, FileHandle, PathHandle}; pub trait PathType {} /// Path marker for the type of [PathReal]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct PathMarker; impl PathType for PathMarker {} /// File marker for the type of [PathReal]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct FileMarker; impl PathType for FileMarker {} /// Dir marker for the type of [PathReal]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct DirMarker; impl PathType for DirMarker {} /// Represents a path in the filesystem. /// /// It can be a simple path, or it can be a file or a directory. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct PathReal { base: PathBuf, path: PathBuf, diff --git a/src/fs/reader.rs b/src/fs/reader.rs index e17c960..93e2ad2 100644 --- a/src/fs/reader.rs +++ b/src/fs/reader.rs @@ -6,6 +6,7 @@ use crate::fs::Result; use super::Error; /// A reader for a file. +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Reader { contents: String, } diff --git a/src/fs/system.rs b/src/fs/system.rs index 8598668..e98cf7c 100644 --- a/src/fs/system.rs +++ b/src/fs/system.rs @@ -9,7 +9,7 @@ use super::{ }; /// Represents to base of a section of a file system. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct FileSystem { base: PathBuf, } diff --git a/src/net/mod.rs b/src/net/mod.rs index db17f20..35706d8 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -134,10 +134,14 @@ //! //! ```rust //! # use kxio::net; +//! # #[tokio::main] +//! # async fn main() -> net::Result<()> { //! # let mock_net = net::mock(); //! # let net = net::Net::from(mock_net); -//! let mock_net = kxio::net::MockNet::try_from(net).expect("recover mock"); +//! let mock_net = kxio::net::MockNet::try_from(net).await.expect("recover mock"); //! mock_net.reset(); +//! # Ok(()) +//! # } //! ```` //! //! ## Error Handling @@ -167,5 +171,5 @@ pub const fn new() -> Net { /// Creates a new `MockNet` for use in tests. pub fn mock() -> MockNet { - Net::mock() + Default::default() } diff --git a/src/net/system.rs b/src/net/system.rs index 7c59a05..2d257de 100644 --- a/src/net/system.rs +++ b/src/net/system.rs @@ -1,7 +1,9 @@ // -use std::{cell::RefCell, rc::Rc}; + +use std::{cell::RefCell, ops::Deref, rc::Rc, sync::Arc}; use reqwest::{Body, Client}; +use tokio::sync::Mutex; use crate::net::{Method, Request, RequestBuilder, Response, Url}; @@ -29,6 +31,7 @@ pub enum MatchOn { /// A planned request and the response to return /// /// Contains a list of the criteria that a request must meet before being considered a match. +#[derive(Debug)] struct Plan { match_request: Vec, response: Response, @@ -57,22 +60,15 @@ impl Plan { } /// An abstraction for the network -#[derive(Clone)] +#[derive(Debug, Clone, Default)] pub struct Net { - plans: Option>>, + plans: Option>>>, } impl Net { /// Creates a new unmocked [Net] for creating real network requests. pub(super) const fn new() -> Self { Self { plans: None } } - - /// Creats a new [MockNet] for use in tests. - pub(super) const fn mock() -> MockNet { - MockNet { - plans: RefCell::new(vec![]), - } - } } impl Net { /// Helper to create a default [Client]. @@ -114,27 +110,28 @@ impl Net { }; let request = request.into().build()?; let index = plans + .lock() + .await + .deref() .borrow() .iter() .position(|plan| plan.matches(&request)); match index { Some(i) => { - let response = plans.borrow_mut().remove(i).response; + let response = plans.lock().await.borrow_mut().remove(i).response; Ok(response) } None => Err(Error::UnexpectedMockRequest(request)), } } } -impl TryFrom for MockNet { - type Error = super::Error; - - fn try_from(net: Net) -> std::result::Result { +impl MockNet { + pub async fn try_from(net: Net) -> std::result::Result { match &net.plans { Some(plans) => Ok(MockNet { - plans: RefCell::new(plans.take()), + plans: Rc::new(RefCell::new(plans.lock().await.take())), }), - None => Err(Self::Error::NetIsNotAMock), + None => Err(super::Error::NetIsNotAMock), } } } @@ -150,7 +147,8 @@ impl TryFrom for MockNet { /// ```rust /// use kxio::net::{Method, Url}; /// # use kxio::net::Result; -/// # fn run() -> Result<()> { +/// # #[tokio::main] +/// # async fn run() -> Result<()> { /// let mock_net = kxio::net::mock(); /// let client = mock_net.client(); /// // define an expected requet, and the response that should be returned @@ -162,13 +160,14 @@ impl TryFrom for MockNet { /// /// // In some rare cases you don't want to assert that all expected requests were made. /// // You should recover the `MockNet` from the `Net` and `MockNet::reset` it. -/// let mock_net = kxio::net::MockNet::try_from(net)?; +/// let mock_net = kxio::net::MockNet::try_from(net).await?; /// mock_net.reset(); // only if explicitly needed /// # Ok(()) /// # } /// ``` +#[derive(Debug, Clone, Default)] pub struct MockNet { - plans: RefCell, + plans: Rc>, } impl MockNet { /// Helper to create a default [Client]. @@ -222,10 +221,11 @@ impl MockNet { /// /// ```rust /// # use kxio::net::Result; - /// # fn run() -> Result<()> { + /// # #[tokio::main] + /// # async fn run() -> Result<()> { /// # let mock_net = kxio::net::mock(); /// # let net: kxio::net::Net = mock_net.into(); - /// let mock_net = kxio::net::MockNet::try_from(net)?; + /// let mock_net = kxio::net::MockNet::try_from(net).await?; /// mock_net.reset(); // only if explicitly needed /// # Ok(()) /// # } @@ -239,7 +239,7 @@ impl From for Net { Self { // keep the original `inner` around to allow it's Drop impelmentation to run when we go // out of scope at the end of the test - plans: Some(Rc::new(RefCell::new(mock_net.plans.take()))), + plans: Some(Arc::new(Mutex::new(RefCell::new(mock_net.plans.take())))), } } } @@ -252,11 +252,12 @@ impl Drop for MockNet { impl Drop for Net { fn drop(&mut self) { if let Some(plans) = &self.plans { - assert!(plans.borrow().is_empty()) + assert!(plans.try_lock().expect("lock plans").take().is_empty()) } } } +#[derive(Debug, Clone, PartialEq, Eq)] pub enum MatchRequest { Method(Method), Url(Url), @@ -264,6 +265,7 @@ pub enum MatchRequest { Body(String), } +#[derive(Debug, Clone)] pub struct WhenRequest<'net> { net: &'net MockNet, match_on: Vec, @@ -302,3 +304,19 @@ impl<'net> WhenRequest<'net> { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn is_normal() {} + + #[test] + fn normal_types() { + is_normal::(); + // is_normal::(); // only used in test setup - no need to be Send or Sync + is_normal::(); + is_normal::(); + // is_normal::(); // only used in test setup - no need to be Send or Sync + } +} diff --git a/tests/fs.rs b/tests/fs.rs index bc6e3a6..2b04b16 100644 --- a/tests/fs.rs +++ b/tests/fs.rs @@ -5,6 +5,8 @@ use kxio::fs; type TestResult = Result<(), fs::Error>; mod path { + use fs::{PathHandle, PathMarker}; + use super::*; mod is_link { @@ -418,6 +420,30 @@ mod path { assert_eq!(src_pathbuf, dst_pathbuf); } } + + #[test] + fn from_file() { + let fs = fs::temp().expect("temp fs"); + + let file_path = fs.base().join("foo"); + let file = fs.file(&file_path); + + let path: PathHandle = file.into(); + + assert_eq!(path.as_pathbuf(), file_path); + } + + #[test] + fn from_dir() { + let fs = fs::temp().expect("temp fs"); + + let dir_path = fs.base().join("foo"); + let dir = fs.dir(&dir_path); + + let path: PathHandle = dir.into(); + + assert_eq!(path.as_pathbuf(), dir_path); + } } mod file { @@ -518,6 +544,53 @@ mod file { path.remove().expect("remove"); } + mod from_path { + use fs::{Error, FileHandle}; + + use super::*; + + #[test] + fn path_is_dir() { + let fs = fs::temp().expect("temp fs"); + let pathbuf = fs.base().join("foo"); + let dir = fs.dir(&pathbuf); + dir.create().expect("create dir"); + + let path = fs.path(&pathbuf); + + let_assert!(Err(Error::NotAFile { path: err_path }) = FileHandle::try_from(path)); + + assert_eq!(err_path, pathbuf); + } + + #[test] + fn path_is_file() { + let fs = fs::temp().expect("temp fs"); + let pathbuf = fs.base().join("foo"); + let file = fs.file(&pathbuf); + file.write("contents").expect("write file"); + + let path = fs.path(&pathbuf); + + let_assert!(Ok(file_result) = FileHandle::try_from(path)); + + assert_eq!(file_result.as_pathbuf(), pathbuf); + } + + #[test] + fn path_is_error() { + let fs = fs::temp().expect("temp fs"); + let pathbuf = fs.base().join("foo"); + // does not exist + + let path = fs.path(&pathbuf); + + let_assert!(Err(Error::NotAFile { path: err_path }) = FileHandle::try_from(path)); + + assert_eq!(err_path, pathbuf); + } + } + mod remove { use super::*; #[test] @@ -708,6 +781,53 @@ mod file { mod dir { use super::*; + mod from_path { + use fs::{DirHandle, Error}; + + use super::*; + + #[test] + fn path_is_dir() { + let fs = fs::temp().expect("temp fs"); + let pathbuf = fs.base().join("foo"); + let dir = fs.dir(&pathbuf); + dir.create().expect("create dir"); + + let path = fs.path(&pathbuf); + + let_assert!(Ok(dir_result) = DirHandle::try_from(path)); + + assert_eq!(dir_result.as_pathbuf(), pathbuf); + } + + #[test] + fn path_is_file() { + let fs = fs::temp().expect("temp fs"); + let pathbuf = fs.base().join("foo"); + let file = fs.file(&pathbuf); + file.write("contents").expect("write file"); + + let path = fs.path(&pathbuf); + + let_assert!(Err(Error::NotADirectory { path: err_path }) = DirHandle::try_from(path)); + + assert_eq!(err_path, pathbuf); + } + + #[test] + fn path_is_error() { + let fs = fs::temp().expect("temp fs"); + let pathbuf = fs.base().join("foo"); + // does not exist + + let path = fs.path(&pathbuf); + + let_assert!(Err(Error::NotADirectory { path: err_path }) = DirHandle::try_from(path)); + + assert_eq!(err_path, pathbuf); + } + } + mod create { use super::*; #[test] diff --git a/tests/net.rs b/tests/net.rs index c7f28e9..ac41ad4 100644 --- a/tests/net.rs +++ b/tests/net.rs @@ -62,7 +62,7 @@ async fn test_get_wrong_url() { assert_eq!(invalid_request.url().to_string(), "https://some.other.url/"); // remove pending unmatched request - we never meant to match against it - let mock_net = MockNet::try_from(net).expect("recover net"); + let mock_net = MockNet::try_from(net).await.expect("recover net"); mock_net.reset(); } @@ -210,7 +210,7 @@ async fn test_post_by_header_wrong_value() { //then let_assert!(Err(kxio::net::Error::UnexpectedMockRequest(_)) = response); - MockNet::try_from(net).expect("recover mock").reset(); + MockNet::try_from(net).await.expect("recover mock").reset(); } #[tokio::test]