Compare commits

..

5 Commits

Author SHA1 Message Date
21d0034b97 Remove stuff 2024-09-20 22:14:21 +02:00
b4d3eadd63 Now works 2024-09-20 22:06:01 +02:00
00269411e2 Works... sort of 2024-09-20 21:57:12 +02:00
a980086fe0 Daemon draft 2024-09-20 21:00:43 +02:00
f4472ee95d Api submodule 2024-09-15 16:21:17 +02:00
18 changed files with 2026 additions and 1803 deletions

View File

@ -4,7 +4,7 @@ extern crate test;
mod tui; mod tui;
use std::{ffi::OsString, fs::OpenOptions, io, path::PathBuf}; use std::{ffi::OsString, fs::OpenOptions, io, path::PathBuf, thread};
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
use structopt::StructOpt; use structopt::StructOpt;
@ -25,7 +25,10 @@ use musichoard::{
MusicHoardBuilder, NoDatabase, NoLibrary, MusicHoardBuilder, NoDatabase, NoLibrary,
}; };
use tui::{App, EventChannel, EventHandler, EventListener, MusicBrainz, Tui, Ui}; use tui::{
App, EventChannel, EventHandler, EventListener, MusicBrainz, MusicBrainzDaemon, RequestChannel,
Tui, Ui,
};
const MUSICHOARD_HTTP_USER_AGENT: &str = concat!( const MUSICHOARD_HTTP_USER_AGENT: &str = concat!(
"MusicHoard/", "MusicHoard/",
@ -91,7 +94,10 @@ fn with<Database: IDatabase + 'static, Library: ILibrary + 'static>(
let listener = EventListener::new(listener_sender); let listener = EventListener::new(listener_sender);
let handler = EventHandler::new(channel.receiver()); let handler = EventHandler::new(channel.receiver());
let app = App::new(music_hoard, musicbrainz, app_sender); let mb_request_channel = RequestChannel::new();
thread::spawn(|| MusicBrainzDaemon::run(musicbrainz, mb_request_channel.receiver, app_sender));
let app = App::new(music_hoard, mb_request_channel.sender);
let ui = Ui; let ui = Ui;
// Run the TUI application. // Run the TUI application.

View File

@ -77,105 +77,105 @@ impl IAppInteractBrowse for AppMachine<BrowseState> {
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use crate::tui::{ // use crate::tui::{
app::{ // app::{
machine::tests::{inner, inner_with_mb, music_hoard}, // machine::tests::{inner, inner_with_mb, music_hoard},
Category, IApp, IAppAccess, // Category, IApp, IAppAccess,
}, // },
lib::interface::musicbrainz::MockIMusicBrainz, // lib::interface::musicbrainz::api::MockIMusicBrainz,
testmod::COLLECTION, // testmod::COLLECTION,
}; // };
use super::*; // use super::*;
#[test] // #[test]
fn quit() { // fn quit() {
let music_hoard = music_hoard(vec![]); // let music_hoard = music_hoard(vec![]);
let browse = AppMachine::browse_state(inner(music_hoard)); // let browse = AppMachine::browse_state(inner(music_hoard));
let app = browse.quit(); // let app = browse.quit();
assert!(!app.is_running()); // assert!(!app.is_running());
app.unwrap_browse(); // app.unwrap_browse();
} // }
#[test] // #[test]
fn increment_decrement() { // fn increment_decrement() {
let mut browse = AppMachine::browse_state(inner(music_hoard(COLLECTION.to_owned()))); // let mut browse = AppMachine::browse_state(inner(music_hoard(COLLECTION.to_owned())));
let sel = &browse.inner.selection; // let sel = &browse.inner.selection;
assert_eq!(sel.category(), Category::Artist); // assert_eq!(sel.category(), Category::Artist);
assert_eq!(sel.selected(), Some(0)); // assert_eq!(sel.selected(), Some(0));
browse = browse.increment_selection(Delta::Line).unwrap_browse(); // browse = browse.increment_selection(Delta::Line).unwrap_browse();
let sel = &browse.inner.selection; // let sel = &browse.inner.selection;
assert_eq!(sel.category(), Category::Artist); // assert_eq!(sel.category(), Category::Artist);
assert_eq!(sel.selected(), Some(1)); // assert_eq!(sel.selected(), Some(1));
browse = browse.increment_category().unwrap_browse(); // browse = browse.increment_category().unwrap_browse();
let sel = &browse.inner.selection; // let sel = &browse.inner.selection;
assert_eq!(sel.category(), Category::Album); // assert_eq!(sel.category(), Category::Album);
assert_eq!(sel.selected(), Some(0)); // assert_eq!(sel.selected(), Some(0));
browse = browse.increment_selection(Delta::Line).unwrap_browse(); // browse = browse.increment_selection(Delta::Line).unwrap_browse();
let sel = &browse.inner.selection; // let sel = &browse.inner.selection;
assert_eq!(sel.category(), Category::Album); // assert_eq!(sel.category(), Category::Album);
assert_eq!(sel.selected(), Some(1)); // assert_eq!(sel.selected(), Some(1));
browse = browse.decrement_selection(Delta::Line).unwrap_browse(); // browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let sel = &browse.inner.selection; // let sel = &browse.inner.selection;
assert_eq!(sel.category(), Category::Album); // assert_eq!(sel.category(), Category::Album);
assert_eq!(sel.selected(), Some(0)); // assert_eq!(sel.selected(), Some(0));
browse = browse.decrement_category().unwrap_browse(); // browse = browse.decrement_category().unwrap_browse();
let sel = &browse.inner.selection; // let sel = &browse.inner.selection;
assert_eq!(sel.category(), Category::Artist); // assert_eq!(sel.category(), Category::Artist);
assert_eq!(sel.selected(), Some(1)); // assert_eq!(sel.selected(), Some(1));
browse = browse.decrement_selection(Delta::Line).unwrap_browse(); // browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let sel = &browse.inner.selection; // let sel = &browse.inner.selection;
assert_eq!(sel.category(), Category::Artist); // assert_eq!(sel.category(), Category::Artist);
assert_eq!(sel.selected(), Some(0)); // assert_eq!(sel.selected(), Some(0));
} // }
#[test] // #[test]
fn show_info_overlay() { // fn show_info_overlay() {
let browse = AppMachine::browse_state(inner(music_hoard(vec![]))); // let browse = AppMachine::browse_state(inner(music_hoard(vec![])));
let app = browse.show_info_overlay(); // let app = browse.show_info_overlay();
app.unwrap_info(); // app.unwrap_info();
} // }
#[test] // #[test]
fn show_reload_menu() { // fn show_reload_menu() {
let browse = AppMachine::browse_state(inner(music_hoard(vec![]))); // let browse = AppMachine::browse_state(inner(music_hoard(vec![])));
let app = browse.show_reload_menu(); // let app = browse.show_reload_menu();
app.unwrap_reload(); // app.unwrap_reload();
} // }
#[test] // #[test]
fn begin_search() { // fn begin_search() {
let browse = AppMachine::browse_state(inner(music_hoard(vec![]))); // let browse = AppMachine::browse_state(inner(music_hoard(vec![])));
let app = browse.begin_search(); // let app = browse.begin_search();
app.unwrap_search(); // app.unwrap_search();
} // }
#[test] // #[test]
fn fetch_musicbrainz() { // fn fetch_musicbrainz() {
let mb_api = MockIMusicBrainz::new(); // let mb_api = MockIMusicBrainz::new();
let browse = // let browse =
AppMachine::browse_state(inner_with_mb(music_hoard(COLLECTION.to_owned()), mb_api)); // AppMachine::browse_state(inner_with_mb(music_hoard(COLLECTION.to_owned()), mb_api));
// Use the second artist for this test. // // Use the second artist for this test.
let browse = browse.increment_selection(Delta::Line).unwrap_browse(); // let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let mut app = browse.fetch_musicbrainz(); // let mut app = browse.fetch_musicbrainz();
let public = app.get(); // let public = app.get();
// Because of fetch's 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::Match(_)) // matches!(public.state, AppState::Match(_))
|| matches!(public.state, AppState::Fetch(_)) // || matches!(public.state, AppState::Fetch(_))
); // );
} // }
} // }

View File

@ -41,16 +41,16 @@ impl IAppInteractError for AppMachine<ErrorState> {
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use crate::tui::app::machine::tests::{inner, music_hoard}; // use crate::tui::app::machine::tests::{inner, music_hoard};
use super::*; // use super::*;
#[test] // #[test]
fn dismiss_error() { // fn dismiss_error() {
let error = AppMachine::error_state(inner(music_hoard(vec![])), "get rekt"); // let error = AppMachine::error_state(inner(music_hoard(vec![])), "get rekt");
let app = error.dismiss_error(); // let app = error.dismiss_error();
app.unwrap_browse(); // app.unwrap_browse();
} // }
} // }

View File

@ -1,24 +1,19 @@
use std::{ use std::{collections::VecDeque, sync::mpsc::{self, TryRecvError}};
sync::{
mpsc::{self, TryRecvError},
Arc, Mutex,
},
thread, time,
};
use musichoard::collection::{ use musichoard::collection::{artist::Artist, musicbrainz::IMusicBrainzRef};
album::AlbumMeta,
artist::{Artist, ArtistMeta},
musicbrainz::{IMusicBrainzRef, Mbid},
};
use crate::tui::{ use crate::tui::{
app::{ app::{
machine::{match_state::MatchState, App, AppInner, AppMachine}, machine::{match_state::MatchState, App, AppInner, AppMachine},
AppPublicState, AppState, IAppEventFetch, IAppInteractFetch, MatchStateInfo, AppPublicState, AppState, IAppEventFetch, IAppInteractFetch, MatchStateInfo,
}, },
event::{Event, EventSender}, lib::{
lib::interface::musicbrainz::{self, Error as MbError, IMusicBrainz}, external::musicbrainz::daemon::{
ApiParams, Job, JobInstance, JobPriority, ReturnSender, SearchArtistParams,
SearchParams, SearchReleaseGroupParams,
},
interface::musicbrainz::api::Error as MbError,
},
}; };
pub struct FetchState { pub struct FetchState {
@ -33,7 +28,6 @@ impl FetchState {
pub type FetchError = MbError; pub type FetchError = MbError;
pub type FetchResult = Result<MatchStateInfo, FetchError>; pub type FetchResult = Result<MatchStateInfo, FetchError>;
pub type FetchSender = mpsc::Sender<FetchResult>;
pub type FetchReceiver = mpsc::Receiver<FetchResult>; pub type FetchReceiver = mpsc::Receiver<FetchResult>;
impl AppMachine<FetchState> { impl AppMachine<FetchState> {
@ -51,7 +45,7 @@ impl AppMachine<FetchState> {
}; };
let (fetch_tx, fetch_rx) = mpsc::channel::<FetchResult>(); let (fetch_tx, fetch_rx) = mpsc::channel::<FetchResult>();
Self::spawn_fetch_thread(&inner, artist, fetch_tx); Self::submit_fetch_job(&inner, artist, fetch_tx);
let fetch = FetchState::new(fetch_rx); let fetch = FetchState::new(fetch_rx);
AppMachine::app_fetch(inner, fetch, true) AppMachine::app_fetch(inner, fetch, true)
@ -85,78 +79,25 @@ impl AppMachine<FetchState> {
} }
} }
fn spawn_fetch_thread( fn submit_fetch_job(inner: &AppInner, artist: &Artist, tx: ReturnSender) {
inner: &AppInner, let mut queue = VecDeque::new();
artist: &Artist,
tx: FetchSender,
) -> thread::JoinHandle<()> {
match artist.meta.musicbrainz { match artist.meta.musicbrainz {
Some(ref arid) => { Some(ref arid) => {
let musicbrainz = Arc::clone(&inner.musicbrainz);
let events = inner.events.clone();
let arid = arid.mbid().clone(); let arid = arid.mbid().clone();
let albums = artist.albums.iter().map(|a| &a.meta).cloned().collect(); for album in artist.albums.iter() {
thread::spawn(|| Self::fetch_albums(musicbrainz, tx, events, arid, albums)) queue.push_back(ApiParams::search(SearchParams::release_group(
SearchReleaseGroupParams::new(arid.clone(), album.meta.clone()),
)));
}
} }
None => { None => {
let musicbrainz = Arc::clone(&inner.musicbrainz); queue.push_back(ApiParams::search(SearchParams::artist(
let events = inner.events.clone(); SearchArtistParams::new(artist.meta.clone()),
let artist = artist.meta.clone(); )));
thread::spawn(|| Self::fetch_artist(musicbrainz, tx, events, artist))
} }
} }
} let job = Job::new(JobPriority::Background, JobInstance::new(tx, queue));
inner.musicbrainz.send(job);
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| MatchStateInfo::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| MatchStateInfo::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<MatchStateInfo, 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(())
} }
} }
@ -188,296 +129,299 @@ impl IAppEventFetch for AppMachine<FetchState> {
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use mockall::{predicate, Sequence}; // use mockall::{predicate, Sequence};
use musichoard::collection::artist::ArtistMeta; // use musichoard::collection::artist::ArtistMeta;
use crate::tui::{ // use crate::tui::{
app::{ // app::{
machine::tests::{inner, music_hoard}, // machine::tests::{inner, music_hoard},
AlbumMatches, ArtistMatches, IApp, // AlbumMatches, ArtistMatches, IApp,
}, // },
event::EventReceiver, // event::EventReceiver,
lib::interface::musicbrainz::{self, Match, MockIMusicBrainz}, // lib::interface::musicbrainz::{
testmod::COLLECTION, // self,
EventChannel, // api::{Match, MockIMusicBrainz},
}; // },
// testmod::COLLECTION,
// EventChannel,
// };
use super::*; // use super::*;
#[test] // #[test]
fn fetch_no_artist() { // fn fetch_no_artist() {
let app = AppMachine::app_fetch_new(inner(music_hoard(vec![]))); // let app = AppMachine::app_fetch_new(inner(music_hoard(vec![])));
assert!(matches!(app.state(), AppState::Error(_))); // assert!(matches!(app.state(), AppState::Error(_)));
} // }
fn event_channel() -> (EventSender, EventReceiver) { // fn event_channel() -> (EventSender, EventReceiver) {
let event_channel = EventChannel::new(); // let event_channel = EventChannel::new();
let events_tx = event_channel.sender(); // let events_tx = event_channel.sender();
let events_rx = event_channel.receiver(); // let events_rx = event_channel.receiver();
(events_tx, events_rx) // (events_tx, events_rx)
} // }
fn album_expectations_1() -> (AlbumMeta, Vec<Match<AlbumMeta>>) { // fn album_expectations_1() -> (AlbumMeta, Vec<Match<AlbumMeta>>) {
let album_1 = COLLECTION[1].albums[0].meta.clone(); // let album_1 = COLLECTION[1].albums[0].meta.clone();
let album_4 = COLLECTION[1].albums[3].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_1 = Match::new(100, album_1.clone());
let album_match_1_2 = Match::new(50, album_4.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()]; // let matches_1 = vec![album_match_1_1.clone(), album_match_1_2.clone()];
(album_1, matches_1) // (album_1, matches_1)
} // }
fn album_expectations_4() -> (AlbumMeta, Vec<Match<AlbumMeta>>) { // fn album_expectations_4() -> (AlbumMeta, Vec<Match<AlbumMeta>>) {
let album_1 = COLLECTION[1].albums[0].meta.clone(); // let album_1 = COLLECTION[1].albums[0].meta.clone();
let album_4 = COLLECTION[1].albums[3].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_1 = Match::new(100, album_4.clone());
let album_match_4_2 = Match::new(30, album_1.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()]; // let matches_4 = vec![album_match_4_1.clone(), album_match_4_2.clone()];
(album_4, matches_4) // (album_4, matches_4)
} // }
fn search_release_group_expectation( // fn search_release_group_expectation(
api: &mut MockIMusicBrainz, // api: &mut MockIMusicBrainz,
seq: &mut Sequence, // seq: &mut Sequence,
arid: &Mbid, // arid: &Mbid,
album: &AlbumMeta, // album: &AlbumMeta,
matches: &[Match<AlbumMeta>], // matches: &[Match<AlbumMeta>],
) { // ) {
let result = Ok(matches.to_owned()); // let result = Ok(matches.to_owned());
api.expect_search_release_group() // api.expect_search_release_group()
.with(predicate::eq(arid.clone()), predicate::eq(album.clone())) // .with(predicate::eq(arid.clone()), predicate::eq(album.clone()))
.times(1) // .times(1)
.in_sequence(seq) // .in_sequence(seq)
.return_once(|_, _| result); // .return_once(|_, _| result);
} // }
#[test] // #[test]
fn fetch_albums() { // fn fetch_albums() {
let mut mb_api = MockIMusicBrainz::new(); // let mut mb_api = MockIMusicBrainz::new();
let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); // let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
let (album_1, matches_1) = album_expectations_1(); // let (album_1, matches_1) = album_expectations_1();
let (album_4, matches_4) = album_expectations_4(); // let (album_4, matches_4) = album_expectations_4();
// Other albums have an MBID and so they will be skipped. // // Other albums have an MBID and so they will be skipped.
let mut seq = Sequence::new(); // 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_1, &matches_1);
search_release_group_expectation(&mut mb_api, &mut seq, &arid, &album_4, &matches_4); // search_release_group_expectation(&mut mb_api, &mut seq, &arid, &album_4, &matches_4);
let music_hoard = music_hoard(COLLECTION.to_owned()); // let music_hoard = music_hoard(COLLECTION.to_owned());
let (events_tx, events_rx) = event_channel(); // let (events_tx, events_rx) = event_channel();
let inner = AppInner::new(music_hoard, mb_api, events_tx); // let inner = AppInner::new(music_hoard, mb_api, events_tx);
let (fetch_tx, fetch_rx) = mpsc::channel(); // let (fetch_tx, fetch_rx) = mpsc::channel();
// Use the second artist for this test. // // Use the second artist for this test.
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[1], fetch_tx); // let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[1], fetch_tx);
handle.join().unwrap(); // handle.join().unwrap();
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady); // assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
let result = fetch_rx.try_recv().unwrap(); // let result = fetch_rx.try_recv().unwrap();
let expected = Ok(MatchStateInfo::Album(AlbumMatches { // let expected = Ok(MatchStateInfo::Album(AlbumMatches {
matching: album_1.clone(), // matching: album_1.clone(),
list: matches_1.iter().cloned().map(Into::into).collect(), // list: matches_1.iter().cloned().map(Into::into).collect(),
})); // }));
assert_eq!(result, expected); // assert_eq!(result, expected);
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady); // assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
let result = fetch_rx.try_recv().unwrap(); // let result = fetch_rx.try_recv().unwrap();
let expected = Ok(MatchStateInfo::Album(AlbumMatches { // let expected = Ok(MatchStateInfo::Album(AlbumMatches {
matching: album_4.clone(), // matching: album_4.clone(),
list: matches_4.iter().cloned().map(Into::into).collect(), // list: matches_4.iter().cloned().map(Into::into).collect(),
})); // }));
assert_eq!(result, expected); // assert_eq!(result, expected);
} // }
fn artist_expectations() -> (ArtistMeta, Vec<Match<ArtistMeta>>) { // fn artist_expectations() -> (ArtistMeta, Vec<Match<ArtistMeta>>) {
let artist = COLLECTION[3].meta.clone(); // let artist = COLLECTION[3].meta.clone();
let artist_match_1 = Match::new(100, artist.clone()); // let artist_match_1 = Match::new(100, artist.clone());
let artist_match_2 = Match::new(50, artist.clone()); // let artist_match_2 = Match::new(50, artist.clone());
let matches = vec![artist_match_1.clone(), artist_match_2.clone()]; // let matches = vec![artist_match_1.clone(), artist_match_2.clone()];
(artist, matches) // (artist, matches)
} // }
fn search_artist_expectation( // fn search_artist_expectation(
api: &mut MockIMusicBrainz, // api: &mut MockIMusicBrainz,
seq: &mut Sequence, // seq: &mut Sequence,
artist: &ArtistMeta, // artist: &ArtistMeta,
matches: &[Match<ArtistMeta>], // matches: &[Match<ArtistMeta>],
) { // ) {
let result = Ok(matches.to_owned()); // let result = Ok(matches.to_owned());
api.expect_search_artist() // api.expect_search_artist()
.with(predicate::eq(artist.clone())) // .with(predicate::eq(artist.clone()))
.times(1) // .times(1)
.in_sequence(seq) // .in_sequence(seq)
.return_once(|_| result); // .return_once(|_| result);
} // }
#[test] // #[test]
fn fetch_artist() { // fn fetch_artist() {
let mut mb_api = MockIMusicBrainz::new(); // let mut mb_api = MockIMusicBrainz::new();
let (artist, matches) = artist_expectations(); // let (artist, matches) = artist_expectations();
let mut seq = Sequence::new(); // let mut seq = Sequence::new();
search_artist_expectation(&mut mb_api, &mut seq, &artist, &matches); // search_artist_expectation(&mut mb_api, &mut seq, &artist, &matches);
let music_hoard = music_hoard(COLLECTION.to_owned()); // let music_hoard = music_hoard(COLLECTION.to_owned());
let (events_tx, events_rx) = event_channel(); // let (events_tx, events_rx) = event_channel();
let inner = AppInner::new(music_hoard, mb_api, events_tx); // let inner = AppInner::new(music_hoard, mb_api, events_tx);
let (fetch_tx, fetch_rx) = mpsc::channel(); // let (fetch_tx, fetch_rx) = mpsc::channel();
// Use the fourth artist for this test as they have no MBID. // // Use the fourth artist for this test as they have no MBID.
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[3], fetch_tx); // let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[3], fetch_tx);
handle.join().unwrap(); // handle.join().unwrap();
assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady); // assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady);
let result = fetch_rx.try_recv().unwrap(); // let result = fetch_rx.try_recv().unwrap();
let expected = Ok(MatchStateInfo::Artist(ArtistMatches { // let expected = Ok(MatchStateInfo::Artist(ArtistMatches {
matching: artist.clone(), // matching: artist.clone(),
list: matches.iter().cloned().map(Into::into).collect(), // list: matches.iter().cloned().map(Into::into).collect(),
})); // }));
assert_eq!(result, expected); // assert_eq!(result, expected);
} // }
#[test] // #[test]
fn fetch_artist_fetch_disconnect() { // fn fetch_artist_fetch_disconnect() {
let mut mb_api = MockIMusicBrainz::new(); // let mut mb_api = MockIMusicBrainz::new();
let (artist, matches) = artist_expectations(); // let (artist, matches) = artist_expectations();
let mut seq = Sequence::new(); // let mut seq = Sequence::new();
search_artist_expectation(&mut mb_api, &mut seq, &artist, &matches); // search_artist_expectation(&mut mb_api, &mut seq, &artist, &matches);
let music_hoard = music_hoard(COLLECTION.to_owned()); // let music_hoard = music_hoard(COLLECTION.to_owned());
let (events_tx, events_rx) = event_channel(); // let (events_tx, events_rx) = event_channel();
let inner = AppInner::new(music_hoard, mb_api, events_tx); // let inner = AppInner::new(music_hoard, mb_api, events_tx);
let (fetch_tx, _) = mpsc::channel(); // let (fetch_tx, _) = mpsc::channel();
// Use the fourth artist for this test as they have no MBID. // // Use the fourth artist for this test as they have no MBID.
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[3], fetch_tx); // let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[3], fetch_tx);
handle.join().unwrap(); // handle.join().unwrap();
assert!(events_rx.try_recv().is_err()); // assert!(events_rx.try_recv().is_err());
} // }
#[test] // #[test]
fn fetch_albums_event_disconnect() { // fn fetch_albums_event_disconnect() {
let mut mb_api = MockIMusicBrainz::new(); // let mut mb_api = MockIMusicBrainz::new();
let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); // let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
let (album_1, matches_1) = album_expectations_1(); // let (album_1, matches_1) = album_expectations_1();
let mut seq = Sequence::new(); // 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_1, &matches_1);
let music_hoard = music_hoard(COLLECTION.to_owned()); // let music_hoard = music_hoard(COLLECTION.to_owned());
let (events_tx, _) = event_channel(); // let (events_tx, _) = event_channel();
let inner = AppInner::new(music_hoard, mb_api, events_tx); // let inner = AppInner::new(music_hoard, mb_api, events_tx);
let (fetch_tx, fetch_rx) = mpsc::channel(); // let (fetch_tx, fetch_rx) = mpsc::channel();
// Use the second artist for this test. // // Use the second artist for this test.
let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[1], fetch_tx); // let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[1], fetch_tx);
handle.join().unwrap(); // handle.join().unwrap();
let result = fetch_rx.try_recv().unwrap(); // let result = fetch_rx.try_recv().unwrap();
let expected = Ok(MatchStateInfo::Album(AlbumMatches { // let expected = Ok(MatchStateInfo::Album(AlbumMatches {
matching: album_1.clone(), // matching: album_1.clone(),
list: matches_1.iter().cloned().map(Into::into).collect(), // list: matches_1.iter().cloned().map(Into::into).collect(),
})); // }));
assert_eq!(result, expected); // assert_eq!(result, expected);
assert_eq!(fetch_rx.try_recv().unwrap_err(), TryRecvError::Disconnected); // 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>();
let artist = COLLECTION[3].meta.clone(); // let artist = COLLECTION[3].meta.clone();
let fetch_result = Ok(MatchStateInfo::artist::<Match<ArtistMeta>>(artist, vec![])); // let fetch_result = Ok(MatchStateInfo::artist::<Match<ArtistMeta>>(artist, vec![]));
tx.send(fetch_result).unwrap(); // tx.send(fetch_result).unwrap();
let inner = inner(music_hoard(COLLECTION.clone())); // let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = FetchState::new(rx); // let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch(inner, fetch, true); // let app = AppMachine::app_fetch(inner, fetch, true);
assert!(matches!(app, AppState::Match(_))); // assert!(matches!(app, AppState::Match(_)));
} // }
#[test] // #[test]
fn recv_ok_fetch_err() { // fn recv_ok_fetch_err() {
let (tx, rx) = mpsc::channel::<FetchResult>(); // let (tx, rx) = mpsc::channel::<FetchResult>();
let fetch_result = Err(musicbrainz::Error::RateLimit); // let fetch_result = Err(musicbrainz::api::Error::RateLimit);
tx.send(fetch_result).unwrap(); // tx.send(fetch_result).unwrap();
let inner = inner(music_hoard(COLLECTION.clone())); // let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = FetchState::new(rx); // let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch(inner, fetch, true); // let app = AppMachine::app_fetch(inner, fetch, true);
assert!(matches!(app, AppState::Error(_))); // assert!(matches!(app, AppState::Error(_)));
} // }
#[test] // #[test]
fn recv_err_empty() { // fn recv_err_empty() {
let (_tx, rx) = mpsc::channel::<FetchResult>(); // let (_tx, rx) = mpsc::channel::<FetchResult>();
let inner = inner(music_hoard(COLLECTION.clone())); // let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = FetchState::new(rx); // let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch(inner, fetch, true); // let app = AppMachine::app_fetch(inner, fetch, true);
assert!(matches!(app, AppState::Fetch(_))); // assert!(matches!(app, AppState::Fetch(_)));
} // }
#[test] // #[test]
fn recv_err_disconnected_first() { // fn recv_err_disconnected_first() {
let (_, rx) = mpsc::channel::<FetchResult>(); // let (_, rx) = mpsc::channel::<FetchResult>();
let inner = inner(music_hoard(COLLECTION.clone())); // let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = FetchState::new(rx); // let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch(inner, fetch, true); // let app = AppMachine::app_fetch(inner, fetch, true);
assert!(matches!(app, AppState::Match(_))); // assert!(matches!(app, AppState::Match(_)));
} // }
#[test] // #[test]
fn recv_err_disconnected_next() { // fn recv_err_disconnected_next() {
let (_, rx) = mpsc::channel::<FetchResult>(); // let (_, rx) = mpsc::channel::<FetchResult>();
let fetch = FetchState::new(rx); // let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch_next(inner(music_hoard(COLLECTION.clone())), fetch); // let app = AppMachine::app_fetch_next(inner(music_hoard(COLLECTION.clone())), fetch);
assert!(matches!(app, AppState::Browse(_))); // assert!(matches!(app, AppState::Browse(_)));
} // }
#[test] // #[test]
fn empty_first_then_ready() { // fn empty_first_then_ready() {
let (tx, rx) = mpsc::channel::<FetchResult>(); // let (tx, rx) = mpsc::channel::<FetchResult>();
let inner = inner(music_hoard(COLLECTION.clone())); // let inner = inner(music_hoard(COLLECTION.clone()));
let fetch = FetchState::new(rx); // let fetch = FetchState::new(rx);
let app = AppMachine::app_fetch(inner, fetch, true); // 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();
let fetch_result = Ok(MatchStateInfo::artist::<Match<ArtistMeta>>(artist, vec![])); // let fetch_result = Ok(MatchStateInfo::artist::<Match<ArtistMeta>>(artist, vec![]));
tx.send(fetch_result).unwrap(); // tx.send(fetch_result).unwrap();
let app = app.unwrap_fetch().fetch_result_ready(); // let app = app.unwrap_fetch().fetch_result_ready();
assert!(matches!(app, AppState::Match(_))); // assert!(matches!(app, AppState::Match(_)));
} // }
#[test] // #[test]
fn abort() { // fn abort() {
let (_, rx) = mpsc::channel::<FetchResult>(); // let (_, rx) = mpsc::channel::<FetchResult>();
let fetch = FetchState::new(rx); // let fetch = FetchState::new(rx);
let app = AppMachine::fetch_state(inner(music_hoard(COLLECTION.clone())), fetch); // let app = AppMachine::fetch_state(inner(music_hoard(COLLECTION.clone())), fetch);
let app = app.abort(); // let app = app.abort();
assert!(matches!(app, AppState::Browse(_))); // assert!(matches!(app, AppState::Browse(_)));
} // }
} // }

