diff --git a/src/collection/mod.rs b/src/collection/mod.rs index 14e6240..a2ca0a3 100644 --- a/src/collection/mod.rs +++ b/src/collection/mod.rs @@ -137,7 +137,7 @@ mod tests { let mut library = MockLibrary::new(); let database = MockDatabase::new(); - let library_result = Err(library::Error::InvalidData(String::from("invalid data"))); + let library_result = Err(library::Error::Invalid(String::from("invalid data"))); library .expect_list() @@ -147,9 +147,8 @@ mod tests { let mut collection_manager = MhCollectionManager::new(library, database); let actual_err = collection_manager.rescan_library().unwrap_err(); - let expected_err = Error::LibraryError( - library::Error::InvalidData(String::from("invalid data")).to_string(), - ); + let expected_err = + Error::LibraryError(library::Error::Invalid(String::from("invalid data")).to_string()); assert_eq!(actual_err, expected_err); assert_eq!(actual_err.to_string(), expected_err.to_string()); diff --git a/src/database/json.rs b/src/database/json.rs index 54fbdf6..b039dad 100644 --- a/src/database/json.rs +++ b/src/database/json.rs @@ -81,11 +81,13 @@ impl JsonDatabaseBackend for JsonDatabaseFileBackend { #[cfg(test)] mod tests { + use std::collections::HashMap; + use mockall::predicate; use super::*; - use crate::{tests::COLLECTION, Artist, TrackFormat}; + use crate::{tests::COLLECTION, Artist, ArtistId, TrackFormat}; fn artist_to_json(artist: &Artist) -> String { let album_artist = &artist.id.name; @@ -203,4 +205,21 @@ mod tests { assert_eq!(write_data, read_data); } + + #[test] + fn errors() { + let mut object = HashMap::::new(); + object.insert( + ArtistId { + name: String::from("artist"), + }, + String::from("string"), + ); + let serde_err = serde_json::to_string(&object); + assert!(serde_err.is_err()); + + let serde_err: Error = serde_err.unwrap_err().into(); + assert!(!serde_err.to_string().is_empty()); + assert!(!format!("{:?}", serde_err).is_empty()); + } } diff --git a/src/database/mod.rs b/src/database/mod.rs index db77a9a..045bc36 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -44,3 +44,17 @@ pub trait Database { /// Write collection to the database. fn write(&mut self, collection: &S) -> Result<(), Error>; } + +#[cfg(test)] +mod tests { + use std::io; + + use super::Error; + + #[test] + fn errors() { + let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into(); + assert!(!io_err.to_string().is_empty()); + assert!(!format!("{:?}", io_err).is_empty()); + } +} diff --git a/src/library/beets.rs b/src/library/beets.rs index b042e6f..91ff2d1 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -164,7 +164,7 @@ impl LibraryPrivate for BeetsLibrary { let split: Vec<&str> = line.split(LIST_FORMAT_SEPARATOR).collect(); if split.len() != 7 { - return Err(Error::InvalidData(line.to_string())); + return Err(Error::Invalid(line.to_string())); } let album_artist = split[0].to_string(); @@ -189,7 +189,7 @@ impl LibraryPrivate for BeetsLibrary { format: match track_format.as_ref() { TRACK_FORMAT_FLAC => TrackFormat::Flac, TRACK_FORMAT_MP3 => TrackFormat::Mp3, - _ => return Err(Error::InvalidData(track_format)), + _ => return Err(Error::Invalid(line.to_string())), }, }; @@ -272,7 +272,7 @@ impl BeetsLibraryExecutor for BeetsLibraryCommandExecutor { } let output = cmd.args(arguments).output()?; if !output.status.success() { - return Err(Error::CmdExecError( + return Err(Error::CmdExec( String::from_utf8_lossy(&output.stderr).to_string(), )); } @@ -346,6 +346,25 @@ mod tests { String::from("^some.all"), ] ); + + let query = Query::new() + .album_artist(QueryOption::Exclude(String::from("some.albumartist"))) + .album_year(QueryOption::Include(3030)) + .track_title(QueryOption::Include(String::from("some.track"))) + .track_artist(QueryOption::Exclude(vec![ + String::from("some.artist.1"), + String::from("some.artist.2"), + ])); + + assert_eq!( + query.to_args(), + vec![ + String::from("^albumartist:some.albumartist"), + String::from("year:3030"), + String::from("title:some.track"), + String::from("^artist:some.artist.1; some.artist.2"), + ] + ); } #[test] @@ -472,4 +491,61 @@ mod tests { let expected: Vec = vec![]; assert_eq!(output, expected); } + + #[test] + fn invalid_data_split() { + let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; + let expected = COLLECTION.to_owned(); + let mut output = artists_to_beets_string(&expected); + let invalid_string = output[2] + .split(LIST_FORMAT_SEPARATOR) + .map(|s| s.to_owned()) + .collect::>()[..4] + .join(LIST_FORMAT_SEPARATOR); + output[2] = invalid_string.clone(); + let result = Ok(output); + + let mut executor = MockBeetsLibraryExecutor::new(); + executor + .expect_exec() + .with(predicate::eq(arguments)) + .times(1) + .return_once(|_| result); + + let mut beets = BeetsLibrary::new(executor); + let err = beets.list(&Query::default()).unwrap_err(); + + assert_eq!(err, Error::Invalid(invalid_string)); + } + + #[test] + fn invalid_data_format() { + let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; + let expected = COLLECTION.to_owned(); + let mut output = artists_to_beets_string(&expected); + let mut invalid_string = output[2] + .split(LIST_FORMAT_SEPARATOR) + .map(|s| s.to_owned()) + .collect::>(); + invalid_string.last_mut().unwrap().clear(); + invalid_string + .last_mut() + .unwrap() + .push_str("invalid format"); + let invalid_string = invalid_string.join(LIST_FORMAT_SEPARATOR); + output[2] = invalid_string.clone(); + let result = Ok(output); + + let mut executor = MockBeetsLibraryExecutor::new(); + executor + .expect_exec() + .with(predicate::eq(arguments)) + .times(1) + .return_once(|_| result); + + let mut beets = BeetsLibrary::new(executor); + let err = beets.list(&Query::default()).unwrap_err(); + + assert_eq!(err, Error::Invalid(invalid_string)); + } } diff --git a/src/library/mod.rs b/src/library/mod.rs index 3ade40e..cd79093 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -20,18 +20,6 @@ pub enum QueryOption { None, } -impl QueryOption { - /// Return `true` if [`QueryOption`] is not [`QueryOption::None`]. - pub fn is_some(&self) -> bool { - !matches!(self, QueryOption::None) - } - - /// Return `true` if [`QueryOption`] is [`QueryOption::None`]. - pub fn is_none(&self) -> bool { - matches!(self, QueryOption::None) - } -} - impl Default for QueryOption { /// Create a [`QueryOption::None`] for type `T`. fn default() -> Self { @@ -101,47 +89,47 @@ impl Query { } /// Error type for library calls. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum Error { /// The underlying library failed to execute a command. - CmdExecError(String), + CmdExec(String), /// The underlying library returned invalid data. - InvalidData(String), + Invalid(String), /// The underlying library experienced an I/O error. - IoError(String), + Io(String), /// The underlying library failed to parse an integer. - ParseIntError(String), + ParseInt(String), /// The underlying library failed to parse a UTF-8 string. - Utf8Error(String), + Utf8(String), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - Self::CmdExecError(ref s) => write!(f, "the library failed to execute a command: {s}"), - Self::InvalidData(ref s) => write!(f, "the library received invalid data: {s}"), - Self::IoError(ref s) => write!(f, "the library experienced an I/O error: {s}"), - Self::ParseIntError(ref s) => write!(f, "the library received an invalid integer: {s}"), - Self::Utf8Error(ref s) => write!(f, "the library received invalid UTF-8: {s}"), + Self::CmdExec(ref s) => write!(f, "the library failed to execute a command: {s}"), + Self::Invalid(ref s) => write!(f, "the library received invalid data: {s}"), + Self::Io(ref s) => write!(f, "the library experienced an I/O error: {s}"), + Self::ParseInt(ref s) => write!(f, "the library received an invalid integer: {s}"), + Self::Utf8(ref s) => write!(f, "the library received invalid UTF-8: {s}"), } } } impl From for Error { fn from(err: std::io::Error) -> Error { - Error::IoError(err.to_string()) + Error::Io(err.to_string()) } } impl From for Error { fn from(err: ParseIntError) -> Error { - Error::ParseIntError(err.to_string()) + Error::ParseInt(err.to_string()) } } impl From for Error { fn from(err: Utf8Error) -> Error { - Error::Utf8Error(err.to_string()) + Error::Utf8(err.to_string()) } } @@ -151,3 +139,31 @@ pub trait Library { /// List lirbary items that match the a specific query. fn list(&mut self, query: &Query) -> Result, Error>; } + +#[cfg(test)] +mod tests { + use std::io; + + use super::Error; + + #[test] + fn errors() { + let cmd_err = Error::CmdExec(String::from("CmdExec")); + let inv_err = Error::Invalid(String::from("Invalid")); + let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "Interrupted").into(); + let int_err: Error = "five".parse::().unwrap_err().into(); + let utf_err: Error = std::str::from_utf8(b"\xe2\x28\xa1").unwrap_err().into(); + + assert!(!cmd_err.to_string().is_empty()); + assert!(!inv_err.to_string().is_empty()); + assert!(!io_err.to_string().is_empty()); + assert!(!int_err.to_string().is_empty()); + assert!(!utf_err.to_string().is_empty()); + + assert!(!format!("{:?}", cmd_err).is_empty()); + assert!(!format!("{:?}", inv_err).is_empty()); + assert!(!format!("{:?}", io_err).is_empty()); + assert!(!format!("{:?}", int_err).is_empty()); + assert!(!format!("{:?}", utf_err).is_empty()); + } +} diff --git a/tests/library/beets.rs b/tests/library/beets.rs index bea16d0..289bfec 100644 --- a/tests/library/beets.rs +++ b/tests/library/beets.rs @@ -1,5 +1,6 @@ use std::{ fs, + path::PathBuf, sync::{Arc, Mutex}, }; @@ -42,6 +43,17 @@ fn test_no_config_list() { assert_eq!(output, expected); } +#[test] +fn test_invalid_config() { + let mut beets = BeetsLibrary::new(BeetsLibraryCommandExecutor::default().config(Some( + &PathBuf::from("./tests/files/library/config-does-not-exist.yml"), + ))); + + let result = beets.list(&Query::default()); + assert!(result.is_err()); + assert!(!result.unwrap_err().to_string().is_empty()); +} + #[test] fn test_full_list() { let beets_arc = BEETS_TEST_CONFIG.clone();