//! Degenerate, boundary, and error-path coverage. //! //! These run in both debug and release: the defects they pin were all //! guarded only by `debug_assert!`, so a debug-only suite never saw them. use trueskill_tt::{ ConstantDrift, ConvergenceOptions, Game, GameOptions, Gaussian, History, InferenceError, Outcome, Rating, }; type R = Rating; fn rating() -> R { R::new( Gaussian::from_ms(25.0, 25.0 / 3.0), 25.0 / 6.0, ConstantDrift(25.0 / 300.0), ) } fn assert_finite(g: Gaussian, what: &str) { assert!( g.mu().is_finite() && g.sigma().is_finite(), "{what} must be finite, got mu={} sigma={}", g.mu(), g.sigma() ); } #[test] fn record_draw_without_draw_probability_is_rejected() { let mut h = History::default(); let err = h.record_draw(&"a", &"b", 1).unwrap_err(); assert!(matches!( err, InferenceError::TieWithoutDrawProbability { .. } )); } #[test] fn builder_draw_without_draw_probability_is_rejected() { let mut h = History::default(); let err = h .event(1) .team(["a"]) .team(["b"]) .draw() .commit() .unwrap_err(); assert!(matches!( err, InferenceError::TieWithoutDrawProbability { .. } )); } #[test] fn draw_with_positive_draw_probability_is_finite() { let mut h = History::builder().p_draw(0.25).build(); h.record_draw(&"a", &"b", 1).unwrap(); let report = h.converge().unwrap(); assert_finite(h.current_skill("a").unwrap(), "drawn competitor skill"); assert_finite(h.current_skill("b").unwrap(), "drawn competitor skill"); assert!(report.log_evidence.is_finite()); assert!(report.converged); } #[test] fn game_ranked_rejects_tie_without_draw_probability() { let a = [rating()]; let b = [rating()]; let teams: Vec<&[R]> = vec![&a, &b]; let err = Game::ranked(&teams, Outcome::draw(2), &GameOptions::default()).unwrap_err(); assert!(matches!( err, InferenceError::TieWithoutDrawProbability { .. } )); } /// `Outcome::winner(w, n)` ties every loser, so any n >= 3 free-for-all hits /// the tie path even though the caller never asked for a draw. #[test] fn winner_of_three_or_more_requires_draw_probability() { let a = [rating()]; let b = [rating()]; let c = [rating()]; let teams: Vec<&[R]> = vec![&a, &b, &c]; let err = Game::ranked(&teams, Outcome::winner(0, 3), &GameOptions::default()).unwrap_err(); assert!(matches!( err, InferenceError::TieWithoutDrawProbability { .. } )); let opts = GameOptions { p_draw: 0.1, ..GameOptions::default() }; let game = Game::ranked(&teams, Outcome::winner(0, 3), &opts).unwrap(); for team in game.posteriors() { for skill in team { assert_finite(skill, "3-team winner posterior"); } } } #[test] fn full_ranking_without_ties_needs_no_draw_probability() { let a = [rating()]; let b = [rating()]; let c = [rating()]; let teams: Vec<&[R]> = vec![&a, &b, &c]; let game = Game::ranked(&teams, Outcome::ranking([0, 1, 2]), &GameOptions::default()).unwrap(); for team in game.posteriors() { for skill in team { assert_finite(skill, "strict ranking posterior"); } } } #[test] fn empty_history_converges_trivially() { let mut h = History::default(); let report = h.converge().unwrap(); assert_eq!(report.iterations, 0); assert!(report.converged); } #[test] fn empty_event_stream_then_converge() { let mut h = History::default(); h.add_events(std::iter::empty()).unwrap(); let report = h.converge().unwrap(); assert_eq!(report.iterations, 0); } #[test] fn empty_history_queries_do_not_panic() { let h = History::default(); assert!(h.learning_curves().is_empty()); assert!(h.learning_curve("nobody").is_empty()); assert!(h.current_skill("nobody").is_none()); } #[test] fn single_event_history_converges() { let mut h = History::default(); h.record_winner(&"a", &"b", 1).unwrap(); let report = h.converge().unwrap(); assert!(report.converged); assert_finite(h.current_skill("a").unwrap(), "single-event skill"); } #[test] fn scored_event_rejects_non_positive_sigma() { let mut h = History::builder().score_sigma(2.0).build(); let err = h .event(1) .team(["a"]) .team(["b"]) .scores_with_sigma([3.0, 1.0], f64::NAN) .commit() .unwrap_err(); assert!(matches!( err, InferenceError::InvalidParameter { name: "score_sigma", .. } )); } #[test] fn convergence_reports_are_finite_across_many_teams() { let opts = GameOptions { p_draw: 0.1, convergence: ConvergenceOptions::default(), ..GameOptions::default() }; let holders: Vec<[R; 1]> = (0..12).map(|_| [rating()]).collect(); let teams: Vec<&[R]> = holders.iter().map(|t| t.as_slice()).collect(); let game = Game::ranked(&teams, Outcome::ranking(0..12), &opts).unwrap(); assert!( game.log_evidence().is_finite(), "12-team log-evidence must be finite, got {}", game.log_evidence() ); for team in game.posteriors() { for skill in team { assert_finite(skill, "12-team posterior"); } } } /// A long diff chain underflows a linear evidence product: each link /// contributes a probability in (0, 1], so ~1000 links flush the product to /// exactly 0.0 and `ln(0.0)` is `-inf`. Accumulating in log space keeps it /// finite. #[test] fn log_evidence_survives_a_long_diff_chain() { let holders: Vec<[R; 1]> = (0..1200).map(|_| [rating()]).collect(); let teams: Vec<&[R]> = holders.iter().map(|t| t.as_slice()).collect(); let game = Game::ranked( &teams, Outcome::ranking(0..holders.len() as u32), &GameOptions::default(), ) .unwrap(); let log_evidence = game.log_evidence(); assert!( log_evidence.is_finite(), "1200-team log-evidence must be finite, got {log_evidence}" ); assert!( log_evidence < 0.0, "log-evidence of a probability must be negative, got {log_evidence}" ); } /// A near-certain outcome rounds the losing tail to exactly zero in the /// `erfc` approximation; the evidence floor keeps `ln` finite. #[test] fn log_evidence_finite_for_near_certain_outcome() { let overwhelming = R::new(Gaussian::from_ms(5_000.0, 0.5), 1.0, ConstantDrift(0.0)); let hopeless = R::new(Gaussian::from_ms(-5_000.0, 0.5), 1.0, ConstantDrift(0.0)); let a = [overwhelming]; let b = [hopeless]; let teams: Vec<&[R]> = vec![&a, &b]; let game = Game::ranked(&teams, Outcome::winner(0, 2), &GameOptions::default()).unwrap(); assert!( game.log_evidence().is_finite(), "got {}", game.log_evidence() ); // And the reverse — a colossal upset — must also stay finite. let upset = Game::ranked(&teams, Outcome::winner(1, 2), &GameOptions::default()).unwrap(); assert!( upset.log_evidence().is_finite(), "upset log-evidence must be finite, got {}", upset.log_evidence() ); } #[test] fn empty_history_has_no_filtered_estimates() { let history: History = History::builder().build(); assert_eq!(history.filtered_log_evidence(), 0.0); assert!(history.filtered_learning_curves().is_empty()); assert!(history.filtered_learning_curve("nobody").is_empty()); }