Ensure consistency between in-memory and database state #146

Merged
wojtek merged 8 commits from 120---ensure-consistency-between-in-memory-and-database-state into main 2024-03-01 09:00:53 +01:00
4 changed files with 337 additions and 348 deletions
Showing only changes of commit bed0a61166 - Show all commits

View File

@ -53,38 +53,8 @@ impl Artist {
_ = self.sort.take();
}
pub fn add_musicbrainz_url<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> {
let url: MusicBrainz = url.as_ref().try_into()?;
match &self.musicbrainz {
Some(current) => {
if current != &url {
return Err(Error::UrlError(format!(
"artist already has a different URL: {current}"
)));
}
}
None => {
_ = self.musicbrainz.insert(url);
}
}
Ok(())
}
pub fn remove_musicbrainz_url<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> {
let url = url.as_ref().try_into()?;
if self.musicbrainz == Some(url) {
_ = self.musicbrainz.take();
}
Ok(())
}
pub fn set_musicbrainz_url<S: AsRef<str>>(&mut self, url: S) -> Result<(), Error> {
_ = self.musicbrainz.insert(url.as_ref().try_into()?);
Ok(())
pub fn set_musicbrainz_url(&mut self, url: MusicBrainz) {
_ = self.musicbrainz.insert(url);
}
pub fn clear_musicbrainz_url(&mut self) {
@ -326,40 +296,12 @@ mod tests {
}
#[test]
fn add_remove_musicbrainz_url() {
let mut artist = Artist::new(ArtistId::new("an artist"));
fn musicbrainz_url() {
let result: Result<MusicBrainz, Error> = MUSICBUTLER.try_into();
assert!(result.is_err());
let mut expected: Option<MusicBrainz> = None;
assert_eq!(artist.musicbrainz, expected);
// Adding incorect URL is an error.
assert!(artist.add_musicbrainz_url(MUSICBUTLER).is_err());
assert_eq!(artist.musicbrainz, expected);
// Adding URL to artist.
assert!(artist.add_musicbrainz_url(MUSICBRAINZ).is_ok());
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap());
assert_eq!(artist.musicbrainz, expected);
// Adding the same URL again is ok, but does not do anything.
assert!(artist.add_musicbrainz_url(MUSICBRAINZ).is_ok());
assert_eq!(artist.musicbrainz, expected);
// Adding further URLs is an error.
assert!(artist.add_musicbrainz_url(MUSICBRAINZ_2).is_err());
assert_eq!(artist.musicbrainz, expected);
// Removing a URL not in the collection is okay, but does not do anything.
assert!(artist.remove_musicbrainz_url(MUSICBRAINZ_2).is_ok());
assert_eq!(artist.musicbrainz, expected);
// Removing a URL in the collection removes it.
assert!(artist.remove_musicbrainz_url(MUSICBRAINZ).is_ok());
_ = expected.take();
assert_eq!(artist.musicbrainz, expected);
assert!(artist.remove_musicbrainz_url(MUSICBRAINZ).is_ok());
assert_eq!(artist.musicbrainz, expected);
let result: Result<MusicBrainz, Error> = MUSICBRAINZ.try_into();
assert!(result.is_ok());
}
#[test]
@ -369,19 +311,15 @@ mod tests {
let mut expected: Option<MusicBrainz> = None;
assert_eq!(artist.musicbrainz, expected);
// Setting an incorrect URL is an error.
assert!(artist.set_musicbrainz_url(MUSICBUTLER).is_err());
assert_eq!(artist.musicbrainz, expected);
// Setting a URL on an artist.
assert!(artist.set_musicbrainz_url(MUSICBRAINZ).is_ok());
artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap());
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap());
assert_eq!(artist.musicbrainz, expected);
assert!(artist.set_musicbrainz_url(MUSICBRAINZ).is_ok());
artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap());
assert_eq!(artist.musicbrainz, expected);
assert!(artist.set_musicbrainz_url(MUSICBRAINZ_2).is_ok());
artist.set_musicbrainz_url(MUSICBRAINZ_2.try_into().unwrap());
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ_2).unwrap());
assert_eq!(artist.musicbrainz, expected);

View File