View File

@ -31,16 +31,16 @@ impl IAppInteractInfo for AppMachine<InfoState> {
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use crate::tui::app::machine::tests::{inner, music_hoard}; // use crate::tui::app::machine::tests::{inner, music_hoard};
use super::*; // use super::*;
#[test] // #[test]
fn hide_info_overlay() { // fn hide_info_overlay() {
let info = AppMachine::info_state(inner(music_hoard(vec![]))); // let info = AppMachine::info_state(inner(music_hoard(vec![])));
let app = info.hide_info_overlay(); // let app = info.hide_info_overlay();
app.unwrap_browse(); // app.unwrap_browse();
} // }
} // }

View File

@ -55,45 +55,45 @@ impl IAppInput for AppInputMode {
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use crate::tui::app::{ // use crate::tui::app::{
machine::tests::{events, mb_api, music_hoard_init}, // machine::tests::{events, mb_api, music_hoard_init},
IApp, // IApp,
}; // };
use super::*; // use super::*;
fn input_event(c: char) -> InputEvent { // fn input_event(c: char) -> InputEvent {
crossterm::event::KeyEvent::new( // crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Char(c), // crossterm::event::KeyCode::Char(c),
crossterm::event::KeyModifiers::empty(), // crossterm::event::KeyModifiers::empty(),
) // )
.into() // .into()
} // }
#[test] // #[test]
fn handle_input() { // fn handle_input() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); // let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
app.input_mut().replace(Input::default()); // app.input_mut().replace(Input::default());
let input = app.mode().unwrap_input(); // let input = app.mode().unwrap_input();
let app = input.input(input_event('H')); // let app = input.input(input_event('H'));
let input = app.mode().unwrap_input(); // let input = app.mode().unwrap_input();
let app = input.input(input_event('e')); // let app = input.input(input_event('e'));
let input = app.mode().unwrap_input(); // let input = app.mode().unwrap_input();
let app = input.input(input_event('l')); // let app = input.input(input_event('l'));
let input = app.mode().unwrap_input(); // let input = app.mode().unwrap_input();
let app = input.input(input_event('l')); // let app = input.input(input_event('l'));
let input = app.mode().unwrap_input(); // let input = app.mode().unwrap_input();
let app = input.input(input_event('o')); // let app = input.input(input_event('o'));
assert_eq!(app.input_ref().as_ref().unwrap().0.value(), "Hello"); // assert_eq!(app.input_ref().as_ref().unwrap().0.value(), "Hello");
app.mode().unwrap_input().confirm().unwrap_browse(); // app.mode().unwrap_input().confirm().unwrap_browse();
} // }
} // }

