use super::failure_tracker::{FailureTracker, ROTATE_AFTER_FAILURES};
pub const PRIMARY_ROTATION: &[&str] = &["chrome-h", "chrome-new"];
pub const EXTENDED_ROTATION: &[&str] = &["firefox", "lightpanda", "servo"];
pub const BROWSER_ROTATION: &[&str] = &[
"chrome-h",
"chrome-new",
"firefox",
"lightpanda",
"servo",
];
pub struct BrowserSelector {
tracker: FailureTracker,
}
impl BrowserSelector {
pub fn new(tracker: FailureTracker) -> Self {
Self { tracker }
}
pub fn failure_tracker(&self) -> &FailureTracker {
&self.tracker
}
pub fn should_rotate(&self, domain: &str, current_browser: &str) -> bool {
self.tracker.failure_count(domain, current_browser) >= ROTATE_AFTER_FAILURES
}
pub fn next_browser(&self, domain: &str, current_browser: &str) -> Option<&'static str> {
let current_idx = BROWSER_ROTATION
.iter()
.position(|&b| b == current_browser)
.unwrap_or(0);
for offset in 1..BROWSER_ROTATION.len() {
let idx = (current_idx + offset) % BROWSER_ROTATION.len();
let candidate = BROWSER_ROTATION[idx];
if self.tracker.failure_count(domain, candidate) < ROTATE_AFTER_FAILURES {
return Some(candidate);
}
}
None
}
pub fn choose_browser<'a>(&self, domain: &str, fallback: &'a str) -> &'a str
where
'static: 'a,
{
for &browser in BROWSER_ROTATION {
if self.tracker.failure_count(domain, browser) < ROTATE_AFTER_FAILURES {
return browser;
}
}
fallback
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_selector() -> BrowserSelector {
BrowserSelector::new(FailureTracker::new())
}
#[test]
fn rotation_constants_are_consistent() {
let mut expected: Vec<&str> = PRIMARY_ROTATION.to_vec();
expected.extend_from_slice(EXTENDED_ROTATION);
assert_eq!(BROWSER_ROTATION, expected.as_slice());
}
#[test]
fn should_rotate_after_threshold() {
let sel = make_selector();
assert!(!sel.should_rotate("example.com", "chrome-h"));
sel.failure_tracker().record_failure("example.com", "chrome-h");
assert!(!sel.should_rotate("example.com", "chrome-h"));
sel.failure_tracker().record_failure("example.com", "chrome-h");
assert!(sel.should_rotate("example.com", "chrome-h"));
}
#[test]
fn next_browser_skips_exhausted() {
let sel = make_selector();
sel.failure_tracker().record_failure("d.com", "chrome-h");
sel.failure_tracker().record_failure("d.com", "chrome-h");
assert_eq!(sel.next_browser("d.com", "chrome-h"), Some("chrome-new"));
sel.failure_tracker().record_failure("d.com", "chrome-new");
sel.failure_tracker().record_failure("d.com", "chrome-new");
assert_eq!(sel.next_browser("d.com", "chrome-h"), Some("firefox"));
}
#[test]
fn next_browser_returns_none_when_all_exhausted() {
let sel = make_selector();
for &browser in BROWSER_ROTATION {
sel.failure_tracker().record_failure("d.com", browser);
sel.failure_tracker().record_failure("d.com", browser);
}
assert_eq!(sel.next_browser("d.com", "chrome-h"), None);
}
#[test]
fn choose_browser_picks_first_available() {
let sel = make_selector();
assert_eq!(sel.choose_browser("d.com", "fallback"), "chrome-h");
sel.failure_tracker().record_failure("d.com", "chrome-h");
sel.failure_tracker().record_failure("d.com", "chrome-h");
assert_eq!(sel.choose_browser("d.com", "fallback"), "chrome-new");
}
#[test]
fn choose_browser_falls_back_when_all_exhausted() {
let sel = make_selector();
for &browser in BROWSER_ROTATION {
sel.failure_tracker().record_failure("d.com", browser);
sel.failure_tracker().record_failure("d.com", browser);
}
assert_eq!(sel.choose_browser("d.com", "fallback"), "fallback");
}
#[test]
fn next_browser_wraps_around() {
let sel = make_selector();
let last = *BROWSER_ROTATION.last().unwrap();
assert_eq!(sel.next_browser("d.com", last), Some("chrome-h"));
}
}