use rand::Rng;
use crate::health::HealthTracker;
pub fn compute_score(health: &HealthTracker, proxy: &str, host: &str) -> f64 {
let affinity = health.get_affinity(proxy, host);
let global = health.get_global_health(proxy);
let minutes = health.minutes_since_last_success(proxy, host);
let recency = 1.0 / (1.0 + minutes);
let jitter: f64 = rand::thread_rng().gen_range(0.0..0.1);
affinity * 0.5 + global * 0.3 + recency * 0.2 + jitter * 0.1
}
#[cfg(test)]
fn compute_score_deterministic(
health: &HealthTracker,
proxy: &str,
host: &str,
jitter: f64,
) -> f64 {
let affinity = health.get_affinity(proxy, host);
let global = health.get_global_health(proxy);
let minutes = health.minutes_since_last_success(proxy, host);
let recency = 1.0 / (1.0 + minutes);
affinity * 0.5 + global * 0.3 + recency * 0.2 + jitter * 0.1
}
pub fn adaptive_k(available_count: usize, avg_success_rate: f64, base_k: usize) -> usize {
if available_count <= 2 {
return available_count;
}
if available_count <= 6 {
if avg_success_rate > 0.85 {
return 1;
}
return base_k.min(2);
}
if available_count > 10 && avg_success_rate > 0.85 {
return 1;
}
if avg_success_rate > 0.6 {
return base_k.min(2);
}
base_k
}
#[cfg(test)]
mod tests {
use super::*;
const PROXY_A: &str = "socks5://1.2.3.4:1080";
const PROXY_B: &str = "socks5://5.6.7.8:9050";
const HOST_X: &str = "yunhq.sse.com.cn";
const HOST_Y: &str = "www.szse.cn";
fn tracker() -> HealthTracker {
HealthTracker::new(10)
}
#[test]
fn score_for_unknown_pair_uses_priors() {
let ht = tracker();
let score = compute_score(&ht, PROXY_A, HOST_X);
assert!(score >= 0.39, "score = {score}");
assert!(score < 0.42, "score = {score}");
}
#[test]
fn score_is_higher_for_successful_proxy() {
let ht = tracker();
for _ in 0..5 {
ht.record_success(PROXY_A, HOST_X, 50.0);
}
for _ in 0..5 {
ht.record_failure(PROXY_B, HOST_X);
}
let mut a_wins = 0;
for _ in 0..100 {
let sa = compute_score(&ht, PROXY_A, HOST_X);
let sb = compute_score(&ht, PROXY_B, HOST_X);
if sa > sb {
a_wins += 1;
}
}
assert!(
a_wins > 90,
"expected PROXY_A to win most of the time, but only won {a_wins}/100"
);
}
#[test]
fn deterministic_score_no_jitter() {
let ht = tracker();
ht.record_success(PROXY_A, HOST_X, 100.0);
let s1 = compute_score_deterministic(&ht, PROXY_A, HOST_X, 0.0);
let s2 = compute_score_deterministic(&ht, PROXY_A, HOST_X, 0.0);
assert!(
(s1 - s2).abs() < 1e-6,
"deterministic scores should be equal"
);
}
#[test]
fn score_weights_sum_correctly() {
let ht = tracker();
ht.record_success(PROXY_A, HOST_X, 10.0);
let score = compute_score_deterministic(&ht, PROXY_A, HOST_X, 0.05);
assert!(score > 0.99, "score = {score}");
assert!(score < 1.01, "score = {score}");
}
#[test]
fn score_recency_decays_with_time() {
let ht = tracker();
ht.record_failure(PROXY_A, HOST_X);
let score = compute_score_deterministic(&ht, PROXY_A, HOST_X, 0.0);
assert!(score < 0.01, "score = {score}");
}
#[test]
fn score_bounded_in_reasonable_range() {
let ht = tracker();
for _ in 0..10 {
let score = compute_score(&ht, PROXY_A, HOST_X);
assert!(score >= 0.0, "score should be non-negative: {score}");
assert!(score <= 1.1, "score should be bounded: {score}");
}
}
#[test]
fn score_uses_global_health_for_unknown_host() {
let ht = tracker();
for _ in 0..5 {
ht.record_success(PROXY_A, HOST_X, 50.0);
}
let score = compute_score_deterministic(&ht, PROXY_A, HOST_Y, 0.0);
assert!(score > 0.54, "score = {score}");
assert!(score < 0.57, "score = {score}");
}
#[test]
fn score_mixed_history() {
let ht = HealthTracker::new(4);
ht.record_success(PROXY_A, HOST_X, 10.0);
ht.record_success(PROXY_A, HOST_X, 10.0);
ht.record_success(PROXY_A, HOST_X, 10.0);
ht.record_failure(PROXY_A, HOST_X);
let score = compute_score_deterministic(&ht, PROXY_A, HOST_X, 0.0);
assert!(score > 0.59, "score = {score}");
assert!(score < 0.82, "score = {score}");
}
#[test]
fn adaptive_k_zero_available() {
assert_eq!(adaptive_k(0, 0.9, 3), 0);
}
#[test]
fn adaptive_k_one_available() {
assert_eq!(adaptive_k(1, 0.9, 3), 1);
}
#[test]
fn adaptive_k_two_available() {
assert_eq!(adaptive_k(2, 0.1, 5), 2);
}
#[test]
fn adaptive_k_small_pool_high_success() {
assert_eq!(adaptive_k(3, 0.90, 3), 1);
assert_eq!(adaptive_k(4, 0.95, 5), 1);
assert_eq!(adaptive_k(6, 0.99, 4), 1);
}
#[test]
fn adaptive_k_small_pool_low_success() {
assert_eq!(adaptive_k(3, 0.50, 3), 2);
assert_eq!(adaptive_k(5, 0.85, 5), 2); assert_eq!(adaptive_k(6, 0.30, 1), 1); }
#[test]
fn adaptive_k_large_pool_high_success() {
assert_eq!(adaptive_k(11, 0.90, 3), 1);
assert_eq!(adaptive_k(50, 0.99, 5), 1);
}
#[test]
fn adaptive_k_medium_pool_moderate_success() {
assert_eq!(adaptive_k(7, 0.70, 3), 2);
assert_eq!(adaptive_k(10, 0.80, 5), 2);
assert_eq!(adaptive_k(8, 0.65, 1), 1); }
#[test]
fn adaptive_k_large_pool_moderate_success() {
assert_eq!(adaptive_k(15, 0.70, 4), 2);
assert_eq!(adaptive_k(20, 0.80, 3), 2);
}
#[test]
fn adaptive_k_medium_pool_low_success() {
assert_eq!(adaptive_k(7, 0.50, 3), 3);
assert_eq!(adaptive_k(10, 0.60, 5), 5); assert_eq!(adaptive_k(8, 0.10, 4), 4);
}
#[test]
fn adaptive_k_large_pool_low_success() {
assert_eq!(adaptive_k(15, 0.30, 4), 4);
assert_eq!(adaptive_k(50, 0.60, 3), 3); }
#[test]
fn adaptive_k_boundary_at_3_proxies() {
assert_eq!(adaptive_k(3, 0.90, 3), 1); assert_eq!(adaptive_k(3, 0.50, 3), 2); }
#[test]
fn adaptive_k_boundary_at_7_proxies() {
assert_eq!(adaptive_k(7, 0.90, 3), 2); assert_eq!(adaptive_k(7, 0.50, 4), 4); }
#[test]
fn adaptive_k_boundary_at_11_proxies() {
assert_eq!(adaptive_k(11, 0.90, 3), 1); assert_eq!(adaptive_k(11, 0.70, 3), 2); assert_eq!(adaptive_k(11, 0.50, 4), 4); }
#[test]
fn adaptive_k_base_k_of_one() {
assert_eq!(adaptive_k(5, 0.70, 1), 1);
assert_eq!(adaptive_k(8, 0.70, 1), 1);
}
#[test]
fn adaptive_k_never_returns_more_than_available() {
assert_eq!(adaptive_k(2, 0.10, 10), 2);
assert_eq!(adaptive_k(1, 0.10, 10), 1);
}
}