Handle idle time between fetch results #212
@ -1,26 +1,9 @@
|
|||||||
use std::{
|
use crate::tui::app::{
|
||||||
sync::{mpsc, Arc, Mutex},
|
machine::{App, AppInner, AppMachine},
|
||||||
thread, time,
|
selection::{Delta, ListSelection},
|
||||||
|
AppPublic, AppState, IAppInteractBrowse,
|
||||||
};
|
};
|
||||||
|
|
||||||
use musichoard::collection::{
|
|
||||||
album::AlbumMeta,
|
|
||||||
artist::{Artist, ArtistMeta},
|
|
||||||
musicbrainz::{IMusicBrainzRef, Mbid},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::tui::{
|
|
||||||
app::{
|
|
||||||
machine::{App, AppInner, AppMachine},
|
|
||||||
selection::{Delta, ListSelection},
|
|
||||||
AppMatchesInfo, AppPublic, AppState, IAppInteractBrowse,
|
|
||||||
},
|
|
||||||
event::{Event, EventSender},
|
|
||||||
lib::interface::musicbrainz::{self, IMusicBrainz},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::fetch::{FetchResult, FetchSender};
|
|
||||||
|
|
||||||
pub struct AppBrowse;
|
pub struct AppBrowse;
|
||||||
|
|
||||||
impl AppMachine<AppBrowse> {
|
impl AppMachine<AppBrowse> {
|
||||||
@ -30,80 +13,6 @@ impl AppMachine<AppBrowse> {
|
|||||||
state: AppBrowse,
|
state: AppBrowse,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_fetch_thread(
|
|
||||||
inner: &AppInner,
|
|
||||||
artist: &Artist,
|
|
||||||
tx: FetchSender,
|
|
||||||
) -> thread::JoinHandle<()> {
|
|
||||||
match artist.meta.musicbrainz {
|
|
||||||
Some(ref arid) => {
|
|
||||||
let musicbrainz = Arc::clone(&inner.musicbrainz);
|
|
||||||
let events = inner.events.clone();
|
|
||||||
let arid = arid.mbid().clone();
|
|
||||||
let albums = artist.albums.iter().map(|a| &a.meta).cloned().collect();
|
|
||||||
thread::spawn(|| Self::fetch_albums(musicbrainz, tx, events, arid, albums))
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let musicbrainz = Arc::clone(&inner.musicbrainz);
|
|
||||||
let events = inner.events.clone();
|
|
||||||
let artist = artist.meta.clone();
|
|
||||||
thread::spawn(|| Self::fetch_artist(musicbrainz, tx, events, artist))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_artist(
|
|
||||||
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>,
|
|
||||||
fetch_tx: FetchSender,
|
|
||||||
events: EventSender,
|
|
||||||
artist: ArtistMeta,
|
|
||||||
) {
|
|
||||||
let result = musicbrainz.lock().unwrap().search_artist(&artist);
|
|
||||||
let result = result.map(|list| AppMatchesInfo::artist(artist, list));
|
|
||||||
Self::send_fetch_result(&fetch_tx, &events, result).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_albums(
|
|
||||||
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>,
|
|
||||||
fetch_tx: FetchSender,
|
|
||||||
events: EventSender,
|
|
||||||
arid: Mbid,
|
|
||||||
albums: Vec<AlbumMeta>,
|
|
||||||
) {
|
|
||||||
let mut musicbrainz = musicbrainz.lock().unwrap();
|
|
||||||
let mut album_iter = albums.into_iter().peekable();
|
|
||||||
while let Some(album) = album_iter.next() {
|
|
||||||
if album.musicbrainz.is_some() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = musicbrainz.search_release_group(&arid, &album);
|
|
||||||
let result = result.map(|list| AppMatchesInfo::album(album, list));
|
|
||||||
if Self::send_fetch_result(&fetch_tx, &events, result).is_err() {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if album_iter.peek().is_some() {
|
|
||||||
thread::sleep(time::Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_fetch_result(
|
|
||||||
fetch_tx: &FetchSender,
|
|
||||||
events: &EventSender,
|
|
||||||
result: Result<AppMatchesInfo, musicbrainz::Error>,
|
|
||||||
) -> Result<(), ()> {
|
|
||||||
// If receiver disconnects just drop the rest.
|
|
||||||
fetch_tx.send(result).map_err(|_| ())?;
|
|
||||||
|
|
||||||
// If this send fails the event listener is dead. Don't panic as this function runs in a
|
|
||||||
// detached thread so this might be happening during normal shut down.
|
|
||||||
events.send(Event::FetchResultReady).map_err(|_| ())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AppMachine<AppBrowse>> for App {
|
impl From<AppMachine<AppBrowse>> for App {
|
||||||
@ -170,35 +79,19 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_musicbrainz(self) -> Self::APP {
|
fn fetch_musicbrainz(self) -> Self::APP {
|
||||||
let coll = self.inner.music_hoard.get_collection();
|
AppMachine::app_fetch_new(self.inner)
|
||||||
let artist = match self.inner.selection.state_artist(coll) {
|
|
||||||
Some(artist_state) => &coll[artist_state.index],
|
|
||||||
None => {
|
|
||||||
return AppMachine::error(self.inner, "cannot fetch: no artist selected").into()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (fetch_tx, fetch_rx) = mpsc::channel::<FetchResult>();
|
|
||||||
Self::spawn_fetch_thread(&self.inner, artist, fetch_tx);
|
|
||||||
|
|
||||||
AppMachine::app_fetch_new(self.inner, fetch_rx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use mockall::{predicate, Sequence};
|
|
||||||
use mpsc::TryRecvError;
|
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
machine::tests::{inner, inner_with_mb, music_hoard},
|
machine::tests::{inner, inner_with_mb, music_hoard},
|
||||||
AppAlbumMatches, AppArtistMatches, Category, IAppAccess, IAppInteract,
|
Category, IAppAccess, IAppInteract,
|
||||||
},
|
},
|
||||||
event::EventReceiver,
|
lib::interface::musicbrainz::MockIMusicBrainz,
|
||||||
lib::interface::musicbrainz::{Match, MockIMusicBrainz},
|
|
||||||
testmod::COLLECTION,
|
testmod::COLLECTION,
|
||||||
EventChannel,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -291,195 +184,10 @@ mod tests {
|
|||||||
|
|
||||||
let public = app.get();
|
let public = app.get();
|
||||||
|
|
||||||
// Because of threaded behaviour, this unit test cannot expect one or the other.
|
// Because of fetch's threaded behaviour, this unit test cannot expect one or the other.
|
||||||
assert!(
|
assert!(
|
||||||
matches!(public.state, AppState::Matches(_))
|
matches!(public.state, AppState::Matches(_))
|
||||||
|| matches!(public.state, AppState::Fetch(_))
|
|| matches!(public.state, AppState::Fetch(_))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event_channel() -> (EventSender, EventReceiver) {
|
|
||||||
let event_channel = EventChannel::new();
|
|
||||||
let events_tx = event_channel.sender();
|
|
||||||
let events_rx = event_channel.receiver();
|
|
||||||
(events_tx, events_rx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn album_expectations_1() -> (AlbumMeta, Vec<Match<AlbumMeta>>) {
|
|
||||||
let album_1 = COLLECTION[1].albums[0].meta.clone();
|
|
||||||
let album_4 = COLLECTION[1].albums[3].meta.clone();
|
|
||||||
|
|
||||||
let album_match_1_1 = Match::new(100, album_1.clone());
|
|
||||||
let album_match_1_2 = Match::new(50, album_4.clone());
|
|
||||||
let matches_1 = vec![album_match_1_1.clone(), album_match_1_2.clone()];
|
|
||||||
|
|
||||||
(album_1, matches_1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn album_expectations_4() -> (AlbumMeta, Vec<Match<AlbumMeta>>) {
|
|
||||||
let album_1 = COLLECTION[1].albums[0].meta.clone();
|
|
||||||
let album_4 = COLLECTION[1].albums[3].meta.clone();
|
|
||||||
|
|
||||||
let album_match_4_1 = Match::new(100, album_4.clone());
|
|
||||||
let album_match_4_2 = Match::new(30, album_1.clone());
|
|
||||||
let matches_4 = vec![album_match_4_1.clone(), album_match_4_2.clone()];
|
|
||||||
|
|
||||||
(album_4, matches_4)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_release_group_expectation(
|
|
||||||
api: &mut MockIMusicBrainz,
|
|
||||||
seq: &mut Sequence,
|
|
||||||
arid: &Mbid,
|
|
||||||
album: &AlbumMeta,
|
|
||||||
matches: &[Match<AlbumMeta>],
|
|
||||||
) {
|
|
||||||
let result = Ok(matches.to_owned());
|
|
||||||
api.expect_search_release_group()
|
|
||||||
.with(predicate::eq(arid.clone()), predicate::eq(album.clone()))
|
|
||||||
.times(1)
|
|
||||||
.in_sequence(seq)
|
|
||||||
.return_once(|_, _| result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fetch_albums() {
|
|
||||||
let mut mb_api = MockIMusicBrainz::new();
|
|
||||||
|
|
||||||
let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
|
|
||||||
|
|
||||||
let (album_1, matches_1) = album_expectations_1();
|
|
||||||
let (album_4, matches_4) = album_expectations_4();
|
|
||||||
|
|
||||||
// Other albums have an MBID and so they will be skipped.
|
|
||||||
let mut seq = Sequence::new();
|
|
||||||
|
|
||||||
search_release_group_expectation(&mut mb_api, &mut seq, &arid, &album_1, &matches_1);
|
|
||||||
search_release_group_expectation(&mut mb_api, &mut seq, &arid, &album_4, &matches_4);
|
|
||||||
|
|
||||||
let music_hoard = music_hoard(COLLECTION.to_owned());
|
|
||||||
let (events_tx, events_rx) = event_channel();
|
|
||||||
let inner = AppInner::new(music_hoard, mb_api, events_tx);
|
|
||||||
|
|
||||||
let (fetch_tx, fetch_rx) = mpsc::channel();
|
|
||||||
// Use the second artist for this test.
|
|
||||||
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[1], fetch_tx);
|
|
||||||
handle.join().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
|
|
||||||
let result = fetch_rx.try_recv().unwrap();
|
|
||||||
let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches {
|
|
||||||
matching: album_1.clone(),
|
|
||||||
list: matches_1.iter().cloned().map(Into::into).collect(),
|
|
||||||
}));
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
|
|
||||||
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
|
|
||||||
let result = fetch_rx.try_recv().unwrap();
|
|
||||||
let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches {
|
|
||||||
matching: album_4.clone(),
|
|
||||||
list: matches_4.iter().cloned().map(Into::into).collect(),
|
|
||||||
}));
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn artist_expectations() -> (ArtistMeta, Vec<Match<ArtistMeta>>) {
|
|
||||||
let artist = COLLECTION[3].meta.clone();
|
|
||||||
|
|
||||||
let artist_match_1 = Match::new(100, artist.clone());
|
|
||||||
let artist_match_2 = Match::new(50, artist.clone());
|
|
||||||
let matches = vec![artist_match_1.clone(), artist_match_2.clone()];
|
|
||||||
|
|
||||||
(artist, matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_artist_expectation(
|
|
||||||
api: &mut MockIMusicBrainz,
|
|
||||||
seq: &mut Sequence,
|
|
||||||
artist: &ArtistMeta,
|
|
||||||
matches: &[Match<ArtistMeta>],
|
|
||||||
) {
|
|
||||||
let result = Ok(matches.to_owned());
|
|
||||||
api.expect_search_artist()
|
|
||||||
.with(predicate::eq(artist.clone()))
|
|
||||||
.times(1)
|
|
||||||
.in_sequence(seq)
|
|
||||||
.return_once(|_| result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fetch_artist() {
|
|
||||||
let mut mb_api = MockIMusicBrainz::new();
|
|
||||||
|
|
||||||
let (artist, matches) = artist_expectations();
|
|
||||||
let mut seq = Sequence::new();
|
|
||||||
search_artist_expectation(&mut mb_api, &mut seq, &artist, &matches);
|
|
||||||
|
|
||||||
let music_hoard = music_hoard(COLLECTION.to_owned());
|
|
||||||
let (events_tx, events_rx) = event_channel();
|
|
||||||
let inner = AppInner::new(music_hoard, mb_api, events_tx);
|
|
||||||
|
|
||||||
let (fetch_tx, fetch_rx) = mpsc::channel();
|
|
||||||
// Use the fourth artist for this test as they have no MBID.
|
|
||||||
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[3], fetch_tx);
|
|
||||||
handle.join().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
|
|
||||||
let result = fetch_rx.try_recv().unwrap();
|
|
||||||
let expected = Ok(AppMatchesInfo::Artist(AppArtistMatches {
|
|
||||||
matching: artist.clone(),
|
|
||||||
list: matches.iter().cloned().map(Into::into).collect(),
|
|
||||||
}));
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fetch_artist_fetch_disconnect() {
|
|
||||||
let mut mb_api = MockIMusicBrainz::new();
|
|
||||||
|
|
||||||
let (artist, matches) = artist_expectations();
|
|
||||||
let mut seq = Sequence::new();
|
|
||||||
search_artist_expectation(&mut mb_api, &mut seq, &artist, &matches);
|
|
||||||
|
|
||||||
let music_hoard = music_hoard(COLLECTION.to_owned());
|
|
||||||
let (events_tx, events_rx) = event_channel();
|
|
||||||
let inner = AppInner::new(music_hoard, mb_api, events_tx);
|
|
||||||
|
|
||||||
let (fetch_tx, _) = mpsc::channel();
|
|
||||||
// Use the fourth artist for this test as they have no MBID.
|
|
||||||
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[3], fetch_tx);
|
|
||||||
handle.join().unwrap();
|
|
||||||
|
|
||||||
assert!(events_rx.try_recv().is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fetch_albums_event_disconnect() {
|
|
||||||
let mut mb_api = MockIMusicBrainz::new();
|
|
||||||
|
|
||||||
let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
|
|
||||||
|
|
||||||
let (album_1, matches_1) = album_expectations_1();
|
|
||||||
|
|
||||||
let mut seq = Sequence::new();
|
|
||||||
search_release_group_expectation(&mut mb_api, &mut seq, &arid, &album_1, &matches_1);
|
|
||||||
|
|
||||||
let music_hoard = music_hoard(COLLECTION.to_owned());
|
|
||||||
let (events_tx, _) = event_channel();
|
|
||||||
let inner = AppInner::new(music_hoard, mb_api, events_tx);
|
|
||||||
|
|
||||||
let (fetch_tx, fetch_rx) = mpsc::channel();
|
|
||||||
// Use the second artist for this test.
|
|
||||||
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[1], fetch_tx);
|
|
||||||
handle.join().unwrap();
|
|
||||||
|
|
||||||
let result = fetch_rx.try_recv().unwrap();
|
|
||||||
let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches {
|
|
||||||
matching: album_1.clone(),
|
|
||||||
list: matches_1.iter().cloned().map(Into::into).collect(),
|
|
||||||
}));
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
|
|
||||||
assert_eq!(fetch_rx.try_recv().unwrap_err(), TryRecvError::Disconnected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,24 @@
|
|||||||
use std::sync::mpsc::{self, TryRecvError};
|
use std::{
|
||||||
|
sync::{
|
||||||
|
mpsc::{self, TryRecvError},
|
||||||
|
Arc, Mutex,
|
||||||
|
},
|
||||||
|
thread, time,
|
||||||
|
};
|
||||||
|
|
||||||
|
use musichoard::collection::{
|
||||||
|
album::AlbumMeta,
|
||||||
|
artist::{Artist, ArtistMeta},
|
||||||
|
musicbrainz::{IMusicBrainzRef, Mbid},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
machine::{App, AppInner, AppMachine},
|
machine::{App, AppInner, AppMachine},
|
||||||
AppMatchesInfo, AppPublic, AppState, IAppEventFetch, IAppInteractFetch,
|
AppMatchesInfo, AppPublic, AppState, IAppEventFetch, IAppInteractFetch,
|
||||||
},
|
},
|
||||||
lib::interface::musicbrainz::Error as MbError,
|
event::{Event, EventSender},
|
||||||
|
lib::interface::musicbrainz::{self, Error as MbError, IMusicBrainz},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::matches::AppMatches;
|
use super::matches::AppMatches;
|
||||||
@ -30,7 +43,16 @@ impl AppMachine<AppFetch> {
|
|||||||
AppMachine { inner, state }
|
AppMachine { inner, state }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app_fetch_new(inner: AppInner, fetch_rx: FetchReceiver) -> App {
|
pub fn app_fetch_new(inner: AppInner) -> App {
|
||||||
|
let coll = inner.music_hoard.get_collection();
|
||||||
|
let artist = match inner.selection.state_artist(coll) {
|
||||||
|
Some(artist_state) => &coll[artist_state.index],
|
||||||
|
None => return AppMachine::error(inner, "cannot fetch: no artist selected").into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (fetch_tx, fetch_rx) = mpsc::channel::<FetchResult>();
|
||||||
|
Self::spawn_fetch_thread(&inner, artist, fetch_tx);
|
||||||
|
|
||||||
let fetch = AppFetch::new(fetch_rx);
|
let fetch = AppFetch::new(fetch_rx);
|
||||||
AppMachine::app_fetch(inner, fetch, true)
|
AppMachine::app_fetch(inner, fetch, true)
|
||||||
}
|
}
|
||||||
@ -62,6 +84,80 @@ impl AppMachine<AppFetch> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spawn_fetch_thread(
|
||||||
|
inner: &AppInner,
|
||||||
|
artist: &Artist,
|
||||||
|
tx: FetchSender,
|
||||||
|
) -> thread::JoinHandle<()> {
|
||||||
|
match artist.meta.musicbrainz {
|
||||||
|
Some(ref arid) => {
|
||||||
|
let musicbrainz = Arc::clone(&inner.musicbrainz);
|
||||||
|
let events = inner.events.clone();
|
||||||
|
let arid = arid.mbid().clone();
|
||||||
|
let albums = artist.albums.iter().map(|a| &a.meta).cloned().collect();
|
||||||
|
thread::spawn(|| Self::fetch_albums(musicbrainz, tx, events, arid, albums))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let musicbrainz = Arc::clone(&inner.musicbrainz);
|
||||||
|
let events = inner.events.clone();
|
||||||
|
let artist = artist.meta.clone();
|
||||||
|
thread::spawn(|| Self::fetch_artist(musicbrainz, tx, events, artist))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_artist(
|
||||||
|
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>,
|
||||||
|
fetch_tx: FetchSender,
|
||||||
|
events: EventSender,
|
||||||
|
artist: ArtistMeta,
|
||||||
|
) {
|
||||||
|
let result = musicbrainz.lock().unwrap().search_artist(&artist);
|
||||||
|
let result = result.map(|list| AppMatchesInfo::artist(artist, list));
|
||||||
|
Self::send_fetch_result(&fetch_tx, &events, result).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_albums(
|
||||||
|
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>,
|
||||||
|
fetch_tx: FetchSender,
|
||||||
|
events: EventSender,
|
||||||
|
arid: Mbid,
|
||||||
|
albums: Vec<AlbumMeta>,
|
||||||
|
) {
|
||||||
|
let mut musicbrainz = musicbrainz.lock().unwrap();
|
||||||
|
let mut album_iter = albums.into_iter().peekable();
|
||||||
|
while let Some(album) = album_iter.next() {
|
||||||
|
if album.musicbrainz.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = musicbrainz.search_release_group(&arid, &album);
|
||||||
|
let result = result.map(|list| AppMatchesInfo::album(album, list));
|
||||||
|
if Self::send_fetch_result(&fetch_tx, &events, result).is_err() {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if album_iter.peek().is_some() {
|
||||||
|
thread::sleep(time::Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_fetch_result(
|
||||||
|
fetch_tx: &FetchSender,
|
||||||
|
events: &EventSender,
|
||||||
|
result: Result<AppMatchesInfo, musicbrainz::Error>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
// If receiver disconnects just drop the rest.
|
||||||
|
fetch_tx.send(result).map_err(|_| ())?;
|
||||||
|
|
||||||
|
// If this send fails the event listener is dead. Don't panic as this function runs in a
|
||||||
|
// detached thread so this might be happening during normal shut down.
|
||||||
|
events.send(Event::FetchResultReady).map_err(|_| ())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AppMachine<AppFetch>> for App {
|
impl From<AppMachine<AppFetch>> for App {
|
||||||
@ -97,16 +193,207 @@ impl IAppEventFetch for AppMachine<AppFetch> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use mockall::{predicate, Sequence};
|
||||||
use musichoard::collection::artist::ArtistMeta;
|
use musichoard::collection::artist::ArtistMeta;
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::machine::tests::{inner, music_hoard},
|
app::{
|
||||||
lib::interface::musicbrainz::{self, Match},
|
machine::tests::{inner, music_hoard},
|
||||||
|
AppAlbumMatches, AppArtistMatches,
|
||||||
|
},
|
||||||
|
event::EventReceiver,
|
||||||
|
lib::interface::musicbrainz::{self, Match, MockIMusicBrainz},
|
||||||
testmod::COLLECTION,
|
testmod::COLLECTION,
|
||||||
|
EventChannel,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn event_channel() -> (EventSender, EventReceiver) {
|
||||||
|
let event_channel = EventChannel::new();
|
||||||
|
let events_tx = event_channel.sender();
|
||||||
|
let events_rx = event_channel.receiver();
|
||||||
|
(events_tx, events_rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn album_expectations_1() -> (AlbumMeta, Vec<Match<AlbumMeta>>) {
|
||||||
|
let album_1 = COLLECTION[1].albums[0].meta.clone();
|
||||||
|
let album_4 = COLLECTION[1].albums[3].meta.clone();
|
||||||
|
|
||||||
|
let album_match_1_1 = Match::new(100, album_1.clone());
|
||||||
|
let album_match_1_2 = Match::new(50, album_4.clone());
|
||||||
|
let matches_1 = vec![album_match_1_1.clone(), album_match_1_2.clone()];
|
||||||
|
|
||||||
|
(album_1, matches_1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn album_expectations_4() -> (AlbumMeta, Vec<Match<AlbumMeta>>) {
|
||||||
|
let album_1 = COLLECTION[1].albums[0].meta.clone();
|
||||||
|
let album_4 = COLLECTION[1].albums[3].meta.clone();
|
||||||
|
|
||||||
|
let album_match_4_1 = Match::new(100, album_4.clone());
|
||||||
|
let album_match_4_2 = Match::new(30, album_1.clone());
|
||||||
|
let matches_4 = vec![album_match_4_1.clone(), album_match_4_2.clone()];
|
||||||
|
|
||||||
|
(album_4, matches_4)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_release_group_expectation(
|
||||||
|
api: &mut MockIMusicBrainz,
|
||||||
|
seq: &mut Sequence,
|
||||||
|
arid: &Mbid,
|
||||||
|
album: &AlbumMeta,
|
||||||
|
matches: &[Match<AlbumMeta>],
|
||||||
|
) {
|
||||||
|
let result = Ok(matches.to_owned());
|
||||||
|
api.expect_search_release_group()
|
||||||
|
.with(predicate::eq(arid.clone()), predicate::eq(album.clone()))
|
||||||
|
.times(1)
|
||||||
|
.in_sequence(seq)
|
||||||
|
.return_once(|_, _| result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_albums() {
|
||||||
|
let mut mb_api = MockIMusicBrainz::new();
|
||||||
|
|
||||||
|
let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
|
||||||
|
|
||||||
|
let (album_1, matches_1) = album_expectations_1();
|
||||||
|
let (album_4, matches_4) = album_expectations_4();
|
||||||
|
|
||||||
|
// Other albums have an MBID and so they will be skipped.
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
|
||||||
|
search_release_group_expectation(&mut mb_api, &mut seq, &arid, &album_1, &matches_1);
|
||||||
|
search_release_group_expectation(&mut mb_api, &mut seq, &arid, &album_4, &matches_4);
|
||||||
|
|
||||||
|
let music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
let (events_tx, events_rx) = event_channel();
|
||||||
|
let inner = AppInner::new(music_hoard, mb_api, events_tx);
|
||||||
|
|
||||||
|
let (fetch_tx, fetch_rx) = mpsc::channel();
|
||||||
|
// Use the second artist for this test.
|
||||||
|
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[1], fetch_tx);
|
||||||
|
handle.join().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
|
||||||
|
let result = fetch_rx.try_recv().unwrap();
|
||||||
|
let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches {
|
||||||
|
matching: album_1.clone(),
|
||||||
|
list: matches_1.iter().cloned().map(Into::into).collect(),
|
||||||
|
}));
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
|
||||||
|
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
|
||||||
|
let result = fetch_rx.try_recv().unwrap();
|
||||||
|
let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches {
|
||||||
|
matching: album_4.clone(),
|
||||||
|
list: matches_4.iter().cloned().map(Into::into).collect(),
|
||||||
|
}));
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn artist_expectations() -> (ArtistMeta, Vec<Match<ArtistMeta>>) {
|
||||||
|
let artist = COLLECTION[3].meta.clone();
|
||||||
|
|
||||||
|
let artist_match_1 = Match::new(100, artist.clone());
|
||||||
|
let artist_match_2 = Match::new(50, artist.clone());
|
||||||
|
let matches = vec![artist_match_1.clone(), artist_match_2.clone()];
|
||||||
|
|
||||||
|
(artist, matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_artist_expectation(
|
||||||
|
api: &mut MockIMusicBrainz,
|
||||||
|
seq: &mut Sequence,
|
||||||
|
artist: &ArtistMeta,
|
||||||
|
matches: &[Match<ArtistMeta>],
|
||||||
|
) {
|
||||||
|
let result = Ok(matches.to_owned());
|
||||||
|
api.expect_search_artist()
|
||||||
|
.with(predicate::eq(artist.clone()))
|
||||||
|
.times(1)
|
||||||
|
.in_sequence(seq)
|
||||||
|
.return_once(|_| result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_artist() {
|
||||||
|
let mut mb_api = MockIMusicBrainz::new();
|
||||||
|
|
||||||
|
let (artist, matches) = artist_expectations();
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
search_artist_expectation(&mut mb_api, &mut seq, &artist, &matches);
|
||||||
|
|
||||||
|
let music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
let (events_tx, events_rx) = event_channel();
|
||||||
|
let inner = AppInner::new(music_hoard, mb_api, events_tx);
|
||||||
|
|
||||||
|
let (fetch_tx, fetch_rx) = mpsc::channel();
|
||||||
|
// Use the fourth artist for this test as they have no MBID.
|
||||||
|
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[3], fetch_tx);
|
||||||
|
handle.join().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
|
||||||
|
let result = fetch_rx.try_recv().unwrap();
|
||||||
|
let expected = Ok(AppMatchesInfo::Artist(AppArtistMatches {
|
||||||
|
matching: artist.clone(),
|
||||||
|
list: matches.iter().cloned().map(Into::into).collect(),
|
||||||
|
}));
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_artist_fetch_disconnect() {
|
||||||
|
let mut mb_api = MockIMusicBrainz::new();
|
||||||
|
|
||||||
|
let (artist, matches) = artist_expectations();
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
search_artist_expectation(&mut mb_api, &mut seq, &artist, &matches);
|
||||||
|
|
||||||
|
let music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
let (events_tx, events_rx) = event_channel();
|
||||||
|
let inner = AppInner::new(music_hoard, mb_api, events_tx);
|
||||||
|
|
||||||
|
let (fetch_tx, _) = mpsc::channel();
|
||||||
|
// Use the fourth artist for this test as they have no MBID.
|
||||||
|
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[3], fetch_tx);
|
||||||
|
handle.join().unwrap();
|
||||||
|
|
||||||
|
assert!(events_rx.try_recv().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_albums_event_disconnect() {
|
||||||
|
let mut mb_api = MockIMusicBrainz::new();
|
||||||
|
|
||||||
|
let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
|
||||||
|
|
||||||
|
let (album_1, matches_1) = album_expectations_1();
|
||||||
|
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
search_release_group_expectation(&mut mb_api, &mut seq, &arid, &album_1, &matches_1);
|
||||||
|
|
||||||
|
let music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
let (events_tx, _) = event_channel();
|
||||||
|
let inner = AppInner::new(music_hoard, mb_api, events_tx);
|
||||||
|
|
||||||
|
let (fetch_tx, fetch_rx) = mpsc::channel();
|
||||||
|
// Use the second artist for this test.
|
||||||
|
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[1], fetch_tx);
|
||||||
|
handle.join().unwrap();
|
||||||
|
|
||||||
|
let result = fetch_rx.try_recv().unwrap();
|
||||||
|
let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches {
|
||||||
|
matching: album_1.clone(),
|
||||||
|
list: matches_1.iter().cloned().map(Into::into).collect(),
|
||||||
|
}));
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
|
||||||
|
assert_eq!(fetch_rx.try_recv().unwrap_err(), TryRecvError::Disconnected);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn recv_ok_fetch_ok() {
|
fn recv_ok_fetch_ok() {
|
||||||
let (tx, rx) = mpsc::channel::<FetchResult>();
|
let (tx, rx) = mpsc::channel::<FetchResult>();
|
||||||
@ -115,7 +402,9 @@ mod tests {
|
|||||||
let fetch_result = Ok(AppMatchesInfo::artist::<Match<ArtistMeta>>(artist, vec![]));
|
let fetch_result = Ok(AppMatchesInfo::artist::<Match<ArtistMeta>>(artist, vec![]));
|
||||||
tx.send(fetch_result).unwrap();
|
tx.send(fetch_result).unwrap();
|
||||||
|
|
||||||
let app = AppMachine::app_fetch_new(inner(music_hoard(COLLECTION.clone())), rx);
|
let inner = inner(music_hoard(COLLECTION.clone()));
|
||||||
|
let fetch = AppFetch::new(rx);
|
||||||
|
let app = AppMachine::app_fetch(inner, fetch, true);
|
||||||
assert!(matches!(app, AppState::Matches(_)));
|
assert!(matches!(app, AppState::Matches(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +415,9 @@ mod tests {
|
|||||||
let fetch_result = Err(musicbrainz::Error::RateLimit);
|
let fetch_result = Err(musicbrainz::Error::RateLimit);
|
||||||
tx.send(fetch_result).unwrap();
|
tx.send(fetch_result).unwrap();
|
||||||
|
|
||||||
let app = AppMachine::app_fetch_new(inner(music_hoard(COLLECTION.clone())), rx);
|
let inner = inner(music_hoard(COLLECTION.clone()));
|
||||||
|
let fetch = AppFetch::new(rx);
|
||||||
|
let app = AppMachine::app_fetch(inner, fetch, true);
|
||||||
assert!(matches!(app, AppState::Error(_)));
|
assert!(matches!(app, AppState::Error(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +425,9 @@ mod tests {
|
|||||||
fn recv_err_empty() {
|
fn recv_err_empty() {
|
||||||
let (_tx, rx) = mpsc::channel::<FetchResult>();
|
let (_tx, rx) = mpsc::channel::<FetchResult>();
|
||||||
|
|
||||||
let app = AppMachine::app_fetch_new(inner(music_hoard(COLLECTION.clone())), rx);
|
let inner = inner(music_hoard(COLLECTION.clone()));
|
||||||
|
let fetch = AppFetch::new(rx);
|
||||||
|
let app = AppMachine::app_fetch(inner, fetch, true);
|
||||||
assert!(matches!(app, AppState::Fetch(_)));
|
assert!(matches!(app, AppState::Fetch(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +435,9 @@ mod tests {
|
|||||||
fn recv_err_disconnected_first() {
|
fn recv_err_disconnected_first() {
|
||||||
let (_, rx) = mpsc::channel::<FetchResult>();
|
let (_, rx) = mpsc::channel::<FetchResult>();
|
||||||
|
|
||||||
let app = AppMachine::app_fetch_new(inner(music_hoard(COLLECTION.clone())), rx);
|
let inner = inner(music_hoard(COLLECTION.clone()));
|
||||||
|
let fetch = AppFetch::new(rx);
|
||||||
|
let app = AppMachine::app_fetch(inner, fetch, true);
|
||||||
assert!(matches!(app, AppState::Matches(_)));
|
assert!(matches!(app, AppState::Matches(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +454,9 @@ mod tests {
|
|||||||
fn empty_first_then_ready() {
|
fn empty_first_then_ready() {
|
||||||
let (tx, rx) = mpsc::channel::<FetchResult>();
|
let (tx, rx) = mpsc::channel::<FetchResult>();
|
||||||
|
|
||||||
let app = AppMachine::app_fetch_new(inner(music_hoard(COLLECTION.clone())), rx);
|
let inner = inner(music_hoard(COLLECTION.clone()));
|
||||||
|
let fetch = AppFetch::new(rx);
|
||||||
|
let app = AppMachine::app_fetch(inner, fetch, true);
|
||||||
assert!(matches!(app, AppState::Fetch(_)));
|
assert!(matches!(app, AppState::Fetch(_)));
|
||||||
|
|
||||||
let artist = COLLECTION[3].meta.clone();
|
let artist = COLLECTION[3].meta.clone();
|
||||||
|
Loading…
Reference in New Issue
Block a user