//! Tests for the new T2 public API surface: typed add_events(iter) and the //! fluent event builder (added in Task 16). use smallvec::smallvec; use trueskill_tt::{ConstantDrift, ConvergenceOptions, Event, History, Member, Outcome, Team}; #[test] fn add_events_bulk_via_iter() { let mut h = History::builder() .mu(0.0) .sigma(2.0) .beta(1.0) .p_draw(0.0) .drift(ConstantDrift::new(0.0)) .convergence(ConvergenceOptions { max_iter: 30, epsilon: 1e-6, alpha: 1.0, }) .build(); let events: Vec> = vec![ Event { time: 1, teams: smallvec![ Team::with_members([Member::new("a")]), Team::with_members([Member::new("b")]), ], outcome: Outcome::winner(0, 2), }, Event { time: 2, teams: smallvec![ Team::with_members([Member::new("b")]), Team::with_members([Member::new("c")]), ], outcome: Outcome::winner(0, 2), }, ]; h.add_events(events).unwrap(); let report = h.converge().unwrap(); assert!(report.converged); assert!(h.current_skill("a").is_some()); assert!(h.current_skill("b").is_some()); assert!(h.current_skill("c").is_some()); } #[test] fn add_events_draw() { let mut h = History::builder() .mu(25.0) .sigma(25.0 / 3.0) .beta(25.0 / 6.0) .p_draw(0.25) .drift(ConstantDrift::new(25.0 / 300.0)) .build(); let events: Vec> = vec![Event { time: 1, teams: smallvec![ Team::with_members([Member::new("alice")]), Team::with_members([Member::new("bob")]), ], outcome: Outcome::draw(2), }]; h.add_events(events).unwrap(); let _ = h.converge().unwrap(); } #[test] fn add_events_rejects_mismatched_outcome_ranks() { use trueskill_tt::InferenceError; let mut h: History = History::builder().build(); let events: Vec> = vec![Event { time: 1, teams: smallvec![ Team::with_members([Member::new("a")]), Team::with_members([Member::new("b")]), ], outcome: Outcome::ranking([0, 1, 2]), // 3 ranks but 2 teams }]; let err = h.add_events(events).unwrap_err(); assert!(matches!(err, InferenceError::MismatchedShape { .. })); } #[test] fn fluent_event_builder_basic() { let mut h = History::builder() .mu(25.0) .sigma(25.0 / 3.0) .beta(25.0 / 6.0) .p_draw(0.0) .build(); h.event(1) .team(["alice", "bob"]) .weights([1.0, 0.7]) .team(["carol"]) .ranking([1, 0]) .commit() .unwrap(); let report = h.converge().unwrap(); assert!(report.converged); assert!(h.current_skill("alice").is_some()); assert!(h.current_skill("bob").is_some()); assert!(h.current_skill("carol").is_some()); } #[test] fn fluent_event_builder_winner_convenience() { let mut h = History::builder() .mu(25.0) .sigma(25.0 / 3.0) .beta(25.0 / 6.0) .p_draw(0.0) .build(); h.event(1) .team(["alice"]) .team(["bob"]) .winner(0) .commit() .unwrap(); let _ = h.converge().unwrap(); } #[test] fn fluent_event_builder_draw() { let mut h = History::builder() .mu(25.0) .sigma(25.0 / 3.0) .beta(25.0 / 6.0) .p_draw(0.25) .build(); h.event(1) .team(["alice"]) .team(["bob"]) .draw() .commit() .unwrap(); let _ = h.converge().unwrap(); } #[test] fn current_skill_and_learning_curve() { use trueskill_tt::History; let mut h = History::builder() .mu(25.0) .sigma(25.0 / 3.0) .beta(25.0 / 6.0) .p_draw(0.0) .build(); h.record_winner(&"a", &"b", 1).unwrap(); h.record_winner(&"a", &"b", 2).unwrap(); let _ = h.converge().unwrap(); let a = h.current_skill(&"a").unwrap(); assert!(a.mu() > 25.0); let b = h.current_skill(&"b").unwrap(); assert!(b.mu() < 25.0); let a_curve = h.learning_curve(&"a").unwrap(); assert_eq!(a_curve.len(), 2); assert_eq!(a_curve[0].0, 1); assert_eq!(a_curve[1].0, 2); let all = h.learning_curves(); assert_eq!(all.len(), 2); assert!(all.contains_key("a")); assert!(all.contains_key("b")); } #[test] fn log_evidence_total_vs_subset() { use trueskill_tt::{ConstantDrift, History}; let mut h = History::builder() .mu(0.0) .sigma(6.0) .beta(1.0) .p_draw(0.0) .drift(ConstantDrift::new(0.0)) .build(); h.record_winner(&"a", &"b", 1).unwrap(); h.record_winner(&"b", &"a", 2).unwrap(); let total = h.log_evidence(); let a_only = h.log_evidence_for(&[&"a"]).unwrap(); assert!(total.is_finite()); assert!(a_only.is_finite()); } #[test] fn predict_quality_two_teams() { use trueskill_tt::History; let mut h = History::builder() .mu(25.0) .sigma(25.0 / 3.0) .beta(25.0 / 6.0) .p_draw(0.0) .build(); h.record_winner(&"a", &"b", 1).unwrap(); let _ = h.converge().unwrap(); let q = h.quality(&[&[&"a"], &[&"b"]]).unwrap(); assert!(q > 0.0 && q <= 1.0); } #[test] fn predict_outcome_two_teams_sums_to_one() { use trueskill_tt::History; let mut h = History::builder() .mu(25.0) .sigma(25.0 / 3.0) .beta(25.0 / 6.0) .p_draw(0.0) .build(); h.record_winner(&"a", &"b", 1).unwrap(); let _ = h.converge().unwrap(); let p = h.predict_outcome(&[&[&"a"], &[&"b"]]).unwrap(); let wins = p.win_probabilities(); assert_eq!(wins.len(), 2); // With p_draw == 0 there is no draw outcome, so the two win // probabilities are the whole space. assert!((p.total() - 1.0).abs() < 1e-9, "total = {}", p.total()); assert!((wins[0] + wins[1] - 1.0).abs() < 1e-9); assert!(wins[0] > wins[1]); } #[test] fn fluent_event_builder_scores() { use trueskill_tt::ConstantDrift; let mut h = History::builder() .mu(25.0) .sigma(25.0 / 3.0) .beta(25.0 / 6.0) .drift(ConstantDrift::new(0.0)) .build(); h.event(1) .team(["alice"]) .team(["bob"]) .scores([12.0, 4.0]) .commit() .unwrap(); let _ = h.converge().unwrap(); let a = h.current_skill(&"alice").unwrap(); let b = h.current_skill(&"bob").unwrap(); assert!(a.mu() > b.mu()); } /// Every field of `ConvergenceReport` must carry real information. /// /// `slices_skipped` was public, hardcoded to `0`, and reported a plausible /// value for a feature that never existed — the same shape as the inert /// `online` flag in #19. It was removed in #33. This pins the remaining fields /// so the next always-constant member has to survive an assertion rather than /// just a reviewer's attention. #[test] fn every_convergence_report_field_is_populated() { let mut h = History::builder().build(); for time in 1..=6i64 { h.record_winner(&"a", &"b", time).unwrap(); } let report = h.converge().unwrap(); assert!( report.iterations > 0, "iterations is zero on a real converge" ); assert!(report.converged, "fixture must converge"); assert!( report.final_step.0.is_finite() && report.final_step.1.is_finite(), "final_step is not finite: {:?}", report.final_step ); assert!( report.log_evidence.is_finite() && report.log_evidence < 0.0, "log_evidence is not a finite negative log probability: {}", report.log_evidence ); assert_eq!( report.per_iteration_time.len(), report.iterations, "per_iteration_time must carry one duration per iteration" ); }