Increase code coverage to a sensible amount (#33)

Closes #26

Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/33
This commit is contained in:
Wojciech Kozlowski 2023-04-13 20:32:33 +02:00
parent 3e6c95d188
commit 3e9c84656e
6 changed files with 170 additions and 34 deletions

View File

@ -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());

View File

@ -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::<ArtistId, String>::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());
}
}

View File

@ -44,3 +44,17 @@ pub trait Database {
/// Write collection to the database.
fn write<S: Serialize + 'static>(&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());
}
}

View File

@ -164,7 +164,7 @@ impl<BLE: BeetsLibraryExecutor> LibraryPrivate for BeetsLibrary<BLE> {
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<BLE: BeetsLibraryExecutor> LibraryPrivate for BeetsLibrary<BLE> {
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<Artist> = 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::<Vec<String>>()[..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::<Vec<String>>();
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));
}
}

View File

@ -20,18 +20,6 @@ pub enum QueryOption<T> {
None,
}
impl<T> QueryOption<T> {
/// 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<T> Default for QueryOption<T> {
/// 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<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
Error::IoError(err.to_string())
Error::Io(err.to_string())
}
}
impl From<ParseIntError> for Error {
fn from(err: ParseIntError) -> Error {
Error::ParseIntError(err.to_string())
Error::ParseInt(err.to_string())
}
}
impl From<Utf8Error> 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<Vec<Artist>, 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::<u32>().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());
}
}

View File

@ -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();