View File

@ -165,218 +165,218 @@ impl IAppInteractMatch for AppMachine<MatchState> {
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use std::sync::mpsc; // use std::sync::mpsc;
use musichoard::collection::{ // use musichoard::collection::{
album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType}, // album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType},
artist::{ArtistId, ArtistMeta}, // artist::{ArtistId, ArtistMeta},
}; // };
use crate::tui::{ // use crate::tui::{
app::{ // app::{
machine::tests::{inner, music_hoard}, // machine::tests::{inner, music_hoard},
IApp, IAppAccess, IAppInput, // IApp, IAppAccess, IAppInput,
}, // },
lib::interface::musicbrainz::Match, // lib::interface::musicbrainz::api::Match,
}; // };
use super::*; // use super::*;
impl<T> Match<T> { // impl<T> Match<T> {
pub fn new(score: u8, item: T) -> Self { // pub fn new(score: u8, item: T) -> Self {
Match { // Match {
score, // score,
item, // item,
disambiguation: None, // disambiguation: None,
} // }
} // }
} // }
fn artist_match() -> MatchStateInfo { // fn artist_match() -> MatchStateInfo {
let artist = ArtistMeta::new(ArtistId::new("Artist")); // let artist = ArtistMeta::new(ArtistId::new("Artist"));
let artist_1 = artist.clone(); // let artist_1 = artist.clone();
let artist_match_1 = Match::new(100, artist_1); // let artist_match_1 = Match::new(100, artist_1);
let artist_2 = artist.clone(); // let artist_2 = artist.clone();
let mut artist_match_2 = Match::new(100, artist_2); // let mut artist_match_2 = Match::new(100, artist_2);
artist_match_2.disambiguation = Some(String::from("some disambiguation")); // artist_match_2.disambiguation = Some(String::from("some disambiguation"));
let list = vec![artist_match_1.clone(), artist_match_2.clone()]; // let list = vec![artist_match_1.clone(), artist_match_2.clone()];
MatchStateInfo::artist(artist, list) // MatchStateInfo::artist(artist, list)
} // }
fn album_match() -> MatchStateInfo { // fn album_match() -> MatchStateInfo {
let album = AlbumMeta::new( // let album = AlbumMeta::new(
AlbumId::new("Album"), // AlbumId::new("Album"),
AlbumDate::new(Some(1990), Some(5), None), // AlbumDate::new(Some(1990), Some(5), None),
Some(AlbumPrimaryType::Album), // Some(AlbumPrimaryType::Album),
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], // vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
); // );
let album_1 = album.clone(); // let album_1 = album.clone();
let album_match_1 = Match::new(100, album_1); // let album_match_1 = Match::new(100, album_1);
let mut album_2 = album.clone(); // let mut album_2 = album.clone();
album_2.id.title.push_str(" extra title part"); // album_2.id.title.push_str(" extra title part");
album_2.secondary_types.pop(); // album_2.secondary_types.pop();
let album_match_2 = Match::new(100, album_2); // let album_match_2 = Match::new(100, album_2);
let list = vec![album_match_1.clone(), album_match_2.clone()]; // let list = vec![album_match_1.clone(), album_match_2.clone()];
MatchStateInfo::album(album, list) // MatchStateInfo::album(album, list)
} // }
fn fetch_state() -> FetchState { // fn fetch_state() -> FetchState {
let (_, rx) = mpsc::channel(); // let (_, rx) = mpsc::channel();
FetchState::new(rx) // FetchState::new(rx)
} // }
fn match_state(matches_info: Option<MatchStateInfo>) -> MatchState { // fn match_state(matches_info: Option<MatchStateInfo>) -> MatchState {
MatchState::new(matches_info, fetch_state()) // MatchState::new(matches_info, fetch_state())
} // }
#[test] // #[test]
fn create_empty() { // fn create_empty() {
let matches = AppMachine::match_state(inner(music_hoard(vec![])), match_state(None)); // let matches = AppMachine::match_state(inner(music_hoard(vec![])), match_state(None));
let widget_state = WidgetState::default(); // let widget_state = WidgetState::default();
assert_eq!(matches.state.current, None); // assert_eq!(matches.state.current, None);
assert_eq!(matches.state.state, widget_state); // assert_eq!(matches.state.state, widget_state);
let mut app: App = matches.into(); // let mut app: App = matches.into();
let public = app.get(); // let public = app.get();
let public_matches = public.state.unwrap_match(); // let public_matches = public.state.unwrap_match();
assert_eq!(public_matches.info, None); // assert_eq!(public_matches.info, None);
assert_eq!(public_matches.state, &widget_state); // assert_eq!(public_matches.state, &widget_state);
} // }
#[test] // #[test]
fn create_nonempty() { // fn create_nonempty() {
let mut album_match = album_match(); // let mut album_match = album_match();
let matches = AppMachine::match_state( // let matches = AppMachine::match_state(
inner(music_hoard(vec![])), // inner(music_hoard(vec![])),
match_state(Some(album_match.clone())), // match_state(Some(album_match.clone())),
); // );
album_match.push_cannot_have_mbid(); // album_match.push_cannot_have_mbid();
album_match.push_manual_input_mbid(); // album_match.push_manual_input_mbid();
let mut widget_state = WidgetState::default(); // let mut widget_state = WidgetState::default();
widget_state.list.select(Some(0)); // widget_state.list.select(Some(0));
assert_eq!(matches.state.current.as_ref(), Some(&album_match)); // assert_eq!(matches.state.current.as_ref(), Some(&album_match));
assert_eq!(matches.state.state, widget_state); // assert_eq!(matches.state.state, widget_state);
let mut app: App = matches.into(); // let mut app: App = matches.into();
let public = app.get(); // let public = app.get();
let public_matches = public.state.unwrap_match(); // let public_matches = public.state.unwrap_match();
assert_eq!(public_matches.info, Some(&album_match)); // assert_eq!(public_matches.info, Some(&album_match));
assert_eq!(public_matches.state, &widget_state); // assert_eq!(public_matches.state, &widget_state);
} // }
fn match_state_flow(mut matches_info: MatchStateInfo) { // fn match_state_flow(mut matches_info: MatchStateInfo) {
// tx must exist for rx to return Empty rather than Disconnected. // // tx must exist for rx to return Empty rather than Disconnected.
#[allow(unused_variables)] // #[allow(unused_variables)]
let (tx, rx) = mpsc::channel(); // let (tx, rx) = mpsc::channel();
let app_matches = MatchState::new(Some(matches_info.clone()), FetchState::new(rx)); // let app_matches = MatchState::new(Some(matches_info.clone()), FetchState::new(rx));
let matches = AppMachine::match_state(inner(music_hoard(vec![])), app_matches); // let matches = AppMachine::match_state(inner(music_hoard(vec![])), app_matches);
matches_info.push_cannot_have_mbid(); // matches_info.push_cannot_have_mbid();
matches_info.push_manual_input_mbid(); // matches_info.push_manual_input_mbid();
let mut widget_state = WidgetState::default(); // let mut widget_state = WidgetState::default();
widget_state.list.select(Some(0)); // widget_state.list.select(Some(0));
assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); // assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
assert_eq!(matches.state.state, widget_state); // assert_eq!(matches.state.state, widget_state);
let matches = matches.prev_match().unwrap_match(); // let matches = matches.prev_match().unwrap_match();
assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); // assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
assert_eq!(matches.state.state.list.selected(), Some(0)); // assert_eq!(matches.state.state.list.selected(), Some(0));
let matches = matches.next_match().unwrap_match(); // let matches = matches.next_match().unwrap_match();
assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); // assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
assert_eq!(matches.state.state.list.selected(), Some(1)); // assert_eq!(matches.state.state.list.selected(), Some(1));
// Next is CannotHaveMBID // // Next is CannotHaveMBID
let matches = matches.next_match().unwrap_match(); // let matches = matches.next_match().unwrap_match();
assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); // assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
assert_eq!(matches.state.state.list.selected(), Some(2)); // assert_eq!(matches.state.state.list.selected(), Some(2));
// Next is ManualInputMbid // // Next is ManualInputMbid
let matches = matches.next_match().unwrap_match(); // let matches = matches.next_match().unwrap_match();
assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); // assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
assert_eq!(matches.state.state.list.selected(), Some(3)); // assert_eq!(matches.state.state.list.selected(), Some(3));
let matches = matches.next_match().unwrap_match(); // let matches = matches.next_match().unwrap_match();
assert_eq!(matches.state.current.as_ref(), Some(&matches_info)); // assert_eq!(matches.state.current.as_ref(), Some(&matches_info));
assert_eq!(matches.state.state.list.selected(), Some(3)); // assert_eq!(matches.state.state.list.selected(), Some(3));
// Go prev_match first as selecting on manual input does not go back to fetch. // // Go prev_match first as selecting on manual input does not go back to fetch.
let matches = matches.prev_match().unwrap_match(); // let matches = matches.prev_match().unwrap_match();
matches.select().unwrap_fetch(); // matches.select().unwrap_fetch();
} // }
#[test] // #[test]
fn artist_matches_flow() { // fn artist_matches_flow() {
match_state_flow(artist_match()); // match_state_flow(artist_match());
} // }
#[test] // #[test]
fn album_matches_flow() { // fn album_matches_flow() {
match_state_flow(album_match()); // match_state_flow(album_match());
} // }
#[test] // #[test]
fn abort() { // fn abort() {
let mut album_match = album_match(); // let mut album_match = album_match();
let matches = AppMachine::match_state( // let matches = AppMachine::match_state(
inner(music_hoard(vec![])), // inner(music_hoard(vec![])),
match_state(Some(album_match.clone())), // match_state(Some(album_match.clone())),
); // );
album_match.push_cannot_have_mbid(); // album_match.push_cannot_have_mbid();
album_match.push_manual_input_mbid(); // album_match.push_manual_input_mbid();
let mut widget_state = WidgetState::default(); // let mut widget_state = WidgetState::default();
widget_state.list.select(Some(0)); // widget_state.list.select(Some(0));
assert_eq!(matches.state.current.as_ref(), Some(&album_match)); // assert_eq!(matches.state.current.as_ref(), Some(&album_match));
assert_eq!(matches.state.state, widget_state); // assert_eq!(matches.state.state, widget_state);
matches.abort().unwrap_browse(); // matches.abort().unwrap_browse();
} // }
#[test] // #[test]
fn select_empty() { // fn select_empty() {
// Note that what really matters in this test is actually that the transmit channel has // // Note that what really matters in this test is actually that the transmit channel has
// disconnected and so the receive within FetchState concludes there are no more matches. // // disconnected and so the receive within FetchState concludes there are no more matches.
let matches = AppMachine::match_state(inner(music_hoard(vec![])), match_state(None)); // let matches = AppMachine::match_state(inner(music_hoard(vec![])), match_state(None));
matches.select().unwrap_browse(); // matches.select().unwrap_browse();
} // }
#[test] // #[test]
fn select_manual_input() { // fn select_manual_input() {
let matches = // let matches =
AppMachine::match_state(inner(music_hoard(vec![])), match_state(Some(album_match()))); // AppMachine::match_state(inner(music_hoard(vec![])), match_state(Some(album_match())));
// album_match has two matches which means that the fourth option should be manual input. // // album_match has two matches which means that the fourth option should be manual input.
let matches = matches.next_match().unwrap_match(); // let matches = matches.next_match().unwrap_match();
let matches = matches.next_match().unwrap_match(); // let matches = matches.next_match().unwrap_match();
let matches = matches.next_match().unwrap_match(); // let matches = matches.next_match().unwrap_match();
let matches = matches.next_match().unwrap_match(); // let matches = matches.next_match().unwrap_match();
let app = matches.select(); // let app = matches.select();
let input = app.mode().unwrap_input(); // let input = app.mode().unwrap_input();
input.confirm().unwrap_match(); // input.confirm().unwrap_match();
} // }
} // }

