From f5f2cc478fd1fa4c974ca2f8529ce452e3d6872e Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sat, 7 Sep 2024 23:17:17 +0200 Subject: [PATCH] Unit tests for browse --- src/tui/app/machine/browse.rs | 327 +++++++++++++++++++++++++++++++++- src/tui/app/machine/fetch.rs | 256 +------------------------- src/tui/event.rs | 32 +++- 3 files changed, 354 insertions(+), 261 deletions(-) diff --git a/src/tui/app/machine/browse.rs b/src/tui/app/machine/browse.rs index a8cb175..7f87803 100644 --- a/src/tui/app/machine/browse.rs +++ b/src/tui/app/machine/browse.rs @@ -1,9 +1,26 @@ -use crate::tui::app::{ - machine::{App, AppInner, AppMachine}, - selection::{Delta, ListSelection}, - AppPublic, AppState, IAppInteractBrowse, +use std::{ + sync::{mpsc, Arc, Mutex}, + thread, time, }; +use musichoard::collection::{ + album::AlbumMeta, + artist::{Artist, ArtistMeta}, + musicbrainz::{IMusicBrainzRef, Mbid}, +}; + +use crate::tui::{ + app::{ + machine::{App, AppInner, AppMachine}, + selection::{Delta, ListSelection}, + AppMatchesInfo, AppPublic, AppState, IAppInteractBrowse, + }, + event::{Event, EventSender}, + lib::interface::musicbrainz::{self, IMusicBrainz}, +}; + +use super::fetch::{FetchResult, FetchSender}; + pub struct AppBrowse; impl AppMachine { @@ -13,6 +30,80 @@ impl AppMachine { state: AppBrowse, } } + + fn spawn_fetch_thread( + inner: &AppInner, + artist: &Artist, + tx: FetchSender, + ) -> thread::JoinHandle<()> { + match artist.meta.musicbrainz { + Some(ref arid) => { + let musicbrainz = Arc::clone(&inner.musicbrainz); + let events = inner.events.clone(); + let arid = arid.mbid().clone(); + let albums = artist.albums.iter().map(|a| &a.meta).cloned().collect(); + thread::spawn(|| Self::fetch_albums(musicbrainz, tx, events, arid, albums)) + } + None => { + let musicbrainz = Arc::clone(&inner.musicbrainz); + let events = inner.events.clone(); + let artist = artist.meta.clone(); + thread::spawn(|| Self::fetch_artist(musicbrainz, tx, events, artist)) + } + } + } + + fn fetch_artist( + musicbrainz: Arc>, + fetch_tx: FetchSender, + events: EventSender, + artist: ArtistMeta, + ) { + let result = musicbrainz.lock().unwrap().search_artist(&artist); + let result = result.map(|list| AppMatchesInfo::artist(artist, list)); + Self::send_fetch_result(&fetch_tx, &events, result).ok(); + } + + fn fetch_albums( + musicbrainz: Arc>, + fetch_tx: FetchSender, + events: EventSender, + arid: Mbid, + albums: Vec, + ) { + let mut musicbrainz = musicbrainz.lock().unwrap(); + let mut album_iter = albums.into_iter().peekable(); + while let Some(album) = album_iter.next() { + if album.musicbrainz.is_some() { + continue; + } + + let result = musicbrainz.search_release_group(&arid, &album); + let result = result.map(|list| AppMatchesInfo::album(album, list)); + if Self::send_fetch_result(&fetch_tx, &events, result).is_err() { + return; + }; + + if album_iter.peek().is_some() { + thread::sleep(time::Duration::from_secs(1)); + } + } + } + + fn send_fetch_result( + fetch_tx: &FetchSender, + events: &EventSender, + result: Result, + ) -> Result<(), ()> { + // If receiver disconnects just drop the rest. + fetch_tx.send(result).map_err(|_| ())?; + + // If this send fails the event listener is dead. Don't panic as this function runs in a + // detached thread so this might be happening during normal shut down. + events.send(Event::FetchResultReady).map_err(|_| ())?; + + Ok(()) + } } impl From> for App { @@ -79,7 +170,18 @@ impl IAppInteractBrowse for AppMachine { } fn fetch_musicbrainz(self) -> Self::APP { - AppMachine::app_fetch_new(self.inner) + let coll = self.inner.music_hoard.get_collection(); + let artist = match self.inner.selection.state_artist(coll) { + Some(artist_state) => &coll[artist_state.index], + None => { + return AppMachine::error(self.inner, "cannot fetch: no artist selected").into() + } + }; + + let (fetch_tx, fetch_rx) = mpsc::channel::(); + Self::spawn_fetch_thread(&self.inner, artist, fetch_tx); + + AppMachine::app_fetch_new(self.inner, fetch_rx) } fn no_op(self) -> Self::APP { @@ -89,12 +191,18 @@ impl IAppInteractBrowse for AppMachine { #[cfg(test)] mod tests { + use mockall::{predicate, Sequence}; + use mpsc::TryRecvError; + use crate::tui::{ app::{ - machine::tests::{inner, music_hoard}, - Category, IAppInteract, + machine::tests::{inner, inner_with_mb, music_hoard}, + AppAlbumMatches, AppArtistMatches, Category, IAppAccess, IAppInteract, }, + event::EventReceiver, + lib::interface::musicbrainz::{Match, MockIMusicBrainz}, testmod::COLLECTION, + EventChannel, }; use super::*; @@ -170,12 +278,215 @@ mod tests { } #[test] - fn fetch_musicbrainz() { + fn fetch_musicbrainz_no_artist() { let browse = AppMachine::browse(inner(music_hoard(vec![]))); let app = browse.fetch_musicbrainz(); app.unwrap_error(); } + #[test] + fn fetch_musicbrainz() { + let mb_api = MockIMusicBrainz::new(); + let browse = AppMachine::browse(inner_with_mb(music_hoard(COLLECTION.to_owned()), mb_api)); + + // Use the second artist for this test. + let browse = browse.increment_selection(Delta::Line).unwrap_browse(); + let mut app = browse.fetch_musicbrainz(); + + let public = app.get(); + + // Because of threaded behaviour, this unit test cannot expect one or the other. + assert!( + matches!(public.state, AppState::Matches(_)) + || matches!(public.state, AppState::Fetch(_)) + ); + } + + fn event_channel() -> (EventSender, EventReceiver) { + let event_channel = EventChannel::new(); + let events_tx = event_channel.sender(); + let events_rx = event_channel.receiver(); + (events_tx, events_rx) + } + + fn album_expectations_1() -> (AlbumMeta, Vec>) { + let album_1 = COLLECTION[1].albums[0].meta.clone(); + let album_4 = COLLECTION[1].albums[3].meta.clone(); + + let album_match_1_1 = Match::new(100, album_1.clone()); + let album_match_1_2 = Match::new(50, album_4.clone()); + let matches_1 = vec![album_match_1_1.clone(), album_match_1_2.clone()]; + + (album_1, matches_1) + } + + fn album_expectations_4() -> (AlbumMeta, Vec>) { + let album_1 = COLLECTION[1].albums[0].meta.clone(); + let album_4 = COLLECTION[1].albums[3].meta.clone(); + + let album_match_4_1 = Match::new(100, album_4.clone()); + let album_match_4_2 = Match::new(30, album_1.clone()); + let matches_4 = vec![album_match_4_1.clone(), album_match_4_2.clone()]; + + (album_4, matches_4) + } + + fn search_release_group_expectation( + api: &mut MockIMusicBrainz, + seq: &mut Sequence, + arid: &Mbid, + album: &AlbumMeta, + matches: &Vec>, + ) { + let result = Ok(matches.clone()); + api.expect_search_release_group() + .with(predicate::eq(arid.clone()), predicate::eq(album.clone())) + .times(1) + .in_sequence(seq) + .return_once(|_, _| result); + } + + #[test] + fn fetch_albums() { + let mut mb_api = MockIMusicBrainz::new(); + + let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); + + let (album_1, matches_1) = album_expectations_1(); + let (album_4, matches_4) = album_expectations_4(); + + // Other albums have an MBID and so they will be skipped. + let mut seq = Sequence::new(); + + search_release_group_expectation(&mut mb_api, &mut seq, &arid, &album_1, &matches_1); + search_release_group_expectation(&mut mb_api, &mut seq, &arid, &album_4, &matches_4); + + let music_hoard = music_hoard(COLLECTION.to_owned()); + let (events_tx, events_rx) = event_channel(); + let inner = AppInner::new(music_hoard, mb_api, events_tx); + + let (fetch_tx, fetch_rx) = mpsc::channel(); + // Use the second artist for this test. + let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[1], fetch_tx); + handle.join().unwrap(); + + assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady); + let result = fetch_rx.try_recv().unwrap(); + let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches { + matching: album_1.clone(), + list: matches_1.iter().cloned().map(Into::into).collect(), + })); + assert_eq!(result, expected); + + assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady); + let result = fetch_rx.try_recv().unwrap(); + let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches { + matching: album_4.clone(), + list: matches_4.iter().cloned().map(Into::into).collect(), + })); + assert_eq!(result, expected); + } + + fn artist_expectations() -> (ArtistMeta, Vec>) { + let artist = COLLECTION[3].meta.clone(); + + let artist_match_1 = Match::new(100, artist.clone()); + let artist_match_2 = Match::new(50, artist.clone()); + let matches = vec![artist_match_1.clone(), artist_match_2.clone()]; + + (artist, matches) + } + + fn search_artist_expectation( + api: &mut MockIMusicBrainz, + seq: &mut Sequence, + artist: &ArtistMeta, + matches: &Vec>, + ) { + let result = Ok(matches.clone()); + api.expect_search_artist() + .with(predicate::eq(artist.clone())) + .times(1) + .in_sequence(seq) + .return_once(|_| result); + } + + #[test] + fn fetch_artist() { + let mut mb_api = MockIMusicBrainz::new(); + + let (artist, matches) = artist_expectations(); + let mut seq = Sequence::new(); + search_artist_expectation(&mut mb_api, &mut seq, &artist, &matches); + + let music_hoard = music_hoard(COLLECTION.to_owned()); + let (events_tx, events_rx) = event_channel(); + let inner = AppInner::new(music_hoard, mb_api, events_tx); + + let (fetch_tx, fetch_rx) = mpsc::channel(); + // Use the fourth artist for this test as they have no MBID. + let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[3], fetch_tx); + handle.join().unwrap(); + + assert_eq!(events_rx.try_recv().unwrap(), Event::FetchResultReady); + let result = fetch_rx.try_recv().unwrap(); + let expected = Ok(AppMatchesInfo::Artist(AppArtistMatches { + matching: artist.clone(), + list: matches.iter().cloned().map(Into::into).collect(), + })); + assert_eq!(result, expected); + } + + #[test] + fn fetch_artist_fetch_disconnect() { + let mut mb_api = MockIMusicBrainz::new(); + + let (artist, matches) = artist_expectations(); + let mut seq = Sequence::new(); + search_artist_expectation(&mut mb_api, &mut seq, &artist, &matches); + + let music_hoard = music_hoard(COLLECTION.to_owned()); + let (events_tx, events_rx) = event_channel(); + let inner = AppInner::new(music_hoard, mb_api, events_tx); + + let (fetch_tx, _) = mpsc::channel(); + // Use the fourth artist for this test as they have no MBID. + let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[3], fetch_tx); + handle.join().unwrap(); + + assert!(events_rx.try_recv().is_err()); + } + + #[test] + fn fetch_albums_event_disconnect() { + let mut mb_api = MockIMusicBrainz::new(); + + let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); + + let (album_1, matches_1) = album_expectations_1(); + + let mut seq = Sequence::new(); + search_release_group_expectation(&mut mb_api, &mut seq, &arid, &album_1, &matches_1); + + let music_hoard = music_hoard(COLLECTION.to_owned()); + let (events_tx, _) = event_channel(); + let inner = AppInner::new(music_hoard, mb_api, events_tx); + + let (fetch_tx, fetch_rx) = mpsc::channel(); + // Use the second artist for this test. + let handle = AppMachine::spawn_fetch_thread(&inner, &COLLECTION[1], fetch_tx); + handle.join().unwrap(); + + let result = fetch_rx.try_recv().unwrap(); + let expected = Ok(AppMatchesInfo::Album(AppAlbumMatches { + matching: album_1.clone(), + list: matches_1.iter().cloned().map(Into::into).collect(), + })); + assert_eq!(result, expected); + + assert_eq!(fetch_rx.try_recv().unwrap_err(), TryRecvError::Disconnected); + } + #[test] fn no_op() { let browse = AppMachine::browse(inner(music_hoard(vec![]))); diff --git a/src/tui/app/machine/fetch.rs b/src/tui/app/machine/fetch.rs index 6357b51..78816b6 100644 --- a/src/tui/app/machine/fetch.rs +++ b/src/tui/app/machine/fetch.rs @@ -1,24 +1,11 @@ -use std::{ - sync::{ - mpsc::{self, TryRecvError}, - Arc, Mutex, - }, - thread, time, -}; - -use musichoard::collection::{ - album::AlbumMeta, - artist::ArtistMeta, - musicbrainz::{IMusicBrainzRef, Mbid}, -}; +use std::sync::mpsc::{self, TryRecvError}; use crate::tui::{ app::{ machine::{App, AppInner, AppMachine}, AppMatchesInfo, AppPublic, AppState, IAppEventFetch, IAppInteractFetch, }, - event::{Event, EventSender}, - lib::interface::musicbrainz::{Error as MbError, IMusicBrainz}, + lib::interface::musicbrainz::Error as MbError, }; use super::matches::AppMatches; @@ -43,31 +30,7 @@ impl AppMachine { AppMachine { inner, state } } - pub fn app_fetch_new(inner: AppInner) -> App { - let coll = inner.music_hoard.get_collection(); - let artist = match inner.selection.state_artist(coll) { - Some(artist_state) => &coll[artist_state.index], - None => return AppMachine::error(inner, "cannot fetch: no artist selected").into(), - }; - - let (fetch_tx, fetch_rx) = mpsc::channel::(); - - match artist.meta.musicbrainz { - Some(ref arid) => { - let musicbrainz = Arc::clone(&inner.musicbrainz); - let events = inner.events.clone(); - let arid = arid.mbid().clone(); - let albums = artist.albums.iter().map(|a| &a.meta).cloned().collect(); - thread::spawn(|| Self::fetch_albums(musicbrainz, fetch_tx, events, arid, albums)); - } - None => { - let musicbrainz = Arc::clone(&inner.musicbrainz); - let events = inner.events.clone(); - let artist = artist.meta.clone(); - thread::spawn(|| Self::fetch_artist(musicbrainz, fetch_tx, events, artist)); - } - }; - + pub fn app_fetch_new(inner: AppInner, fetch_rx: FetchReceiver) -> App { let fetch = AppFetch::new(fetch_rx); AppMachine::app_fetch(inner, fetch, true) } @@ -76,7 +39,7 @@ impl AppMachine { Self::app_fetch(inner, fetch, false) } - pub fn app_fetch(inner: AppInner, fetch: AppFetch, first: bool) -> App { + fn app_fetch(inner: AppInner, fetch: AppFetch, first: bool) -> App { match fetch.fetch_rx.try_recv() { Ok(fetch_result) => match fetch_result { Ok(next_match) => { @@ -99,54 +62,6 @@ impl AppMachine { }, } } - - fn fetch_artist( - musicbrainz: Arc>, - fetch_tx: FetchSender, - events: EventSender, - artist: ArtistMeta, - ) { - let result = musicbrainz.lock().unwrap().search_artist(&artist); - let result = result.map(|list| AppMatchesInfo::artist(artist, list)); - if fetch_tx.send(result).is_ok() { - // 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).ok(); - } - } - - fn fetch_albums( - musicbrainz: Arc>, - fetch_tx: FetchSender, - events: EventSender, - arid: Mbid, - albums: Vec, - ) { - let mut musicbrainz = musicbrainz.lock().unwrap(); - let mut album_iter = albums.into_iter().peekable(); - while let Some(album) = album_iter.next() { - if album.musicbrainz.is_some() { - continue; - } - - let result = musicbrainz.search_release_group(&arid, &album); - let result = result.map(|list| AppMatchesInfo::album(album, list)); - if fetch_tx.send(result).is_err() { - // If receiver disconnects just drop the rest. - return; - } - - if events.send(Event::FetchResultReady).is_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. - return; - } - - if album_iter.peek().is_some() { - thread::sleep(time::Duration::from_secs(1)); - } - } - } } impl From> for App { @@ -204,131 +119,6 @@ mod tests { // use super::*; - // #[test] - // fn fetch_musicbrainz() { - // let mut mb_api = MockIMusicBrainz::new(); - - // let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); - // let album_1 = COLLECTION[1].albums[0].meta.clone(); - // let album_4 = COLLECTION[1].albums[3].meta.clone(); - - // let album_match_1_1 = Match::new(100, album_1.clone()); - // let album_match_1_2 = Match::new(50, album_4.clone()); - // let album_match_4_1 = Match::new(100, album_4.clone()); - // let album_match_4_2 = Match::new(30, album_1.clone()); - // let matches_1 = vec![album_match_1_1.clone(), album_match_1_2.clone()]; - // let matches_4 = vec![album_match_4_1.clone(), album_match_4_2.clone()]; - - // let result_1: Result>, musicbrainz::Error> = Ok(matches_1.clone()); - // let result_4: Result>, musicbrainz::Error> = Ok(matches_4.clone()); - - // // Other albums have an MBID and so they will be skipped. - // let mut seq = Sequence::new(); - - // mb_api - // .expect_search_release_group() - // .with(predicate::eq(arid.clone()), predicate::eq(album_1.clone())) - // .times(1) - // .in_sequence(&mut seq) - // .return_once(|_, _| result_1); - // mb_api - // .expect_search_release_group() - // .with(predicate::eq(arid.clone()), predicate::eq(album_4.clone())) - // .times(1) - // .in_sequence(&mut seq) - // .return_once(|_, _| result_4); - - // let browse = AppMachine::browse(inner_with_mb(music_hoard(COLLECTION.to_owned()), mb_api)); - - // // Use the second artist for this test. - // let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - // let mut app = browse.fetch_musicbrainz(); - - // let public = app.get(); - // assert!(matches!(public.state, AppState::Matches(_))); - - // let public_matches = public.state.unwrap_matches(); - - // let mut matches_1: Vec> = - // matches_1.into_iter().map(Into::into).collect(); - // matches_1.push(MatchOption::CannotHaveMbid); - // let expected = Some(AppMatchesInfo::Album(AppAlbumMatches { - // matching: album_1.clone(), - // list: matches_1.clone(), - // })); - // assert_eq!(public_matches.matches, expected.as_ref()); - - // let mut app = app.unwrap_matches().select(); - - // let public = app.get(); - // assert!(matches!(public.state, AppState::Matches(_))); - - // let public_matches = public.state.unwrap_matches(); - - // let mut matches_4: Vec> = - // matches_4.into_iter().map(Into::into).collect(); - // matches_4.push(MatchOption::CannotHaveMbid); - // let expected = Some(AppMatchesInfo::Album(AppAlbumMatches { - // matching: album_4.clone(), - // list: matches_4.clone(), - // })); - // assert_eq!(public_matches.matches, expected.as_ref()); - - // let app = app.unwrap_matches().select(); - // app.unwrap_browse(); - // } - - // #[test] - // fn fetch_musicbrainz_no_artist() { - // let browse = AppMachine::browse(inner(music_hoard(vec![]))); - // let app = browse.fetch_musicbrainz(); - // app.unwrap_error(); - // } - - // #[test] - // fn fetch_musicbrainz_no_artist_mbid() { - // let mut mb_api = MockIMusicBrainz::new(); - - // let artist = COLLECTION[3].meta.clone(); - - // let artist_match_1 = Match::new(100, artist.clone()); - // let artist_match_2 = Match::new(50, artist.clone()); - // let matches = vec![artist_match_1.clone(), artist_match_2.clone()]; - - // let result: Result>, musicbrainz::Error> = Ok(matches.clone()); - - // mb_api - // .expect_search_artist() - // .with(predicate::eq(artist.clone())) - // .times(1) - // .return_once(|_| result); - - // let browse = AppMachine::browse(inner_with_mb(music_hoard(COLLECTION.to_owned()), mb_api)); - - // // Use the fourth artist for this test as they have no MBID. - // let browse = browse.increment_selection(Delta::Line).unwrap_browse(); - // 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 public = app.get(); - // assert!(matches!(public.state, AppState::Matches(_))); - - // let public_matches = public.state.unwrap_matches(); - - // let mut matches: Vec> = - // matches.into_iter().map(Into::into).collect(); - // matches.push(MatchOption::CannotHaveMbid); - // let expected = Some(AppMatchesInfo::Artist(AppArtistMatches { - // matching: artist.clone(), - // list: matches.clone(), - // })); - // assert_eq!(public_matches.matches, expected.as_ref()); - - // let app = app.unwrap_matches().select(); - // app.unwrap_browse(); - // } - // #[test] // fn fetch_musicbrainz_artist_api_error() { // let mut mb_api = MockIMusicBrainz::new(); @@ -368,44 +158,6 @@ mod tests { // app.unwrap_error(); // } - // #[test] - // fn fetch_musicbrainz_artist_receiver_disconnect() { - // let (tx, rx) = mpsc::channel::(); - // drop(rx); - - // let mut mb_api = MockIMusicBrainz::new(); - - // mb_api - // .expect_search_artist() - // .times(1) - // .return_once(|_| Ok(vec![])); - - // // We only check it does not panic and that it doesn't call the API more than once. - // AppMachine::fetch_artist(Arc::new(Mutex::new(mb_api)), tx, COLLECTION[3].meta.clone()); - // } - - // #[test] - // fn fetch_musicbrainz_albums_receiver_disconnect() { - // let (tx, rx) = mpsc::channel::(); - // drop(rx); - - // let mut mb_api = MockIMusicBrainz::new(); - - // mb_api - // .expect_search_release_group() - // .times(1) - // .return_once(|_, _| Ok(vec![])); - - // // We only check it does not panic and that it doesn't call the API more than once. - // let mbref = &COLLECTION[1].meta.musicbrainz; - // let albums = &COLLECTION[1].albums; - // AppMachine::fetch_albums( - // Arc::new(Mutex::new(mb_api)), - // tx, - // mbref.as_ref().unwrap().mbid().clone(), - // albums.iter().map(|a| &a.meta).cloned().collect(), - // ); - // } // impl Match { // pub fn new(score: u8, item: T) -> Self { diff --git a/src/tui/event.rs b/src/tui/event.rs index 6f34116..95429a6 100644 --- a/src/tui/event.rs +++ b/src/tui/event.rs @@ -33,7 +33,13 @@ impl From for EventError { } } -#[derive(Clone, Copy, Debug)] +impl From for EventError { + fn from(_: mpsc::TryRecvError) -> EventError { + EventError::Recv + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Event { Key(KeyEvent), FetchResultReady, @@ -82,6 +88,11 @@ impl EventReceiver { pub fn recv(&self) -> Result { Ok(self.receiver.recv()?) } + + #[allow(dead_code)] + pub fn try_recv(&self) -> Result { + Ok(self.receiver.try_recv()?) + } } #[cfg(test)] @@ -123,6 +134,25 @@ mod tests { assert!(result.is_err()); } + #[test] + fn event_receiver_try() { + let channel = EventChannel::new(); + let sender = channel.sender(); + let receiver = channel.receiver(); + let event = Event::FetchResultReady; + + let result = receiver.try_recv(); + assert!(result.is_err()); + + sender.send(event).unwrap(); + let result = receiver.try_recv(); + assert!(result.is_ok()); + + drop(sender); + let result = receiver.try_recv(); + assert!(result.is_err()); + } + #[test] fn errors() { let send_err = EventError::Send(Event::Key(KeyEvent {