//! Tests for handover decisions. use apsis_handover::{ Candidate, HandoverDecision, HandoverErrorKind, HandoverPolicy, HandoverRecord, HandoverTracker, decide, decide_with_history, }; /// The default policy: minimum quality 0.2, margin 0.1, dwell 5 s. fn policy() -> HandoverPolicy { HandoverPolicy::default() } /// A candidate with the given identifier and quality. fn candidate(satellite: usize, quality: f64) -> Candidate { Candidate::new(satellite, quality, 100.0) } #[test] fn the_default_policy_has_the_documented_values() { let policy = policy(); assert_eq!(policy.minimum_quality, 0.2); assert_eq!(policy.handover_margin, 0.1); assert_eq!(policy.minimum_dwell_seconds, 5.0); assert!(policy.is_valid()); } #[test] fn a_policy_is_validated() { assert!(HandoverPolicy::new(0.2, 0.0, 0.0).is_valid()); // A negative margin would make a handover worthwhile to a worse // candidate. assert!(!HandoverPolicy::new(0.2, -0.1, 5.0).is_valid()); assert!(!HandoverPolicy::new(0.2, 0.1, -1.0).is_valid()); assert!(!HandoverPolicy::new(f64::NAN, 0.1, 5.0).is_valid()); assert_eq!( HandoverPolicy::new(0.2, -0.1, 5.0) .validate() .expect_err("negative margin") .kind(), HandoverErrorKind::InvalidPolicy ); } #[test] fn the_best_candidate_has_the_greatest_quality() { let candidates = [candidate(1, 0.3), candidate(2, 0.9), candidate(3, 0.5)]; let best = policy().best(&candidates).expect("non-empty"); assert_eq!(best.satellite, 2); } #[test] fn ties_are_broken_by_the_lower_identifier() { let candidates = [candidate(5, 0.7), candidate(2, 0.7), candidate(9, 0.7)]; let best = policy().best(&candidates).expect("non-empty"); assert_eq!(best.satellite, 2); } #[test] fn a_terminal_with_no_server_acquires_the_best_candidate() { let candidates = [candidate(1, 0.5), candidate(2, 0.8)]; let decision = decide(&policy(), None, &candidates).expect("valid"); assert_eq!(decision, HandoverDecision::Acquire { satellite: 2 }); assert!(decision.is_change()); assert_eq!(decision.resulting_satellite(), Some(2)); } #[test] fn a_terminal_with_no_server_drops_when_nothing_is_good_enough() { let candidates = [candidate(1, 0.1), candidate(2, 0.15)]; let decision = decide(&policy(), None, &candidates).expect("valid"); assert_eq!(decision, HandoverDecision::Drop); assert!(!decision.is_change()); assert_eq!(decision.resulting_satellite(), None); } #[test] fn a_good_current_server_is_kept() { // The alternative is better, but by less than the margin. let candidates = [candidate(1, 0.80), candidate(2, 0.85)]; let decision = decide(&policy(), Some(1), &candidates).expect("valid"); assert_eq!(decision, HandoverDecision::Stay); } #[test] fn a_clearly_better_candidate_causes_a_handover() { // The alternative beats the current server by more than the margin. let candidates = [candidate(1, 0.50), candidate(2, 0.80)]; let decision = decide(&policy(), Some(1), &candidates).expect("valid"); assert_eq!( decision, HandoverDecision::HandOver { from: 1, to: 2, from_quality: 0.50, to_quality: 0.80, } ); assert_eq!(decision.resulting_satellite(), Some(2)); } #[test] fn the_margin_is_the_threshold_for_a_handover() { let policy = policy(); // Exactly at the margin is not enough, because the comparison is // strict. let at_margin = [candidate(1, 0.5), candidate(2, 0.6)]; assert_eq!( decide(&policy, Some(1), &at_margin).expect("valid"), HandoverDecision::Stay ); // Just above the margin is enough. let above = [candidate(1, 0.5), candidate(2, 0.600_1)]; assert!(decide(&policy, Some(1), &above).expect("valid").is_change()); } #[test] fn a_zero_margin_hands_over_to_any_better_candidate() { let policy = HandoverPolicy::new(0.2, 0.0, 0.0); let candidates = [candidate(1, 0.5), candidate(2, 0.500_1)]; assert!( decide(&policy, Some(1), &candidates) .expect("valid") .is_change() ); } #[test] fn a_current_server_below_the_minimum_is_abandoned() { let candidates = [candidate(1, 0.1), candidate(2, 0.3)]; let decision = decide(&policy(), Some(1), &candidates).expect("valid"); assert_eq!( decision, HandoverDecision::HandOver { from: 1, to: 2, from_quality: 0.1, to_quality: 0.3, } ); } #[test] fn a_weak_terminal_drops_when_no_candidate_meets_the_minimum() { let candidates = [candidate(1, 0.1), candidate(2, 0.15)]; let decision = decide(&policy(), Some(1), &candidates).expect("valid"); assert_eq!(decision, HandoverDecision::Drop); } #[test] fn a_current_server_that_disappears_causes_a_handover() { // The current server is no longer in the candidate list, which means // its access window has ended. let candidates = [candidate(2, 0.6), candidate(3, 0.4)]; let decision = decide(&policy(), Some(1), &candidates).expect("valid"); assert_eq!( decision, HandoverDecision::HandOver { from: 1, to: 2, from_quality: 0.0, to_quality: 0.6, } ); } #[test] fn a_current_server_that_disappears_and_nothing_replaces_it_drops() { let candidates = [candidate(2, 0.1)]; let decision = decide(&policy(), Some(1), &candidates).expect("valid"); assert_eq!(decision, HandoverDecision::Drop); } #[test] fn the_only_candidate_being_the_current_server_keeps_it() { let candidates = [candidate(1, 0.5)]; let decision = decide(&policy(), Some(1), &candidates).expect("valid"); assert_eq!(decision, HandoverDecision::Stay); } #[test] fn a_weak_current_server_with_no_alternative_drops() { // The current server is below the minimum and is the only candidate, so // there is nowhere to move and the service is lost. let candidates = [candidate(1, 0.1)]; let decision = decide(&policy(), Some(1), &candidates).expect("valid"); assert_eq!(decision, HandoverDecision::Drop); } #[test] fn the_dwell_time_refuses_an_opportunistic_handover() { let candidates = [candidate(1, 0.5), candidate(2, 0.9)]; // The last handover was one second ago, which is inside the five second // dwell time, so the better candidate is not taken. let decision = decide_with_history(&policy(), Some(1), Some(9.0), &candidates, 10.0).expect("valid"); assert_eq!(decision, HandoverDecision::Stay); // After the dwell time the handover proceeds. let decision = decide_with_history(&policy(), Some(1), Some(1.0), &candidates, 10.0).expect("valid"); assert!(decision.is_change()); } #[test] fn the_dwell_time_does_not_block_a_forced_handover() { // The current server has fallen below the minimum, so the handover must // happen even though the dwell time has not elapsed: refusing it would // drop the link. let candidates = [candidate(1, 0.1), candidate(2, 0.9)]; let decision = decide_with_history(&policy(), Some(1), Some(9.9), &candidates, 10.0).expect("valid"); assert!(decision.is_change()); assert_eq!(decision.resulting_satellite(), Some(2)); } #[test] fn the_dwell_time_does_not_block_an_acquisition() { let candidates = [candidate(1, 0.9)]; let decision = decide_with_history(&policy(), None, Some(9.9), &candidates, 10.0).expect("valid"); assert_eq!(decision, HandoverDecision::Acquire { satellite: 1 }); } #[test] fn the_dwell_time_does_not_block_a_handover_for_a_lost_server() { let candidates = [candidate(2, 0.9)]; let decision = decide_with_history(&policy(), Some(1), Some(9.9), &candidates, 10.0).expect("valid"); assert!(decision.is_change()); } #[test] fn a_zero_dwell_time_always_permits_a_handover() { let policy = HandoverPolicy::new(0.2, 0.1, 0.0); let candidates = [candidate(1, 0.5), candidate(2, 0.9)]; let decision = decide_with_history(&policy, Some(1), Some(10.0), &candidates, 10.0).expect("valid"); assert!(decision.is_change()); } #[test] fn invalid_candidates_are_rejected() { let empty: [Candidate; 0] = []; assert_eq!( decide(&policy(), None, &empty) .expect_err("no candidates") .kind(), HandoverErrorKind::NoCandidates ); let duplicated = [candidate(1, 0.5), candidate(1, 0.6)]; assert_eq!( decide(&policy(), None, &duplicated) .expect_err("a duplicate") .kind(), HandoverErrorKind::DuplicateSatellite ); let not_finite = [Candidate::new(1, f64::NAN, 100.0)]; assert_eq!( decide(&policy(), None, ¬_finite) .expect_err("not finite") .kind(), HandoverErrorKind::InvalidQuality ); let negative_remaining = [Candidate::new(1, 0.5, -1.0)]; assert_eq!( decide(&policy(), None, &negative_remaining) .expect_err("negative remaining time") .kind(), HandoverErrorKind::InvalidQuality ); } #[test] fn the_decision_names_are_stable() { assert_eq!(HandoverDecision::Stay.name(), "stay"); assert_eq!( HandoverDecision::HandOver { from: 1, to: 2, from_quality: 0.1, to_quality: 0.2 } .name(), "hand over" ); assert_eq!(HandoverDecision::Acquire { satellite: 1 }.name(), "acquire"); assert_eq!(HandoverDecision::Drop.name(), "drop"); } #[test] fn a_tracker_acquires_and_then_hands_over() { let mut tracker = HandoverTracker::new(policy()); // First step: acquire the best satellite. let decision = tracker .step(0.0, &[candidate(1, 0.5), candidate(2, 0.8)]) .expect("valid"); assert_eq!(decision, HandoverDecision::Acquire { satellite: 2 }); assert_eq!(tracker.current(), Some(2)); assert_eq!(tracker.handover_count(), 0); // The current server stays while it remains the best. let decision = tracker.step(1.0, &[candidate(2, 0.8)]).expect("valid"); assert_eq!(decision, HandoverDecision::Stay); // A much better candidate after the dwell time causes a handover. let decision = tracker .step(10.0, &[candidate(2, 0.3), candidate(3, 0.9)]) .expect("valid"); assert_eq!(decision.resulting_satellite(), Some(3)); assert_eq!(tracker.current(), Some(3)); assert_eq!(tracker.handover_count(), 1); assert_eq!(tracker.last_handover_seconds(), Some(10.0)); } #[test] fn a_tracker_records_the_handover_history() { let mut tracker = HandoverTracker::serving(policy(), 1); // Force two handovers by dropping the current server below the minimum. tracker .step(0.0, &[candidate(1, 0.1), candidate(2, 0.9)]) .expect("valid"); tracker .step(1.0, &[candidate(2, 0.1), candidate(3, 0.9)]) .expect("valid"); assert_eq!(tracker.handover_count(), 2); let history = tracker.history(); assert_eq!(history[0].from, 1); assert_eq!(history[0].to, 2); assert_eq!(history[1].from, 2); assert_eq!(history[1].to, 3); // The quality gain is the difference across each handover. assert!((history[0].quality_gain() - 0.8).abs() < 1e-12); assert!((tracker.total_quality_gain() - 1.6).abs() < 1e-12); } #[test] fn a_tracker_reports_the_handover_rate() { let mut tracker = HandoverTracker::serving(policy(), 1); tracker .step(0.0, &[candidate(1, 0.1), candidate(2, 0.9)]) .expect("valid"); tracker .step(100.0, &[candidate(2, 0.1), candidate(3, 0.9)]) .expect("valid"); // Two handovers a hundred seconds apart give a mean interval of a // hundred seconds. assert_eq!(tracker.mean_interval_seconds(), Some(100.0)); assert_eq!(tracker.handover_rate_per_second(), Some(0.01)); } #[test] fn a_tracker_with_fewer_than_two_handovers_has_no_rate() { let tracker = HandoverTracker::new(policy()); assert_eq!(tracker.mean_interval_seconds(), None); assert_eq!(tracker.handover_rate_per_second(), None); } #[test] fn a_tracker_that_loses_all_candidates_has_no_server() { let mut tracker = HandoverTracker::serving(policy(), 1); let decision = tracker.step(0.0, &[candidate(2, 0.05)]).expect("valid"); assert_eq!(decision, HandoverDecision::Drop); assert_eq!(tracker.current(), None); } #[test] fn a_tracker_rejects_invalid_candidates() { let mut tracker = HandoverTracker::new(policy()); let empty: [Candidate; 0] = []; assert_eq!( tracker.step(0.0, &empty).expect_err("no candidates").kind(), HandoverErrorKind::NoCandidates ); } #[test] fn a_handover_record_reports_its_gain() { let record = HandoverRecord::new(1, 2, 5.0, 0.3, 0.8); assert!((record.quality_gain() - 0.5).abs() < 1e-12); // A forced handover can show a loss. let forced = HandoverRecord::new(1, 2, 5.0, 0.9, 0.3); assert!(forced.quality_gain() < 0.0); }