View File

@ -8,15 +8,14 @@ mod match_state;
mod reload_state; mod reload_state;
mod search_state; mod search_state;
use std::sync::{Arc, Mutex}; use std::sync::mpsc;
use crate::tui::{ use crate::tui::{
app::{ app::{
selection::Selection, AppMode, AppPublic, AppPublicInner, AppPublicState, AppState, IApp, selection::Selection, AppMode, AppPublic, AppPublicInner, AppPublicState, AppState, IApp,
IAppAccess, IAppBase, IAppState, IAppAccess, IAppBase, IAppState,
}, },
event::EventSender, lib::{external::musicbrainz::daemon::Job, IMusicHoard},
lib::{interface::musicbrainz::IMusicBrainz, IMusicHoard},
}; };
use browse_state::BrowseState; use browse_state::BrowseState;
@ -49,9 +48,8 @@ pub struct AppMachine<STATE> {
pub struct AppInner { pub struct AppInner {
running: bool, running: bool,
music_hoard: Box<dyn IMusicHoard>, music_hoard: Box<dyn IMusicHoard>,
musicbrainz: Arc<Mutex<dyn IMusicBrainz + Send>>, musicbrainz: mpsc::Sender<Job>,
selection: Selection, selection: Selection,
events: EventSender,
} }
macro_rules! app_field_ref { macro_rules! app_field_ref {
@ -85,13 +83,12 @@ macro_rules! app_field_mut {
} }
impl App { impl App {
pub fn new<MH: IMusicHoard + 'static, MB: IMusicBrainz + Send + 'static>( pub fn new<MH: IMusicHoard + 'static>(
mut music_hoard: MH, mut music_hoard: MH,
musicbrainz: MB, musicbrainz: mpsc::Sender<Job>,
events: EventSender,
) -> Self { ) -> Self {
let init_result = Self::init(&mut music_hoard); let init_result = Self::init(&mut music_hoard);
let inner = AppInner::new(music_hoard, musicbrainz, events); let inner = AppInner::new(music_hoard, musicbrainz);
match init_result { match init_result {
Ok(()) => AppMachine::browse_state(inner).into(), Ok(()) => AppMachine::browse_state(inner).into(),
Err(err) => AppMachine::critical_state(inner, err.to_string()).into(), Err(err) => AppMachine::critical_state(inner, err.to_string()).into(),
@ -174,18 +171,13 @@ impl IAppAccess for App {
} }
impl AppInner { impl AppInner {
pub fn new<MH: IMusicHoard + 'static, MB: IMusicBrainz + Send + 'static>( pub fn new<MH: IMusicHoard + 'static>(music_hoard: MH, musicbrainz: mpsc::Sender<Job>) -> Self {
music_hoard: MH,
musicbrainz: MB,
events: EventSender,
) -> Self {
let selection = Selection::new(music_hoard.get_collection()); let selection = Selection::new(music_hoard.get_collection());
AppInner { AppInner {
running: true, running: true,
music_hoard: Box::new(music_hoard), music_hoard: Box::new(music_hoard),
musicbrainz: Arc::new(Mutex::new(musicbrainz)), musicbrainz,
selection, selection,
events,
} }
} }
} }
@ -222,378 +214,378 @@ where
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use std::sync::mpsc; // use std::sync::mpsc;
use musichoard::collection::Collection; // use musichoard::collection::Collection;
use crate::tui::{ // use crate::tui::{
app::{AppState, IApp, IAppInput, IAppInteractBrowse}, // app::{AppState, IApp, IAppInput, IAppInteractBrowse},
lib::{interface::musicbrainz::MockIMusicBrainz, MockIMusicHoard}, // lib::{interface::musicbrainz::api::MockIMusicBrainz, MockIMusicHoard},
EventChannel, // EventChannel,
}; // };
use super::*; // use super::*;
impl<StateMode, InputMode> AppMode<StateMode, InputMode> { // impl<StateMode, InputMode> AppMode<StateMode, InputMode> {
fn unwrap_state(self) -> StateMode { // fn unwrap_state(self) -> StateMode {
match self { // match self {
AppMode::State(state) => state, // AppMode::State(state) => state,
_ => panic!(), // _ => panic!(),
} // }
} // }
pub fn unwrap_input(self) -> InputMode { // pub fn unwrap_input(self) -> InputMode {
match self { // match self {
AppMode::Input(input) => input, // AppMode::Input(input) => input,
_ => panic!(), // _ => panic!(),
} // }
} // }
} // }
impl< // impl<
BrowseState, // BrowseState,
InfoState, // InfoState,
ReloadState, // ReloadState,
SearchState, // SearchState,
FetchState, // FetchState,
MatchState, // MatchState,
ErrorState, // ErrorState,
CriticalState, // CriticalState,
> // >
AppState< // AppState<
BrowseState, // BrowseState,
InfoState, // InfoState,
ReloadState, // ReloadState,
SearchState, // SearchState,
FetchState, // FetchState,
MatchState, // MatchState,
ErrorState, // ErrorState,
CriticalState, // CriticalState,
> // >
{ // {
pub fn unwrap_browse(self) -> BrowseState { // pub fn unwrap_browse(self) -> BrowseState {
match self { // match self {
AppState::Browse(browse) => browse, // AppState::Browse(browse) => browse,
_ => panic!(), // _ => panic!(),
} // }
} // }
pub fn unwrap_info(self) -> InfoState { // pub fn unwrap_info(self) -> InfoState {
match self { // match self {
AppState::Info(info) => info, // AppState::Info(info) => info,
_ => panic!(), // _ => panic!(),
} // }
} // }
pub fn unwrap_reload(self) -> ReloadState { // pub fn unwrap_reload(self) -> ReloadState {
match self { // match self {
AppState::Reload(reload) => reload, // AppState::Reload(reload) => reload,
_ => panic!(), // _ => panic!(),
} // }
} // }
pub fn unwrap_search(self) -> SearchState { // pub fn unwrap_search(self) -> SearchState {
match self { // match self {
AppState::Search(search) => search, // AppState::Search(search) => search,
_ => panic!(), // _ => panic!(),
} // }
} // }
pub fn unwrap_fetch(self) -> FetchState { // pub fn unwrap_fetch(self) -> FetchState {
match self { // match self {
AppState::Fetch(fetch) => fetch, // AppState::Fetch(fetch) => fetch,
_ => panic!(), // _ => panic!(),
} // }
} // }
pub fn unwrap_match(self) -> MatchState { // pub fn unwrap_match(self) -> MatchState {
match self { // match self {
AppState::Match(matches) => matches, // AppState::Match(matches) => matches,
_ => panic!(), // _ => panic!(),
} // }
} // }
pub fn unwrap_error(self) -> ErrorState { // pub fn unwrap_error(self) -> ErrorState {
match self { // match self {
AppState::Error(error) => error, // AppState::Error(error) => error,
_ => panic!(), // _ => panic!(),
} // }
} // }
pub fn unwrap_critical(self) -> CriticalState { // pub fn unwrap_critical(self) -> CriticalState {
match self { // match self {
AppState::Critical(critical) => critical, // AppState::Critical(critical) => critical,
_ => panic!(), // _ => panic!(),
} // }
} // }
} // }
pub fn music_hoard(collection: Collection) -> MockIMusicHoard { // pub fn music_hoard(collection: Collection) -> MockIMusicHoard {
let mut music_hoard = MockIMusicHoard::new(); // let mut music_hoard = MockIMusicHoard::new();
music_hoard.expect_get_collection().return_const(collection); // music_hoard.expect_get_collection().return_const(collection);
music_hoard // music_hoard
} // }
pub fn music_hoard_init(collection: Collection) -> MockIMusicHoard { // pub fn music_hoard_init(collection: Collection) -> MockIMusicHoard {
let mut music_hoard = music_hoard(collection); // let mut music_hoard = music_hoard(collection);
music_hoard // music_hoard
.expect_rescan_library() // .expect_rescan_library()
.times(1) // .times(1)
.return_once(|| Ok(())); // .return_once(|| Ok(()));
music_hoard // music_hoard
} // }
pub fn mb_api() -> MockIMusicBrainz { // pub fn mb_api() -> MockIMusicBrainz {
MockIMusicBrainz::new() // MockIMusicBrainz::new()
} // }
pub fn events() -> EventSender { // pub fn events() -> EventSender {
EventChannel::new().sender() // EventChannel::new().sender()
} // }
pub fn inner(music_hoard: MockIMusicHoard) -> AppInner { // pub fn inner(music_hoard: MockIMusicHoard) -> AppInner {
AppInner::new(music_hoard, mb_api(), events()) // AppInner::new(music_hoard, mb_api(), events())
} // }
pub fn inner_with_mb(music_hoard: MockIMusicHoard, mb_api: MockIMusicBrainz) -> AppInner { // pub fn inner_with_mb(music_hoard: MockIMusicHoard, mb_api: MockIMusicBrainz) -> AppInner {
AppInner::new(music_hoard, mb_api, events()) // AppInner::new(music_hoard, mb_api, events())
} // }
#[test] // #[test]
fn input_mode() { // fn input_mode() {
let app = App::new(music_hoard_init(vec![]), mb_api(), events()); // let app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running()); // assert!(app.is_running());
let mode = app.mode(); // let mode = app.mode();
assert!(matches!(mode, AppMode::State(_))); // assert!(matches!(mode, AppMode::State(_)));
let state = mode.unwrap_state(); // let state = mode.unwrap_state();
assert!(matches!(state, AppState::Browse(_))); // assert!(matches!(state, AppState::Browse(_)));
let mut app = state; // let mut app = state;
app.input_mut().replace(Input::default()); // app.input_mut().replace(Input::default());
let public = app.get(); // let public = app.get();
assert!(public.input.is_some()); // assert!(public.input.is_some());
let mode = app.mode(); // let mode = app.mode();
assert!(matches!(mode, AppMode::Input(_))); // assert!(matches!(mode, AppMode::Input(_)));
let mut app = mode.unwrap_input().cancel(); // let mut app = mode.unwrap_input().cancel();
assert!(matches!(app, AppState::Browse(_))); // assert!(matches!(app, AppState::Browse(_)));
let public = app.get(); // let public = app.get();
assert!(public.input.is_none()); // assert!(public.input.is_none());
} // }
#[test] // #[test]
fn state_browse() { // fn state_browse() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); // let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running()); // assert!(app.is_running());
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Browse(_))); // assert!(matches!(state, AppState::Browse(_)));
app = state; // app = state;
app = app.no_op(); // app = app.no_op();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Browse(_))); // assert!(matches!(state, AppState::Browse(_)));
app = state; // app = state;
let public = app.get(); // let public = app.get();
assert!(matches!(public.state, AppState::Browse(_))); // assert!(matches!(public.state, AppState::Browse(_)));
let app = app.force_quit(); // let app = app.force_quit();
assert!(!app.is_running()); // assert!(!app.is_running());
} // }
#[test] // #[test]
fn state_info() { // fn state_info() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); // let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running()); // assert!(app.is_running());
app = app.unwrap_browse().show_info_overlay(); // app = app.unwrap_browse().show_info_overlay();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Info(_))); // assert!(matches!(state, AppState::Info(_)));
app = state; // app = state;
app = app.no_op(); // app = app.no_op();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Info(_))); // assert!(matches!(state, AppState::Info(_)));
app = state; // app = state;
let public = app.get(); // let public = app.get();
assert!(matches!(public.state, AppState::Info(_))); // assert!(matches!(public.state, AppState::Info(_)));
let app = app.force_quit(); // let app = app.force_quit();
assert!(!app.is_running()); // assert!(!app.is_running());
} // }
#[test] // #[test]
fn state_reload() { // fn state_reload() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); // let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running()); // assert!(app.is_running());
app = app.unwrap_browse().show_reload_menu(); // app = app.unwrap_browse().show_reload_menu();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Reload(_))); // assert!(matches!(state, AppState::Reload(_)));
app = state; // app = state;
app = app.no_op(); // app = app.no_op();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Reload(_))); // assert!(matches!(state, AppState::Reload(_)));
app = state; // app = state;
let public = app.get(); // let public = app.get();
assert!(matches!(public.state, AppState::Reload(_))); // assert!(matches!(public.state, AppState::Reload(_)));
let app = app.force_quit(); // let app = app.force_quit();
assert!(!app.is_running()); // assert!(!app.is_running());
} // }
#[test] // #[test]
fn state_search() { // fn state_search() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); // let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running()); // assert!(app.is_running());
app = app.unwrap_browse().begin_search(); // app = app.unwrap_browse().begin_search();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Search(_))); // assert!(matches!(state, AppState::Search(_)));
app = state; // app = state;
app = app.no_op(); // app = app.no_op();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Search(_))); // assert!(matches!(state, AppState::Search(_)));
app = state; // app = state;
let public = app.get(); // let public = app.get();
assert!(matches!(public.state, AppState::Search(""))); // assert!(matches!(public.state, AppState::Search("")));
let app = app.force_quit(); // let app = app.force_quit();
assert!(!app.is_running()); // assert!(!app.is_running());
} // }
#[test] // #[test]
fn state_fetch() { // fn state_fetch() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); // let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running()); // assert!(app.is_running());
let (_, rx) = mpsc::channel(); // let (_, rx) = mpsc::channel();
let inner = app.unwrap_browse().inner; // let inner = app.unwrap_browse().inner;
let state = FetchState::new(rx); // let state = FetchState::new(rx);
app = AppMachine::new(inner, state).into(); // app = AppMachine::new(inner, state).into();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Fetch(_))); // assert!(matches!(state, AppState::Fetch(_)));
app = state; // app = state;
app = app.no_op(); // app = app.no_op();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Fetch(_))); // assert!(matches!(state, AppState::Fetch(_)));
app = state; // app = state;
let public = app.get(); // let public = app.get();
assert!(matches!(public.state, AppState::Fetch(_))); // assert!(matches!(public.state, AppState::Fetch(_)));
let app = app.force_quit(); // let app = app.force_quit();
assert!(!app.is_running()); // assert!(!app.is_running());
} // }
#[test] // #[test]
fn state_match() { // fn state_match() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); // let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running()); // assert!(app.is_running());
let (_, rx) = mpsc::channel(); // let (_, rx) = mpsc::channel();
let fetch = FetchState::new(rx); // let fetch = FetchState::new(rx);
app = // app =
AppMachine::match_state(app.unwrap_browse().inner, MatchState::new(None, fetch)).into(); // AppMachine::match_state(app.unwrap_browse().inner, MatchState::new(None, fetch)).into();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Match(_))); // assert!(matches!(state, AppState::Match(_)));
app = state; // app = state;
app = app.no_op(); // app = app.no_op();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Match(_))); // assert!(matches!(state, AppState::Match(_)));
app = state; // app = state;
let public = app.get(); // let public = app.get();
assert!(matches!(public.state, AppState::Match(_))); // assert!(matches!(public.state, AppState::Match(_)));
let app = app.force_quit(); // let app = app.force_quit();
assert!(!app.is_running()); // assert!(!app.is_running());
} // }
#[test] // #[test]
fn state_error() { // fn state_error() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); // let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running()); // assert!(app.is_running());
app = AppMachine::error_state(app.unwrap_browse().inner, "get rekt").into(); // app = AppMachine::error_state(app.unwrap_browse().inner, "get rekt").into();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Error(_))); // assert!(matches!(state, AppState::Error(_)));
app = state; // app = state;
app = app.no_op(); // app = app.no_op();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Error(_))); // assert!(matches!(state, AppState::Error(_)));
app = state; // app = state;
let public = app.get(); // let public = app.get();
assert!(matches!(public.state, AppState::Error("get rekt"))); // assert!(matches!(public.state, AppState::Error("get rekt")));
app = app.force_quit(); // app = app.force_quit();
assert!(!app.is_running()); // assert!(!app.is_running());
} // }
#[test] // #[test]
fn state_critical() { // fn state_critical() {
let mut app = App::new(music_hoard_init(vec![]), mb_api(), events()); // let mut app = App::new(music_hoard_init(vec![]), mb_api(), events());
assert!(app.is_running()); // assert!(app.is_running());
app = AppMachine::critical_state(app.unwrap_browse().inner, "get rekt").into(); // app = AppMachine::critical_state(app.unwrap_browse().inner, "get rekt").into();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Critical(_))); // assert!(matches!(state, AppState::Critical(_)));
app = state; // app = state;
app = app.no_op(); // app = app.no_op();
let state = app.state(); // let state = app.state();
assert!(matches!(state, AppState::Critical(_))); // assert!(matches!(state, AppState::Critical(_)));
app = state; // app = state;
let public = app.get(); // let public = app.get();
assert!(matches!(public.state, AppState::Critical("get rekt"))); // assert!(matches!(public.state, AppState::Critical("get rekt")));
app = app.force_quit(); // app = app.force_quit();
assert!(!app.is_running()); // assert!(!app.is_running());
} // }
#[test] // #[test]
fn init_error() { // fn init_error() {
let mut music_hoard = MockIMusicHoard::new(); // let mut music_hoard = MockIMusicHoard::new();
music_hoard // music_hoard
.expect_rescan_library() // .expect_rescan_library()
.times(1) // .times(1)
.return_once(|| Err(musichoard::Error::LibraryError(String::from("get rekt")))); // .return_once(|| Err(musichoard::Error::LibraryError(String::from("get rekt"))));
music_hoard.expect_get_collection().return_const(vec![]); // music_hoard.expect_get_collection().return_const(vec![]);
let app = App::new(music_hoard, mb_api(), events()); // let app = App::new(music_hoard, mb_api(), events());
assert!(app.is_running()); // assert!(app.is_running());
app.unwrap_critical(); // app.unwrap_critical();
} // }
} // }
#[cfg(nightly)] #[cfg(nightly)]
#[cfg(test)] #[cfg(test)]

View File

@ -68,58 +68,58 @@ impl IAppInteractReloadPrivate for AppMachine<ReloadState> {
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use crate::tui::app::machine::tests::{inner, music_hoard}; // use crate::tui::app::machine::tests::{inner, music_hoard};
use super::*; // use super::*;
#[test] // #[test]
fn hide_reload_menu() { // fn hide_reload_menu() {
let reload = AppMachine::reload_state(inner(music_hoard(vec![]))); // let reload = AppMachine::reload_state(inner(music_hoard(vec![])));
let app = reload.hide_reload_menu(); // let app = reload.hide_reload_menu();
app.unwrap_browse(); // app.unwrap_browse();
} // }
#[test] // #[test]
fn reload_database() { // fn reload_database() {
let mut music_hoard = music_hoard(vec![]); // let mut music_hoard = music_hoard(vec![]);
music_hoard // music_hoard
.expect_reload_database() // .expect_reload_database()
.times(1) // .times(1)
.return_once(|| Ok(())); // .return_once(|| Ok(()));
let reload = AppMachine::reload_state(inner(music_hoard)); // let reload = AppMachine::reload_state(inner(music_hoard));
let app = reload.reload_database(); // let app = reload.reload_database();
app.unwrap_browse(); // app.unwrap_browse();
} // }
#[test] // #[test]
fn reload_library() { // fn reload_library() {
let mut music_hoard = music_hoard(vec![]); // let mut music_hoard = music_hoard(vec![]);
music_hoard // music_hoard
.expect_rescan_library() // .expect_rescan_library()
.times(1) // .times(1)
.return_once(|| Ok(())); // .return_once(|| Ok(()));
let reload = AppMachine::reload_state(inner(music_hoard)); // let reload = AppMachine::reload_state(inner(music_hoard));
let app = reload.reload_library(); // let app = reload.reload_library();
app.unwrap_browse(); // app.unwrap_browse();
} // }
#[test] // #[test]
fn reload_error() { // fn reload_error() {
let mut music_hoard = music_hoard(vec![]); // let mut music_hoard = music_hoard(vec![]);
music_hoard // music_hoard
.expect_reload_database() // .expect_reload_database()
.times(1) // .times(1)
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); // .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
let reload = AppMachine::reload_state(inner(music_hoard)); // let reload = AppMachine::reload_state(inner(music_hoard));
let app = reload.reload_database(); // let app = reload.reload_database();
app.unwrap_error(); // app.unwrap_error();
} // }
} // }

View File

@ -229,343 +229,343 @@ impl IAppInteractSearchPrivate for AppMachine<SearchState> {
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use ratatui::widgets::ListState; // use ratatui::widgets::ListState;
use crate::tui::{ // use crate::tui::{
app::machine::tests::{inner, music_hoard}, // app::machine::tests::{inner, music_hoard},
testmod::COLLECTION, // testmod::COLLECTION,
}; // };
use super::*; // use super::*;
fn orig(index: Option<usize>) -> ListSelection { // fn orig(index: Option<usize>) -> ListSelection {
let mut artist = ListState::default(); // let mut artist = ListState::default();
artist.select(index); // artist.select(index);
ListSelection { // ListSelection {
artist, // artist,
album: ListState::default(), // album: ListState::default(),
track: ListState::default(), // track: ListState::default(),
} // }
} // }
#[test] // #[test]
fn artist_incremental_search() { // fn artist_incremental_search() {
// Empty collection. // // Empty collection.
let mut search = AppMachine::search_state(inner(music_hoard(vec![])), orig(None)); // let mut search = AppMachine::search_state(inner(music_hoard(vec![])), orig(None));
assert_eq!(search.inner.selection.selected(), None); // assert_eq!(search.inner.selection.selected(), None);
search.state.string = String::from("album_artist 'a'"); // search.state.string = String::from("album_artist 'a'");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), None); // assert_eq!(search.inner.selection.selected(), None);
// Basic test, first element. // // Basic test, first element.
let mut search = // let mut search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from(""); // search.state.string = String::from("");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("album_artist "); // search.state.string = String::from("album_artist ");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("album_artist 'a'"); // search.state.string = String::from("album_artist 'a'");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
// Basic test, non-first element. // // Basic test, non-first element.
let mut search = // let mut search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("album_artist "); // search.state.string = String::from("album_artist ");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("album_artist 'c'"); // search.state.string = String::from("album_artist 'c'");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(2)); // assert_eq!(search.inner.selection.selected(), Some(2));
// Non-lowercase. // // Non-lowercase.
let mut search = // let mut search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("Album_Artist "); // search.state.string = String::from("Album_Artist ");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("Album_Artist 'C'"); // search.state.string = String::from("Album_Artist 'C'");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(2)); // assert_eq!(search.inner.selection.selected(), Some(2));
// Non-ascii. // // Non-ascii.
let mut search = // let mut search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("album_artist "); // search.state.string = String::from("album_artist ");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("album_artist c"); // search.state.string = String::from("album_artist c");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(2)); // assert_eq!(search.inner.selection.selected(), Some(2));
// Non-lowercase, non-ascii. // // Non-lowercase, non-ascii.
let mut search = // let mut search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("Album_Artist "); // search.state.string = String::from("Album_Artist ");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("Album_Artist C"); // search.state.string = String::from("Album_Artist C");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(2)); // assert_eq!(search.inner.selection.selected(), Some(2));
// Stop at name, not sort name. // // Stop at name, not sort name.
let mut search = // let mut search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("the "); // search.state.string = String::from("the ");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(2)); // assert_eq!(search.inner.selection.selected(), Some(2));
search.state.string = String::from("the album_artist 'c'"); // search.state.string = String::from("the album_artist 'c'");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(2)); // assert_eq!(search.inner.selection.selected(), Some(2));
// Search next with common prefix. // // Search next with common prefix.
let mut search = // let mut search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.state.string = String::from("album_artist"); // search.state.string = String::from("album_artist");
search.incremental_search(false); // search.incremental_search(false);
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
search.incremental_search(true); // search.incremental_search(true);
assert_eq!(search.inner.selection.selected(), Some(1)); // assert_eq!(search.inner.selection.selected(), Some(1));
search.incremental_search(true); // search.incremental_search(true);
assert_eq!(search.inner.selection.selected(), Some(2)); // assert_eq!(search.inner.selection.selected(), Some(2));
search.incremental_search(true); // search.incremental_search(true);
assert_eq!(search.inner.selection.selected(), Some(3)); // assert_eq!(search.inner.selection.selected(), Some(3));
search.incremental_search(true); // search.incremental_search(true);
assert_eq!(search.inner.selection.selected(), Some(3)); // assert_eq!(search.inner.selection.selected(), Some(3));
} // }
#[test] // #[test]
fn album_incremental_search() { // fn album_incremental_search() {
let mut search = // let mut search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
search.inner.selection.increment_category(); // search.inner.selection.increment_category();
assert_eq!(search.inner.selection.category(), Category::Album); // assert_eq!(search.inner.selection.category(), Category::Album);
let sel = &search.inner.selection; // let sel = &search.inner.selection;
assert_eq!(sel.selected(), Some(0)); // assert_eq!(sel.selected(), Some(0));
search.state.string = String::from("album_title "); // search.state.string = String::from("album_title ");
search.incremental_search(false); // search.incremental_search(false);
let sel = &search.inner.selection; // let sel = &search.inner.selection;
assert_eq!(sel.selected(), Some(0)); // assert_eq!(sel.selected(), Some(0));
let search = search.append_character('a').unwrap_search(); // let search = search.append_character('a').unwrap_search();
let search = search.append_character('.').unwrap_search(); // let search = search.append_character('.').unwrap_search();
let search = search.append_character('b').unwrap_search(); // let search = search.append_character('b').unwrap_search();
let sel = &search.inner.selection; // let sel = &search.inner.selection;
assert_eq!(sel.selected(), Some(1)); // assert_eq!(sel.selected(), Some(1));
} // }
#[test] // #[test]
fn track_incremental_search() { // fn track_incremental_search() {
let mut search = // let mut search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(1)));
search.inner.selection.increment_category(); // search.inner.selection.increment_category();
search.inner.selection.increment_category(); // search.inner.selection.increment_category();
assert_eq!(search.inner.selection.category(), Category::Track); // assert_eq!(search.inner.selection.category(), Category::Track);
let sel = &search.inner.selection; // let sel = &search.inner.selection;
assert_eq!(sel.selected(), Some(0)); // assert_eq!(sel.selected(), Some(0));
search.state.string = String::from("track "); // search.state.string = String::from("track ");
search.incremental_search(false); // search.incremental_search(false);
let sel = &search.inner.selection; // let sel = &search.inner.selection;
assert_eq!(sel.selected(), Some(0)); // assert_eq!(sel.selected(), Some(0));
let search = search.append_character('a').unwrap_search(); // let search = search.append_character('a').unwrap_search();
let search = search.append_character('.').unwrap_search(); // let search = search.append_character('.').unwrap_search();
let search = search.append_character('a').unwrap_search(); // let search = search.append_character('a').unwrap_search();
let search = search.append_character('.').unwrap_search(); // let search = search.append_character('.').unwrap_search();
let search = search.append_character('2').unwrap_search(); // let search = search.append_character('2').unwrap_search();
let sel = &search.inner.selection; // let sel = &search.inner.selection;
assert_eq!(sel.selected(), Some(1)); // assert_eq!(sel.selected(), Some(1));
} // }
#[test] // #[test]
fn search() { // fn search() {
let search = // let search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2)));
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
let search = search.append_character('a').unwrap_search(); // let search = search.append_character('a').unwrap_search();
let search = search.append_character('l').unwrap_search(); // let search = search.append_character('l').unwrap_search();
let search = search.append_character('b').unwrap_search(); // let search = search.append_character('b').unwrap_search();
let search = search.append_character('u').unwrap_search(); // let search = search.append_character('u').unwrap_search();
let search = search.append_character('m').unwrap_search(); // let search = search.append_character('m').unwrap_search();
let search = search.append_character('_').unwrap_search(); // let search = search.append_character('_').unwrap_search();
let search = search.append_character('a').unwrap_search(); // let search = search.append_character('a').unwrap_search();
let search = search.append_character('r').unwrap_search(); // let search = search.append_character('r').unwrap_search();
let search = search.append_character('t').unwrap_search(); // let search = search.append_character('t').unwrap_search();
let search = search.append_character('i').unwrap_search(); // let search = search.append_character('i').unwrap_search();
let search = search.append_character('s').unwrap_search(); // let search = search.append_character('s').unwrap_search();
let search = search.append_character('t').unwrap_search(); // let search = search.append_character('t').unwrap_search();
let search = search.append_character(' ').unwrap_search(); // let search = search.append_character(' ').unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
let search = search.append_character('\'').unwrap_search(); // let search = search.append_character('\'').unwrap_search();
let search = search.append_character('c').unwrap_search(); // let search = search.append_character('c').unwrap_search();
let search = search.append_character('\'').unwrap_search(); // let search = search.append_character('\'').unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(2)); // assert_eq!(search.inner.selection.selected(), Some(2));
let search = search.step_back().unwrap_search(); // let search = search.step_back().unwrap_search();
let search = search.step_back().unwrap_search(); // let search = search.step_back().unwrap_search();
let search = search.step_back().unwrap_search(); // let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
let search = search.append_character('\'').unwrap_search(); // let search = search.append_character('\'').unwrap_search();
let search = search.append_character('b').unwrap_search(); // let search = search.append_character('b').unwrap_search();
let search = search.append_character('\'').unwrap_search(); // let search = search.append_character('\'').unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(1)); // assert_eq!(search.inner.selection.selected(), Some(1));
let app = search.finish_search(); // let app = search.finish_search();
let browse = app.unwrap_browse(); // let browse = app.unwrap_browse();
assert_eq!(browse.inner.selection.selected(), Some(1)); // assert_eq!(browse.inner.selection.selected(), Some(1));
} // }
#[test] // #[test]
fn search_next_step_back() { // fn search_next_step_back() {
let search = // let search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2)));
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
let search = search.step_back().unwrap_search(); // let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
let search = search.append_character('a').unwrap_search(); // let search = search.append_character('a').unwrap_search();
let search = search.search_next().unwrap_search(); // let search = search.search_next().unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(1)); // assert_eq!(search.inner.selection.selected(), Some(1));
let search = search.search_next().unwrap_search(); // let search = search.search_next().unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(2)); // assert_eq!(search.inner.selection.selected(), Some(2));
let search = search.search_next().unwrap_search(); // let search = search.search_next().unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(3)); // assert_eq!(search.inner.selection.selected(), Some(3));
let search = search.search_next().unwrap_search(); // let search = search.search_next().unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(3)); // assert_eq!(search.inner.selection.selected(), Some(3));
let search = search.step_back().unwrap_search(); // let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(3)); // assert_eq!(search.inner.selection.selected(), Some(3));
let search = search.step_back().unwrap_search(); // let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(2)); // assert_eq!(search.inner.selection.selected(), Some(2));
let search = search.step_back().unwrap_search(); // let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(1)); // assert_eq!(search.inner.selection.selected(), Some(1));
let search = search.step_back().unwrap_search(); // let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
let search = search.step_back().unwrap_search(); // let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
} // }
#[test] // #[test]
fn cancel_search() { // fn cancel_search() {
let search = // let search =
AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2))); // AppMachine::search_state(inner(music_hoard(COLLECTION.to_owned())), orig(Some(2)));
assert_eq!(search.inner.selection.selected(), Some(0)); // assert_eq!(search.inner.selection.selected(), Some(0));
let search = search.append_character('a').unwrap_search(); // let search = search.append_character('a').unwrap_search();
let search = search.append_character('l').unwrap_search(); // let search = search.append_character('l').unwrap_search();
let search = search.append_character('b').unwrap_search(); // let search = search.append_character('b').unwrap_search();
let search = search.append_character('u').unwrap_search(); // let search = search.append_character('u').unwrap_search();
let search = search.append_character('m').unwrap_search(); // let search = search.append_character('m').unwrap_search();
let search = search.append_character('_').unwrap_search(); // let search = search.append_character('_').unwrap_search();
let search = search.append_character('a').unwrap_search(); // let search = search.append_character('a').unwrap_search();
let search = search.append_character('r').unwrap_search(); // let search = search.append_character('r').unwrap_search();
let search = search.append_character('t').unwrap_search(); // let search = search.append_character('t').unwrap_search();
let search = search.append_character('i').unwrap_search(); // let search = search.append_character('i').unwrap_search();
let search = search.append_character('s').unwrap_search(); // let search = search.append_character('s').unwrap_search();
let search = search.append_character('t').unwrap_search(); // let search = search.append_character('t').unwrap_search();
let search = search.append_character(' ').unwrap_search(); // let search = search.append_character(' ').unwrap_search();
let search = search.append_character('\'').unwrap_search(); // let search = search.append_character('\'').unwrap_search();
let search = search.append_character('b').unwrap_search(); // let search = search.append_character('b').unwrap_search();
let search = search.append_character('\'').unwrap_search(); // let search = search.append_character('\'').unwrap_search();
assert_eq!(search.inner.selection.selected(), Some(1)); // assert_eq!(search.inner.selection.selected(), Some(1));
let browse = search.cancel_search().unwrap_browse(); // let browse = search.cancel_search().unwrap_browse();
assert_eq!(browse.inner.selection.selected(), Some(2)); // assert_eq!(browse.inner.selection.selected(), Some(2));
} // }
#[test] // #[test]
fn empty_search() { // fn empty_search() {
let search = AppMachine::search_state(inner(music_hoard(vec![])), orig(None)); // let search = AppMachine::search_state(inner(music_hoard(vec![])), orig(None));
assert_eq!(search.inner.selection.selected(), None); // assert_eq!(search.inner.selection.selected(), None);
let search = search.append_character('a').unwrap_search(); // let search = search.append_character('a').unwrap_search();
assert_eq!(search.inner.selection.selected(), None); // assert_eq!(search.inner.selection.selected(), None);
let search = search.search_next().unwrap_search(); // let search = search.search_next().unwrap_search();
assert_eq!(search.inner.selection.selected(), None); // assert_eq!(search.inner.selection.selected(), None);
let search = search.step_back().unwrap_search(); // let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.selected(), None); // assert_eq!(search.inner.selection.selected(), None);
let search = search.step_back().unwrap_search(); // let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.selected(), None); // assert_eq!(search.inner.selection.selected(), None);
let browse = search.cancel_search().unwrap_browse(); // let browse = search.cancel_search().unwrap_browse();
assert_eq!(browse.inner.selection.selected(), None); // assert_eq!(browse.inner.selection.selected(), None);
} // }
} // }
#[cfg(nightly)] // #[cfg(nightly)]
#[cfg(test)] // #[cfg(test)]
mod benches { // mod benches {
// The purpose of these benches was to evaluate the benefit of AhoCorasick over std solutions. // // The purpose of these benches was to evaluate the benefit of AhoCorasick over std solutions.
use test::Bencher; // use test::Bencher;
use crate::tui::{app::machine::benchmod::ARTISTS, lib::MockIMusicHoard}; // use crate::tui::{app::machine::benchmod::ARTISTS, lib::MockIMusicHoard};
use super::*; // use super::*;
type Search = AppMachine<MockIMusicHoard, SearchState>; // type Search = AppMachine<MockIMusicHoard, SearchState>;
#[bench] // #[bench]
fn is_char_sensitive(b: &mut Bencher) { // fn is_char_sensitive(b: &mut Bencher) {
let mut iter = ARTISTS.iter().cycle(); // let mut iter = ARTISTS.iter().cycle();
b.iter(|| test::black_box(Search::is_char_sensitive(&iter.next().unwrap()))) // b.iter(|| test::black_box(Search::is_char_sensitive(&iter.next().unwrap())))
} // }
#[bench] // #[bench]
fn normalize_search(b: &mut Bencher) { // fn normalize_search(b: &mut Bencher) {
let mut iter = ARTISTS.iter().cycle(); // let mut iter = ARTISTS.iter().cycle();
b.iter(|| test::black_box(Search::normalize_search(&iter.next().unwrap(), true, true))) // b.iter(|| test::black_box(Search::normalize_search(&iter.next().unwrap(), true, true)))
} // }
} // }

