post-push-party 0.1.9

Push code, earn points, throw a party!
use super::{BonusTrack, PushContext, Reward, Tier};

/// bonus for pushing multiple times in quick succession
pub struct RapidFire;

/// time window in seconds for rapid fire bonus
const RAPID_FIRE_WINDOW_SECS: u64 = 15 * 60;

static TIERS: &[Tier] = &[
    Tier {
        cost: 50,
        reward: Reward::Multiplier(2),
    },
    Tier {
        cost: 500,
        reward: Reward::Multiplier(3),
    },
    Tier {
        cost: 3000,
        reward: Reward::Multiplier(4),
    },
    Tier {
        cost: 20000,
        reward: Reward::Multiplier(5),
    },
    Tier {
        cost: 120000,
        reward: Reward::Multiplier(6),
    },
];

impl BonusTrack for RapidFire {
    fn id(&self) -> &'static str {
        "rapid_fire"
    }

    fn name(&self) -> &'static str {
        "Rapid Fire"
    }

    fn description(&self) -> &'static str {
        "Multiplier for pushing twice within 15 minutes."
    }

    fn tiers(&self) -> &'static [Tier] {
        TIERS
    }

    fn applies(&self, ctx: &PushContext) -> u32 {
        if ctx.push.commits().is_empty() {
            return 0;
        }

        let cutoff = ctx.clock.now().saturating_sub(RAPID_FIRE_WINDOW_SECS);
        let has_recent_push = ctx.history.count_since(cutoff).unwrap_or_default() != 0;
        if has_recent_push { 1 } else { 0 }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        bonus_track::Clock,
        git::{Commit, Push},
        storage::{DbConnection, PushEntry, PushHistory},
    };

    #[test]
    fn applies_when_pushed_within_window() {
        let conn = DbConnection::create_in_memory().unwrap();

        let bonus = RapidFire;
        let push = Push::new(vec![Commit::default()]);
        let history = PushHistory::new(&conn).with_entries([PushEntry::at(1000)]);
        let clock = Clock::at(1000 + 5 * 60);

        let ctx = PushContext {
            push: &push,
            history: &history,
            clock: &clock,
        };

        assert_eq!(bonus.applies(&ctx), 1);
    }

    #[test]
    fn applies_at_exact_boundary() {
        let conn = DbConnection::create_in_memory().unwrap();

        let bonus = RapidFire;
        let push = Push::new(vec![Commit::default()]);
        let history = PushHistory::new(&conn).with_entries([PushEntry::at(1000)]);
        let clock = Clock::at(1000 + RAPID_FIRE_WINDOW_SECS);

        let ctx = PushContext {
            push: &push,
            history: &history,
            clock: &clock,
        };

        assert_eq!(bonus.applies(&ctx), 1);
    }

    #[test]
    fn does_not_apply_outside_window() {
        let conn = DbConnection::create_in_memory().unwrap();

        let bonus = RapidFire;
        let push = Push::new(vec![Commit::default()]);
        let history = PushHistory::new(&conn).with_entries([PushEntry::at(1000)]);
        let clock = Clock::at(1000 + RAPID_FIRE_WINDOW_SECS + 60);

        let ctx = PushContext {
            push: &push,
            history: &history,
            clock: &clock,
        };

        assert_eq!(bonus.applies(&ctx), 0);
    }

    #[test]
    fn does_not_apply_with_no_history() {
        let conn = DbConnection::create_in_memory().unwrap();

        let bonus = RapidFire;
        let push = Push::new(vec![Commit::default()]);
        let history = PushHistory::new(&conn);
        let clock = Clock::at(1000);

        let ctx = PushContext {
            push: &push,
            history: &history,
            clock: &clock,
        };

        assert_eq!(bonus.applies(&ctx), 0);
    }

    #[test]
    fn does_not_apply_to_empty_pushes() {
        let conn = DbConnection::create_in_memory().unwrap();

        let bonus = RapidFire;
        let push = Push::new(vec![]);
        let history = PushHistory::new(&conn).with_entries([PushEntry::at(1000)]);
        let clock = Clock::at(1000 + 5 * 60);

        let ctx = PushContext {
            push: &push,
            history: &history,
            clock: &clock,
        };

        assert_eq!(bonus.applies(&ctx), 0);
    }
}