diff --git a/koucha/src/fetch.rs b/koucha/src/fetch.rs index 62e4505..532b4fe 100644 --- a/koucha/src/fetch.rs +++ b/koucha/src/fetch.rs @@ -93,9 +93,9 @@ mod tests { use super::*; use crate::test_utils::{ ITEM_TITLE, ITEM_GUID, ITEM_GUID2, ITEM_DESC, ITEM_CONT, - CHANNEL_TITLE, CHANNEL_DESC, FEED1 + CHANNEL_TITLE, CHANNEL_DESC, FEED1, + get_datetime }; - use chrono::TimeZone; fn create_guid(value: String) -> rss::Guid { rss::Guid { value, permalink: false } @@ -140,7 +140,7 @@ mod tests { let rss_channel = create_channel([rss_item, rss_item2].to_vec()); - let date: DateTime = Utc.with_ymd_and_hms(2020,1,1,0,0,0).unwrap(); + let date: DateTime = get_datetime(); let channel = FetchedRSSChannel::parse(rss_channel, date).unwrap(); diff --git a/koucha/src/score.rs b/koucha/src/score.rs index d8ff046..4cdac60 100644 --- a/koucha/src/score.rs +++ b/koucha/src/score.rs @@ -54,9 +54,19 @@ pub enum Score { impl Score { pub fn new() -> DecayingScore { + Self::new_with_initial(default::INITIAL_SCORE) + } + + pub fn new_with_initial(initial_score: i64) -> DecayingScore { + Self::new_with_initial_and_time(initial_score, Utc::now()) + } + + pub fn new_with_initial_and_time( + initial: i64, time: DateTime + ) -> DecayingScore { DecayingScore { - value: default::INITIAL_SCORE, - last_updated: Utc::now(), + value: initial, + last_updated: time, } } @@ -68,12 +78,20 @@ impl Score { } pub fn update_score(self, gravity: Gravity) -> Self { + self.update_score_at_time(gravity, Utc::now()) + } + + fn update_score_at_time(self, gravity: Gravity, time: DateTime) -> Self { match self { - Self::Decaying(d) => Score::Decaying(d.update_score(gravity)), + Self::Decaying(d) => Score::Decaying( + d.apply_gravity_to_time(gravity, time) + ), Self::Boosted(b) => { - let try_unfrozen = b.try_unfreeze(); + let try_unfrozen = b.try_unfreeze_at_time(time); match try_unfrozen { - Self::Decaying(s) => Score::Decaying(s.update_score(gravity)), + Self::Decaying(s) => Score::Decaying( + s.apply_gravity_to_time(gravity, time) + ), Self::Boosted(b) => Score::Boosted(b), } } @@ -110,16 +128,12 @@ impl DecayingScore { self.value } - fn update_score(self, gravity: Gravity) -> Self { - self.update_score_at_time(gravity, Utc::now()) - } - - fn update_score_at_time( + fn apply_gravity_to_time( self, gravity: Gravity, update_time: DateTime ) -> Self { let elapsed_time: TimeDelta = update_time.signed_duration_since(self.last_updated); let new_value = if elapsed_time <= TimeDelta::zero() { self.value } else { - i64::from(gravity) * (elapsed_time.num_seconds() / SECONDS_IN_A_DAY) + self.value + i64::from(gravity) * (elapsed_time.num_seconds() / SECONDS_IN_A_DAY) }; Self { last_updated: update_time, @@ -151,13 +165,9 @@ impl BoostedScore { } } - fn try_unfreeze(self) -> Score { - self.try_unfreeze_at_time(Utc::now()) - } - fn try_unfreeze_at_time(self, update_time: DateTime) -> Score { let boost_end = self.boosted_at + TimeDelta::seconds(default::BOOST_FREEZE_IN_SECONDS); - if boost_end > update_time { + if boost_end < update_time { Score::Decaying(DecayingScore { value: self.value, last_updated: boost_end, @@ -167,3 +177,210 @@ impl BoostedScore { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::get_datetime; + + #[test] + fn gravity_default() { + let gravity = Gravity::new(None); + assert_eq!(i64::from(gravity), default::GRAVITY); + } + + #[test] + fn boost_default() { + let boost = Boost::new(None); + assert_eq!(i64::from(boost), default::BOOST); + } + + // "Score" Tests + + #[test] + fn parse_decaying() { + let ups = UnparsedScore { + value: 10, + last_updated: get_datetime(), + last_boosted: None, + }; + + ups.parse().get_decaying().unwrap(); + } + + #[test] + fn parse_boosted() { + let dt = get_datetime(); + let ups = UnparsedScore { + value: 10, + last_updated: dt, + last_boosted: Some(dt), + }; + + ups.parse().get_boosted().unwrap(); + } + + #[test] + fn new() { + let score = Score::new(); + assert_eq!(score.value, default::INITIAL_SCORE); + } + + #[test] + fn new_with_values() { + let dt = get_datetime(); + let score = Score::new_with_initial_and_time(10, dt); + assert_eq!(score.value, 10); + assert_eq!(score.last_updated, dt); + } + + #[test] + fn update_score_stays_decaying() { + let dt = get_datetime(); + let score = Score::Decaying(Score::new_with_initial_and_time(10, dt)); + let gravity = Gravity::new(None); + + let dt2 = dt + TimeDelta::seconds(SECONDS_IN_A_DAY); + + score.update_score_at_time(gravity, dt2).get_decaying().unwrap(); + } + + #[test] + fn update_score_stays_frozen() { + let dt = get_datetime(); + let score = Score::Boosted(BoostedScore { value: 10, boosted_at: dt }); + let gravity = Gravity::new(None); + + let dt2 = dt + TimeDelta::seconds(default::BOOST_FREEZE_IN_SECONDS); + + score.update_score_at_time(gravity, dt2).get_boosted().unwrap(); + } + + #[test] + fn update_score_thaws_and_decays() { + let dt = get_datetime(); + let score = Score::Boosted(BoostedScore { value: 10, boosted_at: dt }); + let gravity = Gravity::new(None); + + let dt2 = dt + TimeDelta::seconds( + default::BOOST_FREEZE_IN_SECONDS + SECONDS_IN_A_DAY + ); + + let updated = score.update_score_at_time(gravity, dt2) + .get_decaying().unwrap(); + assert!(updated.value < 10) + } + + #[test] + fn get_decaying_success() { + let dt = get_datetime(); + let score = Score::Decaying(Score::new_with_initial_and_time(10, dt)); + + score.get_decaying().unwrap(); + } + + #[test] + #[should_panic = "Attempted to get_boosted() of a decaying score"] + fn get_boosted_failure() { + let dt = get_datetime(); + let score = Score::Decaying(Score::new_with_initial_and_time(10, dt)); + + score.get_boosted().unwrap(); + } + + #[test] + #[should_panic = "Attempted to get_decaying() of a boosted score"] + fn get_decaying_failure() { + let dt = get_datetime(); + let boost = Boost::new(None); + let score = Score::Boosted( + Score::new_with_initial_and_time(10, dt).boost_at_time(boost, dt) + ); + + score.get_decaying().unwrap(); + } + + #[test] + fn get_boosted_success() { + let dt = get_datetime(); + let boost = Boost::new(None); + let score = Score::Boosted( + Score::new_with_initial_and_time(10, dt).boost_at_time(boost, dt) + ); + + score.get_boosted().unwrap(); + } + + // "DecayingScore" Tests + + #[test] + fn apply_gravity_to_future() { + let dt = get_datetime(); + let score = DecayingScore { value: 10, last_updated: dt }; + let future = dt + TimeDelta::seconds(SECONDS_IN_A_DAY); + let gravity = Gravity::new(None); + + let updated = score.apply_gravity_to_time(gravity, future); + + assert_eq!(updated.value, 10 + default::GRAVITY); + assert_eq!(updated.last_updated, future); + } + + #[test] + fn apply_gravity_to_past() { + let dt = get_datetime(); + let score = DecayingScore { value: 10, last_updated: dt }; + let past = dt - TimeDelta::seconds(SECONDS_IN_A_DAY); + let gravity = Gravity::new(None); + + let updated = score.apply_gravity_to_time(gravity, past); + + assert_eq!(updated.value, 10); + assert_eq!(updated.last_updated, past); + } + + #[test] + fn boost() { + let dt = get_datetime(); + let score = DecayingScore { value: 10, last_updated: dt }; + let boost = Boost::new(None); + + let boosted = score.boost_at_time(boost, dt); + assert_eq!(boosted.value, 10 + default::BOOST); + assert_eq!(boosted.boosted_at, dt); + } + + // "BoostedScore" tests + + #[test] + fn unboost() { + let dt = get_datetime(); + let score = DecayingScore { value: 10, last_updated: dt }; + let boost = Boost::new(None); + let boosted = score.boost_at_time(boost, dt); + + let unboosted = boosted.unboost(boost); + assert_eq!(unboosted.value, 10); + assert_eq!(unboosted.last_updated, dt); + } + + #[test] + fn boosted_stays_frozen() { + let dt = get_datetime(); + let score = BoostedScore { value: 10, boosted_at: dt }; + + let last_second = dt + TimeDelta::seconds(default::BOOST_FREEZE_IN_SECONDS); + + score.try_unfreeze_at_time(last_second).get_boosted().unwrap(); + } + + #[test] + fn boosted_thaws() { + let dt = get_datetime(); + let score = BoostedScore { value: 10, boosted_at: dt }; + + let first_second = dt + TimeDelta::days(default::BOOST_FREEZE_IN_SECONDS+1); + + score.try_unfreeze_at_time(first_second).get_decaying().unwrap(); + } +} diff --git a/koucha/src/test_utils.rs b/koucha/src/test_utils.rs index 5f2cb9b..870a257 100644 --- a/koucha/src/test_utils.rs +++ b/koucha/src/test_utils.rs @@ -11,6 +11,11 @@ use crate::{ } }; use reqwest::Url; +use chrono::{ + Utc, + TimeZone, + DateTime +}; pub const FEED1: &str = "https://example.com/feed"; pub const FEED2: &str = "https://example2.com/feed"; @@ -26,6 +31,11 @@ pub const ITEM_TITLE: &str = "My Item!"; pub const ITEM_DESC: &str = "My Item's description"; pub const ITEM_CONT: &str = "The content of my Item"; + +pub fn get_datetime() -> DateTime { + Utc.with_ymd_and_hms(2020,1,1,0,0,0).unwrap() +} + pub async fn setup_adapter() -> Adapter { AdapterBuilder::new() .database_url("sqlite::memory:")