View File

@ -6,7 +6,7 @@ pub use selection::{Category, Delta, Selection, WidgetState};
use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection}; use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection};
use crate::tui::lib::interface::musicbrainz::Match; use crate::tui::lib::interface::musicbrainz::api::Match;
pub enum AppState<B, I, R, S, F, M, E, C> { pub enum AppState<B, I, R, S, F, M, E, C> {
Browse(B), Browse(B),

View File

@ -0,0 +1,104 @@
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API).
use std::collections::HashMap;
use musichoard::{
collection::{
album::{AlbumDate, AlbumMeta, AlbumSeq},
artist::ArtistMeta,
musicbrainz::Mbid,
},
external::musicbrainz::{
api::{
search::{
SearchArtistRequest, SearchArtistResponseArtist, SearchReleaseGroupRequest,
SearchReleaseGroupResponseReleaseGroup,
},
MusicBrainzClient,
},
IMusicBrainzHttp,
},
};
use crate::tui::lib::interface::musicbrainz::api::{Error, IMusicBrainz, Match};
// GRCOV_EXCL_START
pub struct MusicBrainz<Http> {
client: MusicBrainzClient<Http>,
}
impl<Http> MusicBrainz<Http> {
pub fn new(client: MusicBrainzClient<Http>) -> Self {
MusicBrainz { client }
}
}
impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> {
let query = SearchArtistRequest::new().string(&artist.id.name);
let mb_response = self.client.search_artist(query)?;
Ok(mb_response
.artists
.into_iter()
.map(from_search_artist_response_artist)
.collect())
}
fn search_release_group(
&mut self,
arid: &Mbid,
album: &AlbumMeta,
) -> Result<Vec<Match<AlbumMeta>>, Error> {
// Some release groups may have a promotional early release messing up the search. Searching
// with just the year should be enough anyway.
let date = AlbumDate::new(album.date.year, None, None);
let query = SearchReleaseGroupRequest::new()
.arid(arid)
.and()
.first_release_date(&date)
.and()
.release_group(&album.id.title);
let mb_response = self.client.search_release_group(query)?;
Ok(mb_response
.release_groups
.into_iter()
.map(from_search_release_group_response_release_group)
.collect())
}
}
fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match<ArtistMeta> {
Match {
score: entity.score,
item: ArtistMeta {
id: entity.name,
sort: entity.sort.map(Into::into),
musicbrainz: Some(entity.id.into()),
properties: HashMap::new(),
},
disambiguation: entity.disambiguation,
}
}
fn from_search_release_group_response_release_group(
entity: SearchReleaseGroupResponseReleaseGroup,
) -> Match<AlbumMeta> {
Match {
score: entity.score,
item: AlbumMeta {
id: entity.title,
date: entity.first_release_date,
seq: AlbumSeq::default(),
musicbrainz: Some(entity.id.into()),
primary_type: Some(entity.primary_type),
secondary_types: entity.secondary_types.unwrap_or_default(),
},
disambiguation: None,
}
}
// GRCOV_EXCL_STOP