@ -14,169 +14,49 @@ use crate::core::{
/// The Music Hoard. It is responsible for pulling information from both the library and the
/// database, ensuring its consistent and writing back any changes.
#[derive(Debug)]
pub struct MusicHoard<LIB, DB> {
collection: Collection,
library: LIB,
database: DB,
// There is no database cache since the database contains the entirety of the `collection`
// itself. Therefore, [`collection`] also represents the last state of the database.
library_cache: HashMap<ArtistId, Artist>,
pre_commit: Collection,
}
/// Phantom type for when a library implementation is not needed.
#[derive(Debug)]
pub struct NoLibrary;
/// Phantom type for when a database implementation is not needed.
#[derive(Debug)]
pub struct NoDatabase;
impl Default for MusicHoard<NoLibrary, NoDatabase> {
/// Create a new [`MusicHoard`] without any library or database.
fn default() -> Self {
MusicHoard::new(NoLibrary, NoDatabase)
MusicHoard::empty()
}
}
impl MusicHoard<NoLibrary, NoDatabase> {
/// Create a new [`MusicHoard`] without any library or database.
pub fn empty() -> Self {
MusicHoard {
collection: vec![],
library: NoLibrary,
database: NoDatabase,
library_cache: HashMap::new(),
pre_commit: vec![],
}
}
}
impl<LIB, DB> MusicHoard<LIB, DB> {
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`].
pub fn new(library: LIB, database: DB) -> Self {
MusicHoard {
collection: vec![],
library,
database,
library_cache: HashMap::new(),
}
}
/// Retrieve the [`Collection`].
pub fn get_collection(&self) -> &Collection {
&self.collection
}
pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) {
let artist_id: ArtistId = artist_id.into();
if self.get_artist(&artist_id).is_none() {
self.collection.push(Artist::new(artist_id));
Self::sort_artists(&mut self.collection);
}
}
pub fn remove_artist<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) {
let index_opt = self
.collection
.iter()
.position(|a| &a.id == artist_id.as_ref());
if let Some(index) = index_opt {
self.collection.remove(index);
}
}
pub fn set_artist_sort<ID: AsRef<ArtistId>, SORT: Into<ArtistId>>(
&mut self,
artist_id: ID,
artist_sort: SORT,
) -> Result<(), Error> {
self.get_artist_mut_or_err(artist_id.as_ref())?
.set_sort_key(artist_sort);
Self::sort(&mut self.collection);
Ok(())
}
pub fn clear_artist_sort<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
self.get_artist_mut_or_err(artist_id.as_ref())?
.clear_sort_key();
Self::sort(&mut self.collection);
Ok(())
}
pub fn add_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
url: S,
) -> Result<(), Error> {
Ok(self
.get_artist_mut_or_err(artist_id.as_ref())?
.add_musicbrainz_url(url)?)
}
pub fn remove_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
url: S,
) -> Result<(), Error> {
Ok(self
.get_artist_mut_or_err(artist_id.as_ref())?
.remove_musicbrainz_url(url)?)
}
pub fn set_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
url: S,
) -> Result<(), Error> {
Ok(self
.get_artist_mut_or_err(artist_id.as_ref())?
.set_musicbrainz_url(url)?)
}
pub fn clear_musicbrainz_url<ID: AsRef<ArtistId>>(
&mut self,
artist_id: ID,
) -> Result<(), Error> {
self.get_artist_mut_or_err(artist_id.as_ref())?
.clear_musicbrainz_url();
Ok(())
}
pub fn add_to_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self,
artist_id: ID,
property: S,
values: Vec<S>,
) -> Result<(), Error> {
self.get_artist_mut_or_err(artist_id.as_ref())?
.add_to_property(property, values);
Ok(())
}
pub fn remove_from_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
property: S,
values: Vec<S>,
) -> Result<(), Error> {
self.get_artist_mut_or_err(artist_id.as_ref())?
.remove_from_property(property, values);
Ok(())
}
pub fn set_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self,
artist_id: ID,
property: S,
values: Vec<S>,
) -> Result<(), Error> {
self.get_artist_mut_or_err(artist_id.as_ref())?
.set_property(property, values);
Ok(())
}
pub fn clear_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
property: S,
) -> Result<(), Error> {
self.get_artist_mut_or_err(artist_id.as_ref())?
.clear_property(property);
Ok(())
}
fn sort(collection: &mut [Artist]) {
Self::sort_artists(collection);
Self::sort_albums_and_tracks(collection.iter_mut());
}
fn sort_artists(collection: &mut [Artist]) {
collection.sort_unstable();
}
@ -272,21 +152,40 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
Ok(collection)
}
fn get_artist(&self, artist_id: &ArtistId) -> Option<&Artist> {
self.collection.iter().find(|a| &a.id == artist_id)
fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist> {
collection.iter().find(|a| &a.id == artist_id)
}
fn get_artist_mut(&mut self, artist_id: &ArtistId) -> Option<&mut Artist> {
self.collection.iter_mut().find(|a| &a.id == artist_id)
fn get_artist_mut<'a>(
collection: &'a mut Collection,
artist_id: &ArtistId,
) -> Option<&'a mut Artist> {
collection.iter_mut().find(|a| &a.id == artist_id)
}
fn get_artist_mut_or_err(&mut self, artist_id: &ArtistId) -> Result<&mut Artist, Error> {
self.get_artist_mut(artist_id).ok_or_else(|| {
fn get_artist_mut_or_err<'a>(
collection: &'a mut Collection,
artist_id: &ArtistId,
) -> Result<&'a mut Artist, Error> {
Self::get_artist_mut(collection, artist_id).ok_or_else(|| {
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
})
}
}
impl<LIB: ILibrary> MusicHoard<LIB, NoDatabase> {
/// Create a new [`MusicHoard`] with the provided [`IDatabase`].
pub fn library(library: LIB) -> Self {
MusicHoard {
collection: vec![],
library,
database: NoDatabase,
library_cache: HashMap::new(),
pre_commit: vec![],
}
}
}
impl<LIB: ILibrary, DB> MusicHoard<LIB, DB> {
/// Rescan the library and merge with the in-memory collection.
pub fn rescan_library(&mut self) -> Result<(), Error> {
@ -299,6 +198,21 @@ impl<LIB: ILibrary, DB> MusicHoard<LIB, DB> {
}
}
impl<DB: IDatabase> MusicHoard<NoLibrary, DB> {
/// Create a new [`MusicHoard`] with the provided [`IDatabase`].
pub fn database(database: DB) -> Result<Self, Error> {
let mut mh = MusicHoard {
collection: vec![],
library: NoLibrary,
database,
library_cache: HashMap::new(),
pre_commit: vec![],
};
mh.load_from_database()?;
Ok(mh)
}
}
impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
/// Load the database and merge with the in-memory collection.
pub fn load_from_database(&mut self) -> Result<(), Error> {
@ -309,16 +223,161 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
Ok(())
}
/// Save the in-memory collection to the database.
pub fn save_to_database(&mut self) -> Result<(), Error> {
self.database.save(&self.collection)?;
fn commit(&mut self) -> Result<(), Error> {
if self.collection != self.pre_commit {
if let Err(err) = self.database.save(&self.pre_commit) {
self.pre_commit = self.collection.clone();
return Err(err.into());
}
self.collection = self.pre_commit.clone();
}
Ok(())
}
fn update_collection<F>(&mut self, func: F) -> Result<(), Error>
where
F: FnOnce(&mut Collection),
{
func(&mut self.pre_commit);
self.commit()
}
fn update_artist_and<ID: AsRef<ArtistId>, F1, F2>(
&mut self,
artist_id: ID,
f1: F1,
f2: F2,
) -> Result<(), Error>
where
F1: FnOnce(&mut Artist),
F2: FnOnce(&mut Collection),
{
f1(Self::get_artist_mut_or_err(
&mut self.pre_commit,
artist_id.as_ref(),
)?);
self.update_collection(f2)
}
fn update_artist<ID: AsRef<ArtistId>, F>(&mut self, artist_id: ID, func: F) -> Result<(), Error>
where
F: FnOnce(&mut Artist),
{
self.update_artist_and(artist_id, func, |_| {})
}
pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
let artist_id: ArtistId = artist_id.into();
self.update_collection(|collection| {
if Self::get_artist(collection, &artist_id).is_none() {
collection.push(Artist::new(artist_id));
Self::sort_artists(collection);
}
})
}
pub fn remove_artist<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
self.update_collection(|collection| {
let index_opt = collection.iter().position(|a| &a.id == artist_id.as_ref());
if let Some(index) = index_opt {
collection.remove(index);
}
})
}
pub fn set_artist_sort<ID: AsRef<ArtistId>, SORT: Into<ArtistId>>(
&mut self,
artist_id: ID,
artist_sort: SORT,
) -> Result<(), Error> {
self.update_artist_and(
artist_id,
|artist| artist.set_sort_key(artist_sort),
|collection| Self::sort_artists(collection),
)
}
pub fn clear_artist_sort<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
self.update_artist_and(
artist_id,
|artist| artist.clear_sort_key(),
|collection| Self::sort_artists(collection),
)
}
pub fn set_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
url: S,
) -> Result<(), Error> {
let url = url.as_ref().try_into()?;
self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url))
}
pub fn clear_musicbrainz_url<ID: AsRef<ArtistId>>(
&mut self,
artist_id: ID,
) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.clear_musicbrainz_url())
}
pub fn add_to_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self,
artist_id: ID,
property: S,
values: Vec<S>,
) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.add_to_property(property, values))
}
pub fn remove_from_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
property: S,
values: Vec<S>,
) -> Result<(), Error> {
self.update_artist(artist_id, |artist| {
artist.remove_from_property(property, values)
})
}
pub fn set_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self,
artist_id: ID,
property: S,
values: Vec<S>,
) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.set_property(property, values))
}
pub fn clear_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
property: S,
) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.clear_property(property))
}
}
impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`].
pub fn new(library: LIB, database: DB) -> Result<Self, Error> {
let mut mh = MusicHoard {
collection: vec![],
library,
database,
library_cache: HashMap::new(),
pre_commit: vec![],
};
mh.load_from_database()?;
Ok(mh)
}
}
#[cfg(test)]
mod tests {
use mockall::predicate;
use mockall::{predicate, Sequence};
use crate::core::{
collection::artist::{ArtistId, MusicBrainz},
@ -338,28 +397,53 @@ mod tests {
fn artist_new_delete() {
let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::default();
let mut expected: Vec<Artist> = vec![];
let with_artist = vec![Artist::new(artist_id.clone())];
music_hoard.add_artist(artist_id.clone());
expected.push(Artist::new(artist_id.clone()));
assert_eq!(music_hoard.collection, expected);
let mut database = MockIDatabase::new();
let mut seq = Sequence::new();
database
.expect_load()
.times(1)
.times(1)
.in_sequence(&mut seq)
.returning(|| Ok(vec![]));
database
.expect_save()
.times(1)
.in_sequence(&mut seq)
.with(predicate::eq(with_artist.clone()))
.returning(|_| Ok(()));
database
.expect_save()
.times(1)
.in_sequence(&mut seq)
.with(predicate::eq(vec![]))
.returning(|_| Ok(()));
music_hoard.add_artist(artist_id.clone());
assert_eq!(music_hoard.collection, expected);
let mut music_hoard = MusicHoard::database(database).unwrap();
music_hoard.remove_artist(&artist_id_2);
assert_eq!(music_hoard.collection, expected);
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
assert_eq!(music_hoard.collection, with_artist);
music_hoard.remove_artist(&artist_id);
_ = expected.pop();
assert_eq!(music_hoard.collection, expected);
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
assert_eq!(music_hoard.collection, with_artist);
assert!(music_hoard.remove_artist(&artist_id_2).is_ok());
assert_eq!(music_hoard.collection, with_artist);
assert!(music_hoard.remove_artist(&artist_id).is_ok());
assert_eq!(music_hoard.collection, vec![]);
}
#[test]
fn artist_sort_set_clear() {
let mut music_hoard = MusicHoard::default();
let mut database = MockIDatabase::new();
database.expect_load().times(1).returning(|| Ok(vec![]));
database.expect_save().times(4).returning(|_| Ok(()));
type MH = MusicHoard<NoLibrary, MockIDatabase>;
let mut music_hoard: MH = MusicHoard::database(database).unwrap();
let artist_1_id = ArtistId::new("the artist");
let artist_1_sort = ArtistId::new("artist, the");
@ -370,11 +454,11 @@ mod tests {
assert!(artist_1_sort < artist_2_id);
assert!(artist_2_id < artist_1_id);
music_hoard.add_artist(artist_1_id.clone());
music_hoard.add_artist(artist_2_id.clone());
assert!(music_hoard.add_artist(artist_1_id.clone()).is_ok());
assert!(music_hoard.add_artist(artist_2_id.clone()).is_ok());
let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap();
let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap();
let artist_1: &Artist = MH::get_artist(&music_hoard.collection, &artist_1_id).unwrap();
let artist_2: &Artist = MH::get_artist(&music_hoard.collection, &artist_2_id).unwrap();
assert!(artist_2 < artist_1);
@ -385,8 +469,8 @@ mod tests {
.set_artist_sort(artist_1_id.as_ref(), artist_1_sort.clone())
.unwrap();
let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap();
let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap();
let artist_1: &Artist = MH::get_artist(&music_hoard.collection, &artist_1_id).unwrap();
let artist_2: &Artist = MH::get_artist(&music_hoard.collection, &artist_2_id).unwrap();
assert!(artist_1 < artist_2);
@ -395,8 +479,8 @@ mod tests {
music_hoard.clear_artist_sort(artist_1_id.as_ref()).unwrap();
let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap();
let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap();
let artist_1: &Artist = MH::get_artist(&music_hoard.collection, &artist_1_id).unwrap();
let artist_2: &Artist = MH::get_artist(&music_hoard.collection, &artist_2_id).unwrap();
assert!(artist_2 < artist_1);
@ -406,12 +490,16 @@ mod tests {
#[test]
fn collection_error() {
let mut database = MockIDatabase::new();
database.expect_load().times(1).returning(|| Ok(vec![]));
database.expect_save().times(1).returning(|_| Ok(()));
let artist_id = ArtistId::new("an artist");
let mut music_hoard = MusicHoard::default();
music_hoard.add_artist(artist_id.clone());
let mut music_hoard = MusicHoard::database(database).unwrap();
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let actual_err = music_hoard
.add_musicbrainz_url(&artist_id, MUSICBUTLER)
.set_musicbrainz_url(&artist_id, MUSICBUTLER)
.unwrap_err();
let expected_err = Error::CollectionError(format!(
"an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}"
@ -420,51 +508,17 @@ mod tests {
assert_eq!(actual_err.to_string(), expected_err.to_string());
}
#[test]
fn add_remove_musicbrainz_url() {
let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::default();
music_hoard.add_artist(artist_id.clone());
let mut expected: Option<MusicBrainz> = None;
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Adding URL to an artist not in the collection is an error.
assert!(music_hoard
.add_musicbrainz_url(&artist_id_2, MUSICBRAINZ)
.is_err());
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Adding URL to artist.
assert!(music_hoard
.add_musicbrainz_url(&artist_id, MUSICBRAINZ)
.is_ok());
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap());
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Removing a URL from an artist not in the collection is an error.
assert!(music_hoard
.remove_musicbrainz_url(&artist_id_2, MUSICBRAINZ)
.is_err());
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Removing a URL in the collection removes it.
assert!(music_hoard
.remove_musicbrainz_url(&artist_id, MUSICBRAINZ)
.is_ok());
_ = expected.take();
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
}
#[test]
fn set_clear_musicbrainz_url() {
let mut database = MockIDatabase::new();
database.expect_load().times(1).returning(|| Ok(vec![]));
database.expect_save().times(3).returning(|_| Ok(()));
let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::default();
let mut music_hoard = MusicHoard::database(database).unwrap();
music_hoard.add_artist(artist_id.clone());
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let mut expected: Option<MusicBrainz> = None;
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
@ -494,11 +548,15 @@ mod tests {
#[test]
fn add_to_remove_from_property() {
let mut database = MockIDatabase::new();
database.expect_load().times(1).returning(|| Ok(vec![]));
database.expect_save().times(3).returning(|_| Ok(()));
let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::default();
let mut music_hoard = MusicHoard::database(database).unwrap();
music_hoard.add_artist(artist_id.clone());
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let mut expected: Vec<String> = vec![];
assert!(music_hoard.collection[0].properties.is_empty());
@ -539,11 +597,15 @@ mod tests {
#[test]
fn set_clear_property() {
let mut database = MockIDatabase::new();
database.expect_load().times(1).returning(|| Ok(vec![]));
database.expect_save().times(3).returning(|_| Ok(()));
let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::default();
let mut music_hoard = MusicHoard::database(database).unwrap();
music_hoard.add_artist(artist_id.clone());
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let mut expected: Vec<String> = vec![];
assert!(music_hoard.collection[0].properties.is_empty());
@ -709,7 +771,6 @@ mod tests {
#[test]
fn rescan_library_ordered() {
let mut library = MockILibrary::new();
let database = MockIDatabase::new();
let library_input = Query::new();
let library_result = Ok(LIBRARY_ITEMS.to_owned());
@ -720,7 +781,7 @@ mod tests {
.times(1)
.return_once(|_| library_result);
let mut music_hoard = MusicHoard::new(library, database);
let mut music_hoard = MusicHoard::library(library);
music_hoard.rescan_library().unwrap();
assert_eq!(music_hoard.get_collection(), &*LIBRARY_COLLECTION);
@ -729,7 +790,6 @@ mod tests {
#[test]
fn rescan_library_unordered() {
let mut library = MockILibrary::new();
let database = MockIDatabase::new();
let library_input = Query::new();
let mut library_result = Ok(LIBRARY_ITEMS.to_owned());
@ -744,7 +804,7 @@ mod tests {
.times(1)
.return_once(|_| library_result);
let mut music_hoard = MusicHoard::new(library, database);
let mut music_hoard = MusicHoard::library(library);
music_hoard.rescan_library().unwrap();
assert_eq!(music_hoard.get_collection(), &*LIBRARY_COLLECTION);
@ -753,7 +813,6 @@ mod tests {
#[test]
fn rescan_library_album_title_year_clash() {
let mut library = MockILibrary::new();
let database = MockIDatabase::new();
let mut expected = LIBRARY_COLLECTION.to_owned();
let removed_album_id = expected[0].albums[0].id.clone();
@ -778,7 +837,7 @@ mod tests {
.times(1)
.return_once(|_| library_result);
let mut music_hoard = MusicHoard::new(library, database);
let mut music_hoard = MusicHoard::library(library);
music_hoard.rescan_library().unwrap();
assert_eq!(music_hoard.get_collection(), &expected);
@ -787,7 +846,6 @@ mod tests {
#[test]
fn rescan_library_album_artist_sort_clash() {
let mut library = MockILibrary::new();
let database = MockIDatabase::new();
let library_input = Query::new();
let mut library_items = LIBRARY_ITEMS.to_owned();
@ -811,7 +869,7 @@ mod tests {
.times(1)
.return_once(|_| library_result);
let mut music_hoard = MusicHoard::new(library, database);
let mut music_hoard = MusicHoard::library(library);
assert!(music_hoard.rescan_library().is_err());
}
@ -826,46 +884,14 @@ mod tests {
.times(1)
.return_once(|| Ok(FULL_COLLECTION.to_owned()));
let mut music_hoard = MusicHoard::new(library, database);
let music_hoard = MusicHoard::new(library, database).unwrap();
music_hoard.load_from_database().unwrap();
assert_eq!(music_hoard.get_collection(), &*FULL_COLLECTION);
}
#[test]
fn rescan_get_save() {
let mut library = MockILibrary::new();
let mut database = MockIDatabase::new();
let library_input = Query::new();
let library_result = Ok(LIBRARY_ITEMS.to_owned());
let database_input = LIBRARY_COLLECTION.to_owned();
let database_result = Ok(());
library
.expect_list()
.with(predicate::eq(library_input))
.times(1)
.return_once(|_| library_result);
database
.expect_save()
.with(predicate::eq(database_input))
.times(1)
.return_once(|_: &Collection| database_result);
let mut music_hoard = MusicHoard::new(library, database);
music_hoard.rescan_library().unwrap();
assert_eq!(music_hoard.get_collection(), &*LIBRARY_COLLECTION);
music_hoard.save_to_database().unwrap();
}
#[test]
fn library_error() {
let mut library = MockILibrary::new();
let database = MockIDatabase::new();
let library_result = Err(library::Error::Invalid(String::from("invalid data")));
@ -874,7 +900,7 @@ mod tests {
.times(1)
.return_once(|_| library_result);
let mut music_hoard = MusicHoard::new(library, database);
let mut music_hoard = MusicHoard::library(library);
let actual_err = music_hoard.rescan_library().unwrap_err();
let expected_err =
@ -886,7 +912,6 @@ mod tests {
#[test]
fn database_load_error() {
let library = MockILibrary::new();
let mut database = MockIDatabase::new();
let database_result = Err(database::LoadError::IoError(String::from("I/O error")));
@ -896,9 +921,7 @@ mod tests {
.times(1)
.return_once(|| database_result);
let mut music_hoard = MusicHoard::new(library, database);
let actual_err = music_hoard.load_from_database().unwrap_err();
let actual_err = MusicHoard::database(database).unwrap_err();
let expected_err = Error::DatabaseError(
database::LoadError::IoError(String::from("I/O error")).to_string(),
);
@ -909,19 +932,21 @@ mod tests {
#[test]
fn database_save_error() {
let library = MockILibrary::new();
let mut database = MockIDatabase::new();
let database_result = Err(database::SaveError::IoError(String::from("I/O error")));
database.expect_load().return_once(|| Ok(vec![]));
database
.expect_save()
.times(1)
.return_once(|_: &Collection| database_result);
let mut music_hoard = MusicHoard::new(library, database);
let mut music_hoard = MusicHoard::database(database).unwrap();
let actual_err = music_hoard.save_to_database().unwrap_err();
let actual_err = music_hoard
.add_artist(ArtistId::new("an artist"))
.unwrap_err();
let expected_err = Error::DatabaseError(
database::SaveError::IoError(String::from("I/O error")).to_string(),
);

View File

@ -1,7 +1,10 @@
use crate::core::{
database::IDatabase,
library::ILibrary,
musichoard::musichoard::{MusicHoard, NoDatabase, NoLibrary},
use crate::{
core::{
database::IDatabase,
library::ILibrary,
musichoard::musichoard::{MusicHoard, NoDatabase, NoLibrary},
},
Error,
};
/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of
@ -44,9 +47,32 @@ impl<LIB, DB> MusicHoardBuilder<LIB, DB> {
database,
}
}
}
impl MusicHoardBuilder<NoLibrary, NoDatabase> {
/// Build [`MusicHoard`] with the currently set library and database.
pub fn build(self) -> MusicHoard<LIB, DB> {
pub fn build(self) -> MusicHoard<NoLibrary, NoDatabase> {
MusicHoard::empty()
}
}
impl<LIB: ILibrary> MusicHoardBuilder<LIB, NoDatabase> {
/// Build [`MusicHoard`] with the currently set library and database.
pub fn build(self) -> MusicHoard<LIB, NoDatabase> {
MusicHoard::library(self.library)
}
}
impl<DB: IDatabase> MusicHoardBuilder<NoLibrary, DB> {
/// Build [`MusicHoard`] with the currently set library and database.
pub fn build(self) -> Result<MusicHoard<NoLibrary, DB>, Error> {
MusicHoard::database(self.database)
}
}
impl<LIB: ILibrary, DB: IDatabase> MusicHoardBuilder<LIB, DB> {
/// Build [`MusicHoard`] with the currently set library and database.
pub fn build(self) -> Result<MusicHoard<LIB, DB>, Error> {
MusicHoard::new(self.library, self.database)
}
}
@ -74,9 +100,9 @@ mod tests {
fn no_library_with_database() {
let mut mh = MusicHoardBuilder::default()
.set_database(NullDatabase)
.build();
.build()
.unwrap();
assert!(mh.load_from_database().is_ok());
assert!(mh.save_to_database().is_ok());
}
#[test]
@ -84,9 +110,9 @@ mod tests {
let mut mh = MusicHoardBuilder::default()
.set_library(NullLibrary)
.set_database(NullDatabase)
.build();
.build()
.unwrap();
assert!(mh.rescan_library().is_ok());
assert!(mh.load_from_database().is_ok());
assert!(mh.save_to_database().is_ok());
}
}

View File

@ -29,7 +29,7 @@ fn merge_library_then_database() {
let backend = JsonDatabaseFileBackend::new(&*database::json::DATABASE_TEST_FILE);
let database = JsonDatabase::new(backend);
let mut music_hoard = MusicHoard::new(library, database);
let mut music_hoard = MusicHoard::new(library, database).unwrap();
music_hoard.rescan_library().unwrap();
music_hoard.load_from_database().unwrap();
@ -52,7 +52,7 @@ fn merge_database_then_library() {
let backend = JsonDatabaseFileBackend::new(&*database::json::DATABASE_TEST_FILE);
let database = JsonDatabase::new(backend);
let mut music_hoard = MusicHoard::new(library, database);
let mut music_hoard = MusicHoard::new(library, database).unwrap();
music_hoard.load_from_database().unwrap();
music_hoard.rescan_library().unwrap();