The allowance: the rolling thinking window and the daily voice cap.
Runs every allowance law.
19pub fn allowance<A: Allowance>(fresh: &impl Fn() -> A, restart: &impl Fn(A) -> A) { 20 info!("conformance: allowance"); 21 thinking_has_room_until_the_window_is_spent(fresh()); 22 thinking_spending_ages_out_of_a_rolling_window(fresh()); 23 thinking_a_changed_limit_applies_at_once(fresh()); 24 thinking_usage_says_when_room_returns(fresh()); 25 thinking_spending_survives_a_restart(fresh, restart); 26 voice_a_line_that_would_pass_the_cap_is_refused(fresh()); 27 voice_a_reservation_counts_before_it_is_settled(fresh()); 28 voice_a_line_that_did_not_come_out_is_given_back_and_remembered(fresh()); 29 voice_a_line_that_was_spoken_stays_spent_and_clears_the_last_failure(fresh()); 30 voice_a_new_day_is_a_fresh_allowance(fresh()); 31 voice_spending_survives_a_restart(fresh, restart); 32}
34pub fn thinking_has_room_until_the_window_is_spent<A: Allowance>(a: A) { 35 let l = limit(1000, 5); 36 assert_eq!(run_ready(a.thinking_room(at(0), &l)).unwrap(), ThinkingRoom::Room); 37 run_ready(a.charge_thinking(at(0), Tokens::new(600))).unwrap(); 38 assert_eq!(run_ready(a.thinking_room(at(1), &l)).unwrap(), ThinkingRoom::Room, "600 of 1000"); 39 run_ready(a.charge_thinking(at(1), Tokens::new(600))).unwrap(); 40 assert!(matches!(run_ready(a.thinking_room(at(2), &l)).unwrap(), ThinkingRoom::Spent { .. }), "1200 of 1000: asked first, charged after, so the last may overshoot"); 41 assert_eq!(run_ready(a.thinking_room(at(2), &TokenLimit::new(None, 5).unwrap())).unwrap(), ThinkingRoom::Room, "no limit at all"); 42} 43 44pub fn thinking_spending_ages_out_of_a_rolling_window<A: Allowance>(a: A) { 45 let l = limit(1000, 5); 46 run_ready(a.charge_thinking(at(0), Tokens::new(1000))).unwrap(); 47 assert_eq!(run_ready(a.thinking_room(at(4 * H), &l)).unwrap(), ThinkingRoom::Spent { frees_up_at: at(5 * H) }); 48 assert_eq!(run_ready(a.thinking_room(at(5 * H + 1), &l)).unwrap(), ThinkingRoom::Room, "a rolling window, not a clock day"); 49 assert_eq!(run_ready(a.thinking_usage(at(5 * H + 1), &l)).unwrap().used, Tokens::new(0)); 50} 51 52pub fn thinking_a_changed_limit_applies_at_once<A: Allowance>(a: A) { 53 run_ready(a.charge_thinking(at(0), Tokens::new(900))).unwrap(); 54 assert!(matches!(run_ready(a.thinking_room(at(H), &limit(500, 5))).unwrap(), ThinkingRoom::Spent { .. })); 55 assert_eq!(run_ready(a.thinking_room(at(H), &limit(5000, 5))).unwrap(), ThinkingRoom::Room); 56 assert_eq!(run_ready(a.thinking_room(at(3 * H), &limit(500, 2))).unwrap(), ThinkingRoom::Room, "a shorter window forgets the spending sooner"); 57} 58 59pub fn thinking_usage_says_when_room_returns<A: Allowance>(a: A) { 60 let l = limit(1000, 5); 61 let u = run_ready(a.thinking_usage(at(0), &l)).unwrap(); 62 assert_eq!((u.used, u.limit, u.window_hours, u.frees_up_at), (Tokens::new(0), Some(Tokens::new(1000)), 5, None)); 63 run_ready(a.charge_thinking(at(2 * H), Tokens::new(300))).unwrap(); 64 run_ready(a.charge_thinking(at(3 * H), Tokens::new(300))).unwrap(); 65 let u = run_ready(a.thinking_usage(at(4 * H), &l)).unwrap(); 66 assert_eq!((u.used, u.frees_up_at), (Tokens::new(600), Some(at(7 * H))), "when the oldest spending leaves"); 67} 68 69pub fn thinking_spending_survives_a_restart<A: Allowance>(fresh: &impl Fn() -> A, restart: &impl Fn(A) -> A) { 70 let a = fresh(); 71 run_ready(a.charge_thinking(at(10), Tokens::new(700))).unwrap(); 72 let a = restart(a); 73 assert_eq!(run_ready(a.thinking_usage(at(20), &limit(1000, 5))).unwrap().used, Tokens::new(700), "a restart must not grant a fresh allowance"); 74} 75 76pub fn voice_a_line_that_would_pass_the_cap_is_refused<A: Allowance>(a: A) { 77 let now = at(DAY * 7 + 1000); 78 let r = run_ready(a.reserve_voice(now, 6, 10)).unwrap(); 79 run_ready(a.settle_voice(r, Settlement::Spoken)).unwrap(); 80 match run_ready(a.reserve_voice(at(DAY * 7 + 2000), 6, 10)) { 81 Err(ReserveError::DailyCap { spent, cap, frees_up_at }) => { 82 assert_eq!((spent, cap), (6, 10)); 83 assert_eq!(frees_up_at, Day::containing(now).ends_at(), "a spent day frees up when the next one begins"); 84 } 85 other => panic!("expected the daily cap, got {other:?}"), 86 } 87 let r = run_ready(a.reserve_voice(at(DAY * 7 + 3000), 4, 10)).expect("exactly the cap is allowed"); 88 run_ready(a.settle_voice(r, Settlement::Spoken)).unwrap(); 89} 90 91pub fn voice_a_reservation_counts_before_it_is_settled<A: Allowance>(a: A) { 92 // Two lines asked for at once must not both pass a cap with room for one. 93 let first = run_ready(a.reserve_voice(at(1000), 6, 10)).unwrap(); 94 assert!(matches!(run_ready(a.reserve_voice(at(1001), 6, 10)), Err(ReserveError::DailyCap { .. })), "the first is still outstanding"); 95 assert_eq!(run_ready(a.voice_spend(at(1002))).unwrap().spent_today, 6, "and is already spent"); 96 run_ready(a.settle_voice(first, Settlement::Spoken)).unwrap(); 97} 98 99pub fn voice_a_line_that_did_not_come_out_is_given_back_and_remembered<A: Allowance>(a: A) { 100 let r = run_ready(a.reserve_voice(at(1000), 6, 10)).unwrap(); 101 let why = SpeakError::Unreachable(Diagnostic::new("the voice is down")); 102 run_ready(a.settle_voice(r, Settlement::NotSpoken(why.clone()))).unwrap(); 103 let spend = run_ready(a.voice_spend(at(1001))).unwrap(); 104 assert_eq!(spend.spent_today, 0, "the characters came back"); 105 assert_eq!(spend.last_failure, Some(why), "and the parents can see why the voice fell silent"); 106 assert!(run_ready(a.reserve_voice(at(1002), 6, 10)).is_ok(), "so the same line can be tried again"); 107} 108 109pub fn voice_a_line_that_was_spoken_stays_spent_and_clears_the_last_failure<A: Allowance>(a: A) { 110 let r = run_ready(a.reserve_voice(at(1000), 3, 10)).unwrap(); 111 run_ready(a.settle_voice(r, Settlement::NotSpoken(SpeakError::TooLong))).unwrap(); 112 assert!(run_ready(a.voice_spend(at(1001))).unwrap().last_failure.is_some()); 113 let r = run_ready(a.reserve_voice(at(1002), 4, 10)).unwrap(); 114 run_ready(a.settle_voice(r, Settlement::Spoken)).unwrap(); 115 let spend = run_ready(a.voice_spend(at(1003))).unwrap(); 116 assert_eq!((spend.spent_today, spend.last_failure), (4, None), "one that comes out clears the last failure"); 117} 118 119pub fn voice_a_new_day_is_a_fresh_allowance<A: Allowance>(a: A) { 120 let today = at(DAY * 3 + 5); 121 let r = run_ready(a.reserve_voice(today, 10, 10)).unwrap(); 122 run_ready(a.settle_voice(r, Settlement::Spoken)).unwrap(); 123 assert!(run_ready(a.reserve_voice(at(DAY * 3 + 6), 1, 10)).is_err()); 124 assert_eq!(run_ready(a.voice_spend(at(DAY * 4))).unwrap().spent_today, 0, "the screen shows the day it is asked about"); 125 let r = run_ready(a.reserve_voice(at(DAY * 4), 10, 10)).expect("tomorrow starts from zero"); 126 run_ready(a.settle_voice(r, Settlement::Spoken)).unwrap(); 127} 128 129pub fn voice_spending_survives_a_restart<A: Allowance>(fresh: &impl Fn() -> A, restart: &impl Fn(A) -> A) { 130 let a = fresh(); 131 let r = run_ready(a.reserve_voice(at(1000), 6, 10)).unwrap(); 132 run_ready(a.settle_voice(r, Settlement::Spoken)).unwrap(); 133 let a = restart(a); 134 assert_eq!(run_ready(a.voice_spend(at(1001))).unwrap().spent_today, 6); 135 assert!( 136 matches!(run_ready(a.reserve_voice(at(1002), 6, 10)), Err(ReserveError::DailyCap { .. })), 137 "a restart (or a rebuild) must not grant the day's allowance twice" 138 ); 139}