//! The realistic program: keys arrive owned, queries are written with literals. //! //! Every prediction and joint query used to take `&[&[&K]]`, which at //! `K = String` made a string literal *impossible* — the shape required three //! levels of temporaries that all had to outlive the call. They are generic //! over the borrowed key now, so one spelling works at both key types. //! //! Both key types are exercised in every test, because the point is that the //! spelling is the same. use trueskill_tt::{ConstantDrift, History, NullObserver}; type Owned = History; type Borrowed = History; fn owned() -> Owned { let mut h: Owned = History::builder().key_type::().build(); for t in 1..=4 { h.record_winner(&"alice".to_string(), &"bob".to_string(), t) .expect("ingests"); } h.converge().expect("converges"); h } fn borrowed() -> Borrowed { let mut h = History::default(); for t in 1..=4 { h.record_winner(&"alice", &"bob", t).expect("ingests"); } h.converge().expect("converges"); h } #[test] fn predictions_take_literals_at_either_key_type() { let teams: &[&[&str]] = &[&["alice"], &["bob"]]; let a = owned() .predict_win_probabilities(teams) .expect("K = String"); let b = borrowed() .predict_win_probabilities(teams) .expect("K = &'static str"); assert_eq!(a, b, "the same fit through the same spelling"); assert!(a[0] > a[1], "alice won every game"); } #[test] fn every_team_shaped_query_accepts_the_same_slice() { let h = owned(); let teams: &[&[&str]] = &[&["alice"], &["bob"]]; h.quality(teams).expect("quality"); let _ = h.predict_outcome(teams).expect("outcome"); h.predict_ranking(teams, &[0, 1]).expect("ranking"); h.expected_information_gain(teams) .expect("information gain"); } #[test] fn linear_combinations_take_bare_keys() { // `&[(&K, f64)]` at `K = String` meant `&[(&String, f64)]` — no literals. let h = owned(); let terms: &[(&str, f64)] = &[("alice", 1.0), ("bob", -1.0)]; // Ranked history, so the joint is unavailable — but the *call* compiles, // which is what this pins. The error proves it reached the joint check // rather than failing to resolve a key. let err = h .posterior_of(terms) .expect_err("ranked history has no joint"); assert!( format!("{err}").contains("ranked"), "expected the joint-unavailable path, got {err}" ); } /// `lookup` is gone with `Index` (#73); the accessors that answer the same /// question all take a borrowed key. #[test] fn membership_queries_accept_a_borrowed_key() { let h = owned(); assert!(h.current_skill("alice").is_some()); assert!(h.rating("alice").is_some()); assert!(h.learning_curve("alice").is_some()); assert!(h.current_skill("nobody").is_none()); assert!(h.rating("nobody").is_none()); assert!(h.learning_curve("nobody").is_none()); } #[test] fn gamma_sets_drift_without_naming_constant_drift() { let mut a: Borrowed = History::builder().gamma(0.5).build(); let mut b: Borrowed = History::builder().drift(ConstantDrift::new(0.5)).build(); for h in [&mut a, &mut b] { h.record_winner(&"x", &"y", 1).unwrap(); h.record_winner(&"y", &"x", 100).unwrap(); h.converge().unwrap(); } let (ga, gb) = (a.current_skill("x").unwrap(), b.current_skill("x").unwrap()); assert_eq!((ga.mu(), ga.sigma()), (gb.mu(), gb.sigma())); // Control: the shorthand is not a no-op — a different gamma differs. let mut c: Borrowed = History::builder().gamma(0.0).build(); c.record_winner(&"x", &"y", 1).unwrap(); c.record_winner(&"y", &"x", 100).unwrap(); c.converge().unwrap(); assert_ne!(c.current_skill("x").unwrap().sigma(), ga.sigma()); } #[test] #[should_panic(expected = "gamma must be finite and non-negative")] fn a_negative_gamma_is_rejected_rather_than_squared_away() { let _: Borrowed = History::builder().gamma(-0.5).build(); }