View File

@ -0,0 +1,274 @@
use std::{collections::VecDeque, sync::mpsc, thread, time};
use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid};
use crate::tui::{
app::MatchStateInfo,
event::{Event, EventSender},
lib::interface::musicbrainz::api::{Error as ApiError, IMusicBrainz},
};
pub enum Error {
RequestRecv(String),
Api(String),
}
impl From<mpsc::RecvError> for Error {
fn from(value: mpsc::RecvError) -> Self {
Error::RequestRecv(value.to_string())
}
}
impl From<ApiError> for Error {
fn from(value: ApiError) -> Self {
Error::Api(value.to_string())
}
}
pub struct MusicBrainzDaemon {
api: Box<dyn IMusicBrainz>,
request_receiver: mpsc::Receiver<Job>,
job_queue: JobQueue,
events: EventSender,
}
struct JobQueue {
foreground_queue: VecDeque<JobInstance>,
background_queue: VecDeque<JobInstance>,
}
impl JobQueue {
fn new() -> Self {
JobQueue {
foreground_queue: VecDeque::new(),
background_queue: VecDeque::new(),
}
}
fn is_empty(&self) -> bool {
self.foreground_queue.is_empty() && self.background_queue.is_empty()
}
fn front_mut(&mut self) -> Option<&mut JobInstance> {
self.foreground_queue
.front_mut()
.or_else(|| self.background_queue.front_mut())
}
fn pop_front(&mut self) -> Option<JobInstance> {
self.foreground_queue
.pop_front()
.or_else(|| self.background_queue.pop_front())
}
fn push_back(&mut self, job: Job) {
match job.priority {
JobPriority::Foreground => self.foreground_queue.push_back(job.instance),
JobPriority::Background => self.background_queue.push_back(job.instance),
}
}
}
pub struct Job {
priority: JobPriority,
instance: JobInstance,
}
impl Job {
pub fn new(priority: JobPriority, instance: JobInstance) -> Self {
Job { priority, instance }
}
}
pub enum JobPriority {
Foreground,
Background,
}
pub type ReturnSender = mpsc::Sender<Result<MatchStateInfo, ApiError>>;
pub struct JobInstance {
return_sender: ReturnSender,
call_queue: VecDeque<ApiParams>,
}
impl JobInstance {
pub fn new(return_sender: ReturnSender, call_queue: VecDeque<ApiParams>) -> Self {
JobInstance {
return_sender,
call_queue,
}
}
}
pub enum ApiParams {
Search(SearchParams),
}
impl ApiParams {
pub fn search(params: SearchParams) -> Self {
ApiParams::Search(params)
}
}
pub enum SearchParams {
Artist(SearchArtistParams),
ReleaseGroup(SearchReleaseGroupParams),
}
impl SearchParams {
pub fn artist(params: SearchArtistParams) -> Self {
SearchParams::Artist(params)
}
pub fn release_group(params: SearchReleaseGroupParams) -> Self {
SearchParams::ReleaseGroup(params)
}
}
pub struct SearchArtistParams {
artist: ArtistMeta,
}
impl SearchArtistParams {
pub fn new(artist: ArtistMeta) -> Self {
SearchArtistParams { artist }
}
}
pub struct SearchReleaseGroupParams {
arid: Mbid,
album: AlbumMeta,
}
impl SearchReleaseGroupParams {
pub fn new(arid: Mbid, album: AlbumMeta) -> Self {
SearchReleaseGroupParams { arid, album }
}
}
pub struct RequestChannel {
pub receiver: mpsc::Receiver<Job>,
pub sender: mpsc::Sender<Job>,
}
impl RequestChannel {
pub fn new() -> Self {
let (sender, receiver) = mpsc::channel();
RequestChannel { receiver, sender }
}
}
impl MusicBrainzDaemon {
pub fn run<MB: IMusicBrainz + 'static>(
api: MB,
request_receiver: mpsc::Receiver<Job>,
events: EventSender,
) -> Result<(), Error> {
let daemon = MusicBrainzDaemon {
api: Box::new(api),
request_receiver,
job_queue: JobQueue::new(),
events,
};
daemon.main()
}
fn wait_for_jobs(&mut self) -> Result<(), Error> {
if self.job_queue.is_empty() {
self.job_queue.push_back(self.request_receiver.recv()?);
}
Ok(())
}
fn enqueue_all_pending_jobs(&mut self) -> Result<(), Error> {
loop {
match self.request_receiver.try_recv() {
Ok(job) => self.job_queue.push_back(job),
Err(mpsc::TryRecvError::Empty) => return Ok(()),
Err(mpsc::TryRecvError::Disconnected) => {
return Err(Error::RequestRecv(
mpsc::TryRecvError::Disconnected.to_string(),
))
}
}
}
}
fn execute_next_job(&mut self) -> Option<()> {
if let Some(instance) = self.job_queue.front_mut() {
let result = instance.execute_next(&mut self.api, &mut self.events);
if result.is_none() {
self.job_queue.pop_front();
}
return Some(());
}
None
}
fn main(mut self) -> Result<(), Error> {
loop {
self.wait_for_jobs()?;
self.enqueue_all_pending_jobs()?;
if let Some(()) = self.execute_next_job() {
// Sleep for one second. Required by MB API rate limiting. Assume all other
// processing takes negligible time such that regardless of how much other
// processing there is to be done, this one second sleep is necessary.
thread::sleep(time::Duration::from_secs(1));
}
}
}
}
impl JobInstance {
fn execute_next(
&mut self,
api: &mut Box<dyn IMusicBrainz>,
events: &mut EventSender,
) -> Option<()> {
self.call_queue
.pop_front()
.map(|api_params| self.execute_call(api, events, api_params));
if !self.call_queue.is_empty() {
Some(())
} else {
None
}
}
fn execute_call(
&mut self,
api: &mut Box<dyn IMusicBrainz>,
events: &mut EventSender,
api_params: ApiParams,
) {
match api_params {
ApiParams::Search(search) => match search {
SearchParams::Artist(params) => {
let result = api.search_artist(&params.artist);
let result = result.map(|list| MatchStateInfo::artist(params.artist, list));
self.return_result(events, result).ok();
}
SearchParams::ReleaseGroup(params) => {
let result = api.search_release_group(&params.arid, &params.album);
let result = result.map(|list| MatchStateInfo::album(params.album, list));
self.return_result(events, result).ok();
}
},
}
}
fn return_result(
&mut self,
events: &mut EventSender,
result: Result<MatchStateInfo, ApiError>,
) -> Result<(), ()> {
// If receiver disconnects just drop the rest.
self.return_sender.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(())
}
}

View File

@ -1,104 +1,3 @@
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API). pub mod api;
pub mod daemon;
use std::collections::HashMap;
use musichoard::{
collection::{
album::{AlbumDate, AlbumMeta, AlbumSeq},
artist::ArtistMeta,
musicbrainz::Mbid,
},
external::musicbrainz::{
api::{
search::{
SearchArtistRequest, SearchArtistResponseArtist, SearchReleaseGroupRequest,
SearchReleaseGroupResponseReleaseGroup,
},
MusicBrainzClient,
},
IMusicBrainzHttp,
},
};
use crate::tui::lib::interface::musicbrainz::{Error, IMusicBrainz, Match};
// GRCOV_EXCL_START
pub struct MusicBrainz<Http> {
client: MusicBrainzClient<Http>,
}
impl<Http> MusicBrainz<Http> {
pub fn new(client: MusicBrainzClient<Http>) -> Self {
MusicBrainz { client }
}
}
impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> {
let query = SearchArtistRequest::new().string(&artist.id.name);
let mb_response = self.client.search_artist(query)?;
Ok(mb_response
.artists
.into_iter()
.map(from_search_artist_response_artist)
.collect())
}
fn search_release_group(
&mut self,
arid: &Mbid,
album: &AlbumMeta,
) -> Result<Vec<Match<AlbumMeta>>, Error> {
// Some release groups may have a promotional early release messing up the search. Searching
// with just the year should be enough anyway.
let date = AlbumDate::new(album.date.year, None, None);
let query = SearchReleaseGroupRequest::new()
.arid(arid)
.and()
.first_release_date(&date)
.and()
.release_group(&album.id.title);
let mb_response = self.client.search_release_group(query)?;
Ok(mb_response
.release_groups
.into_iter()
.map(from_search_release_group_response_release_group)
.collect())
}
}
fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match<ArtistMeta> {
Match {
score: entity.score,
item: ArtistMeta {
id: entity.name,
sort: entity.sort.map(Into::into),
musicbrainz: Some(entity.id.into()),
properties: HashMap::new(),
},
disambiguation: entity.disambiguation,
}
}
fn from_search_release_group_response_release_group(
entity: SearchReleaseGroupResponseReleaseGroup,
) -> Match<AlbumMeta> {
Match {
score: entity.score,
item: AlbumMeta {
id: entity.title,
date: entity.first_release_date,
seq: AlbumSeq::default(),
musicbrainz: Some(entity.id.into()),
primary_type: Some(entity.primary_type),
secondary_types: entity.secondary_types.unwrap_or_default(),
},
disambiguation: None,
}
}
// GRCOV_EXCL_STOP

View File

@ -0,0 +1,26 @@
//! Module for accessing MusicBrainz metadata.
#[cfg(test)]
use mockall::automock;
use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid};
/// Trait for interacting with the MusicBrainz API.
#[cfg_attr(test, automock)]
pub trait IMusicBrainz {
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error>;
fn search_release_group(
&mut self,
arid: &Mbid,
album: &AlbumMeta,
) -> Result<Vec<Match<AlbumMeta>>, Error>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Match<T> {
pub score: u8,
pub item: T,
pub disambiguation: Option<String>,
}
pub type Error = musichoard::external::musicbrainz::api::Error;

View File

@ -1,26 +1 @@
//! Module for accessing MusicBrainz metadata. pub mod api;
#[cfg(test)]
use mockall::automock;
use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid};
/// Trait for interacting with the MusicBrainz API.
#[cfg_attr(test, automock)]
pub trait IMusicBrainz {
fn search_artist(&mut self, name: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error>;
fn search_release_group(
&mut self,
arid: &Mbid,
album: &AlbumMeta,
) -> Result<Vec<Match<AlbumMeta>>, Error>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Match<T> {
pub score: u8,
pub item: T,
pub disambiguation: Option<String>,
}
pub type Error = musichoard::external::musicbrainz::api::Error;

View File

@ -8,7 +8,10 @@ mod ui;
pub use app::App; pub use app::App;
pub use event::EventChannel; pub use event::EventChannel;
pub use handler::EventHandler; pub use handler::EventHandler;
pub use lib::external::musicbrainz::MusicBrainz; pub use lib::external::musicbrainz::{
api::MusicBrainz,
daemon::{MusicBrainzDaemon, RequestChannel},
};
pub use listener::EventListener; pub use listener::EventListener;
pub use ui::Ui; pub use ui::Ui;
@ -170,154 +173,154 @@ impl<B: Backend, UI: IUi, APP: IApp + IAppAccess> Tui<B, UI, APP> {
#[cfg(test)] #[cfg(test)]
mod testmod; mod testmod;
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use std::{io, thread}; // use std::{io, thread};
use event::EventSender; // use event::EventSender;
use lib::interface::musicbrainz::MockIMusicBrainz; // use lib::interface::musicbrainz::api::MockIMusicBrainz;
use ratatui::{backend::TestBackend, Terminal}; // use ratatui::{backend::TestBackend, Terminal};
use musichoard::collection::Collection; // use musichoard::collection::Collection;
use crate::tui::{ // use crate::tui::{
app::App, handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener, // app::App, handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener,
ui::Ui, // ui::Ui,
}; // };
use super::*; // use super::*;
use testmod::COLLECTION; // use testmod::COLLECTION;
pub fn terminal() -> Terminal<TestBackend> { // pub fn terminal() -> Terminal<TestBackend> {
let backend = TestBackend::new(150, 30); // let backend = TestBackend::new(150, 30);
Terminal::new(backend).unwrap() // Terminal::new(backend).unwrap()
} // }
fn music_hoard(collection: Collection) -> MockIMusicHoard { // fn music_hoard(collection: Collection) -> MockIMusicHoard {
let mut music_hoard = MockIMusicHoard::new(); // let mut music_hoard = MockIMusicHoard::new();
music_hoard.expect_reload_database().returning(|| Ok(())); // music_hoard.expect_reload_database().returning(|| Ok(()));
music_hoard.expect_rescan_library().returning(|| Ok(())); // music_hoard.expect_rescan_library().returning(|| Ok(()));
music_hoard.expect_get_collection().return_const(collection); // music_hoard.expect_get_collection().return_const(collection);
music_hoard // music_hoard
} // }
fn events() -> EventSender { // fn events() -> EventSender {
EventChannel::new().sender() // EventChannel::new().sender()
} // }
fn app(collection: Collection) -> App { // fn app(collection: Collection) -> App {
App::new(music_hoard(collection), MockIMusicBrainz::new(), events()) // App::new(music_hoard(collection), MockIMusicBrainz::new(), events())
} // }
fn listener() -> MockIEventListener { // fn listener() -> MockIEventListener {
let mut listener = MockIEventListener::new(); // let mut listener = MockIEventListener::new();
listener.expect_spawn().return_once(|| { // listener.expect_spawn().return_once(|| {
thread::spawn(|| { // thread::spawn(|| {
thread::park(); // thread::park();
EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "unparked")) // EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "unparked"))
}) // })
}); // });
listener // listener
} // }
fn handler() -> MockIEventHandler<App> { // fn handler() -> MockIEventHandler<App> {
let mut handler = MockIEventHandler::new(); // let mut handler = MockIEventHandler::new();
handler // handler
.expect_handle_next_event() // .expect_handle_next_event()
.return_once(|app: App| Ok(app.force_quit())); // .return_once(|app: App| Ok(app.force_quit()));
handler // handler
} // }
#[test] // #[test]
fn run() { // fn run() {
let terminal = terminal(); // let terminal = terminal();
let app = app(COLLECTION.to_owned()); // let app = app(COLLECTION.to_owned());
let ui = Ui; // let ui = Ui;
let listener = listener(); // let listener = listener();
let handler = handler(); // let handler = handler();
let result = Tui::main(terminal, app, ui, handler, listener); // let result = Tui::main(terminal, app, ui, handler, listener);
assert!(result.is_ok()); // assert!(result.is_ok());
} // }
#[test] // #[test]
fn event_error() { // fn event_error() {
let terminal = terminal(); // let terminal = terminal();
let app = app(COLLECTION.to_owned()); // let app = app(COLLECTION.to_owned());
let ui = Ui; // let ui = Ui;
let listener = listener(); // let listener = listener();
let mut handler = MockIEventHandler::new(); // let mut handler = MockIEventHandler::new();
handler // handler
.expect_handle_next_event() // .expect_handle_next_event()
.return_once(|_| Err(EventError::Recv)); // .return_once(|_| Err(EventError::Recv));
let result = Tui::main(terminal, app, ui, handler, listener); // let result = Tui::main(terminal, app, ui, handler, listener);
assert!(result.is_err()); // assert!(result.is_err());
let error = EventError::Recv; // let error = EventError::Recv;
assert_eq!(result.unwrap_err(), Error::Event(error.to_string())); // assert_eq!(result.unwrap_err(), Error::Event(error.to_string()));
} // }
#[test] // #[test]
fn listener_error() { // fn listener_error() {
let terminal = terminal(); // let terminal = terminal();
let app = app(COLLECTION.to_owned()); // let app = app(COLLECTION.to_owned());
let ui = Ui; // let ui = Ui;
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error")); // let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| error); // let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| error);
while !listener_handle.is_finished() {} // while !listener_handle.is_finished() {}
let mut listener = MockIEventListener::new(); // let mut listener = MockIEventListener::new();
listener.expect_spawn().return_once(|| listener_handle); // listener.expect_spawn().return_once(|| listener_handle);
let mut handler = MockIEventHandler::new(); // let mut handler = MockIEventHandler::new();
handler // handler
.expect_handle_next_event() // .expect_handle_next_event()
.return_once(|_| Err(EventError::Recv)); // .return_once(|_| Err(EventError::Recv));
let result = Tui::main(terminal, app, ui, handler, listener); // let result = Tui::main(terminal, app, ui, handler, listener);
assert!(result.is_err()); // assert!(result.is_err());
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error")); // let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
assert_eq!(result.unwrap_err(), Error::Event(error.to_string())); // assert_eq!(result.unwrap_err(), Error::Event(error.to_string()));
} // }
#[test] // #[test]
fn listener_panic() { // fn listener_panic() {
let terminal = terminal(); // let terminal = terminal();
let app = app(COLLECTION.to_owned()); // let app = app(COLLECTION.to_owned());
let ui = Ui; // let ui = Ui;
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| panic!()); // let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| panic!());
while !listener_handle.is_finished() {} // while !listener_handle.is_finished() {}
let mut listener = MockIEventListener::new(); // let mut listener = MockIEventListener::new();
listener.expect_spawn().return_once(|| listener_handle); // listener.expect_spawn().return_once(|| listener_handle);
let mut handler = MockIEventHandler::new(); // let mut handler = MockIEventHandler::new();
handler // handler
.expect_handle_next_event() // .expect_handle_next_event()
.return_once(|_| Err(EventError::Recv)); // .return_once(|_| Err(EventError::Recv));
let result = Tui::main(terminal, app, ui, handler, listener); // let result = Tui::main(terminal, app, ui, handler, listener);
assert!(result.is_err()); // assert!(result.is_err());
assert_eq!(result.unwrap_err(), Error::ListenerPanic); // assert_eq!(result.unwrap_err(), Error::ListenerPanic);
} // }
#[test] // #[test]
fn errors() { // fn errors() {
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into(); // let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
let event_err: Error = EventError::Recv.into(); // let event_err: Error = EventError::Recv.into();
let listener_err = Error::ListenerPanic; // let listener_err = Error::ListenerPanic;
assert!(!format!("{:?}", io_err).is_empty()); // assert!(!format!("{:?}", io_err).is_empty());
assert!(!format!("{:?}", event_err).is_empty()); // assert!(!format!("{:?}", event_err).is_empty());
assert!(!format!("{:?}", listener_err).is_empty()); // assert!(!format!("{:?}", listener_err).is_empty());
} // }
} // }

View File

@ -198,232 +198,232 @@ impl IUi for Ui {
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use musichoard::collection::{ // use musichoard::collection::{
album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType}, // album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType},
artist::{Artist, ArtistId, ArtistMeta}, // artist::{Artist, ArtistId, ArtistMeta},
}; // };
use crate::tui::{ // use crate::tui::{
app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic}, // app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic},
lib::interface::musicbrainz::Match, // lib::interface::musicbrainz::api::Match,
testmod::COLLECTION, // testmod::COLLECTION,
tests::terminal, // tests::terminal,
}; // };
use super::*; // use super::*;
// Automock does not support returning types with generic lifetimes. // // Automock does not support returning types with generic lifetimes.
impl<'app> IAppAccess for AppPublic<'app> { // impl<'app> IAppAccess for AppPublic<'app> {
fn get(&mut self) -> AppPublic { // fn get(&mut self) -> AppPublic {
AppPublic { // AppPublic {
inner: AppPublicInner { // inner: AppPublicInner {
collection: self.inner.collection, // collection: self.inner.collection,
selection: self.inner.selection, // selection: self.inner.selection,
}, // },
state: match self.state { // state: match self.state {
AppState::Browse(()) => AppState::Browse(()), // AppState::Browse(()) => AppState::Browse(()),
AppState::Info(()) => AppState::Info(()), // AppState::Info(()) => AppState::Info(()),
AppState::Reload(()) => AppState::Reload(()), // AppState::Reload(()) => AppState::Reload(()),
AppState::Search(s) => AppState::Search(s), // AppState::Search(s) => AppState::Search(s),
AppState::Fetch(()) => AppState::Fetch(()), // AppState::Fetch(()) => AppState::Fetch(()),
AppState::Match(ref mut m) => AppState::Match(MatchStatePublic { // AppState::Match(ref mut m) => AppState::Match(MatchStatePublic {
info: m.info, // info: m.info,
state: m.state, // state: m.state,
}), // }),
AppState::Error(s) => AppState::Error(s), // AppState::Error(s) => AppState::Error(s),
AppState::Critical(s) => AppState::Critical(s), // AppState::Critical(s) => AppState::Critical(s),
}, // },
input: self.input, // input: self.input,
} // }
} // }
} // }
fn public_inner<'app>( // fn public_inner<'app>(
collection: &'app Collection, // collection: &'app Collection,
selection: &'app mut Selection, // selection: &'app mut Selection,
) -> AppPublicInner<'app> { // ) -> AppPublicInner<'app> {
AppPublicInner { // AppPublicInner {
collection, // collection,
selection, // selection,
} // }
} // }
fn artist_matches(matching: ArtistMeta, list: Vec<Match<ArtistMeta>>) -> MatchStateInfo { // fn artist_matches(matching: ArtistMeta, list: Vec<Match<ArtistMeta>>) -> MatchStateInfo {
let mut list: Vec<MatchOption<ArtistMeta>> = list.into_iter().map(Into::into).collect(); // let mut list: Vec<MatchOption<ArtistMeta>> = list.into_iter().map(Into::into).collect();
list.push(MatchOption::CannotHaveMbid); // list.push(MatchOption::CannotHaveMbid);
list.push(MatchOption::ManualInputMbid); // list.push(MatchOption::ManualInputMbid);
MatchStateInfo::artist(matching, list) // MatchStateInfo::artist(matching, list)
} // }
fn album_matches(matching: AlbumMeta, list: Vec<Match<AlbumMeta>>) -> MatchStateInfo { // fn album_matches(matching: AlbumMeta, list: Vec<Match<AlbumMeta>>) -> MatchStateInfo {
let mut list: Vec<MatchOption<AlbumMeta>> = list.into_iter().map(Into::into).collect(); // let mut list: Vec<MatchOption<AlbumMeta>> = list.into_iter().map(Into::into).collect();
list.push(MatchOption::CannotHaveMbid); // list.push(MatchOption::CannotHaveMbid);
list.push(MatchOption::ManualInputMbid); // list.push(MatchOption::ManualInputMbid);
MatchStateInfo::album(matching, list) // MatchStateInfo::album(matching, list)
} // }
fn draw_test_suite(collection: &Collection, selection: &mut Selection) { // fn draw_test_suite(collection: &Collection, selection: &mut Selection) {
let mut terminal = terminal(); // let mut terminal = terminal();
let mut app = AppPublic { // let mut app = AppPublic {
inner: public_inner(collection, selection), // inner: public_inner(collection, selection),
state: AppState::Browse(()), // state: AppState::Browse(()),
input: None, // input: None,
}; // };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
app.state = AppState::Info(()); // app.state = AppState::Info(());
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
app.state = AppState::Reload(()); // app.state = AppState::Reload(());
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
app.state = AppState::Search(""); // app.state = AppState::Search("");
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
app.state = AppState::Fetch(()); // app.state = AppState::Fetch(());
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
app.state = AppState::Error("get rekt scrub"); // app.state = AppState::Error("get rekt scrub");
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
app.state = AppState::Critical("get critically rekt scrub"); // app.state = AppState::Critical("get critically rekt scrub");
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
} // }
#[test] // #[test]
fn empty() { // fn empty() {
let artists: Vec<Artist> = vec![]; // let artists: Vec<Artist> = vec![];
let mut selection = Selection::new(&artists); // let mut selection = Selection::new(&artists);
draw_test_suite(&artists, &mut selection); // draw_test_suite(&artists, &mut selection);
} // }
#[test] // #[test]
fn empty_album() { // fn empty_album() {
let mut artists: Vec<Artist> = vec![Artist::new(ArtistId::new("An artist"))]; // let mut artists: Vec<Artist> = vec![Artist::new(ArtistId::new("An artist"))];
artists[0] // artists[0]
.albums // .albums
.push(Album::new("An album", AlbumDate::default(), None, vec![])); // .push(Album::new("An album", AlbumDate::default(), None, vec![]));
let mut selection = Selection::new(&artists); // let mut selection = Selection::new(&artists);
draw_test_suite(&artists, &mut selection); // draw_test_suite(&artists, &mut selection);
} // }
#[test] // #[test]
fn collection() { // fn collection() {
let artists = &COLLECTION; // let artists = &COLLECTION;
let mut selection = Selection::new(artists); // let mut selection = Selection::new(artists);
draw_test_suite(artists, &mut selection); // draw_test_suite(artists, &mut selection);
// Change the track (which has a different track format). // // Change the track (which has a different track format).
selection.increment_category(); // selection.increment_category();
selection.increment_category(); // selection.increment_category();
selection.increment_selection(artists, Delta::Line); // selection.increment_selection(artists, Delta::Line);
draw_test_suite(artists, &mut selection); // draw_test_suite(artists, &mut selection);
// Change the artist (which has a multi-link entry). // // Change the artist (which has a multi-link entry).
selection.decrement_category(); // selection.decrement_category();
selection.decrement_category(); // selection.decrement_category();
selection.increment_selection(artists, Delta::Line); // selection.increment_selection(artists, Delta::Line);
draw_test_suite(artists, &mut selection); // draw_test_suite(artists, &mut selection);
} // }
#[test] // #[test]
fn draw_empty_matches() { // fn draw_empty_matches() {
let collection = &COLLECTION; // let collection = &COLLECTION;
let mut selection = Selection::new(collection); // let mut selection = Selection::new(collection);
let mut terminal = terminal(); // let mut terminal = terminal();
let mut widget_state = WidgetState::default(); // let mut widget_state = WidgetState::default();
let mut app = AppPublic { // let mut app = AppPublic {
inner: public_inner(collection, &mut selection), // inner: public_inner(collection, &mut selection),
state: AppState::Match(MatchStatePublic { // state: AppState::Match(MatchStatePublic {
info: None, // info: None,
state: &mut widget_state, // state: &mut widget_state,
}), // }),
input: None, // input: None,
}; // };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
} // }
#[test] // #[test]
fn draw_artist_matches() { // fn draw_artist_matches() {
let collection = &COLLECTION; // let collection = &COLLECTION;
let mut selection = Selection::new(collection); // let mut selection = Selection::new(collection);
let mut terminal = terminal(); // let mut terminal = terminal();
let artist = ArtistMeta::new(ArtistId::new("an artist")); // let artist = ArtistMeta::new(ArtistId::new("an artist"));
let artist_match = Match { // let artist_match = Match {
score: 80, // score: 80,
item: artist.clone(), // item: artist.clone(),
disambiguation: None, // disambiguation: None,
}; // };
let list = vec![artist_match.clone(), artist_match.clone()]; // let list = vec![artist_match.clone(), artist_match.clone()];
let artist_matches = artist_matches(artist, list); // let artist_matches = artist_matches(artist, list);
let mut widget_state = WidgetState::default(); // let mut widget_state = WidgetState::default();
widget_state.list.select(Some(0)); // widget_state.list.select(Some(0));
let mut app = AppPublic { // let mut app = AppPublic {
inner: public_inner(collection, &mut selection), // inner: public_inner(collection, &mut selection),
state: AppState::Match(MatchStatePublic { // state: AppState::Match(MatchStatePublic {
info: Some(&artist_matches), // info: Some(&artist_matches),
state: &mut widget_state, // state: &mut widget_state,
}), // }),
input: None, // input: None,
}; // };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
let input = tui_input::Input::default(); // let input = tui_input::Input::default();
app.input = Some(&input); // app.input = Some(&input);
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
} // }
#[test] // #[test]
fn draw_album_matches() { // fn draw_album_matches() {
let collection = &COLLECTION; // let collection = &COLLECTION;
let mut selection = Selection::new(collection); // let mut selection = Selection::new(collection);
let mut terminal = terminal(); // let mut terminal = terminal();
let album = AlbumMeta::new( // let album = AlbumMeta::new(
AlbumId::new("An Album"), // AlbumId::new("An Album"),
AlbumDate::new(Some(1990), Some(5), None), // AlbumDate::new(Some(1990), Some(5), None),
Some(AlbumPrimaryType::Album), // Some(AlbumPrimaryType::Album),
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], // vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
); // );
let album_match = Match { // let album_match = Match {
score: 80, // score: 80,
item: album.clone(), // item: album.clone(),
disambiguation: None, // disambiguation: None,
}; // };
let list = vec![album_match.clone(), album_match.clone()]; // let list = vec![album_match.clone(), album_match.clone()];
let album_matches = album_matches(album, list); // let album_matches = album_matches(album, list);
let mut widget_state = WidgetState::default(); // let mut widget_state = WidgetState::default();
widget_state.list.select(Some(0)); // widget_state.list.select(Some(0));
let mut app = AppPublic { // let mut app = AppPublic {
inner: public_inner(collection, &mut selection), // inner: public_inner(collection, &mut selection),
state: AppState::Match(MatchStatePublic { // state: AppState::Match(MatchStatePublic {
info: Some(&album_matches), // info: Some(&album_matches),
state: &mut widget_state, // state: &mut widget_state,
}), // }),
input: None, // input: None,
}; // };
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
let input = tui_input::Input::default(); // let input = tui_input::Input::default();
app.input = Some(&input); // app.input = Some(&input);
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); // terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
} // }
} // }