use crate::models::ScheduledTest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TestScheduler {
pub duration_history: HashMap<String, Vec<u64>>,
pub default_duration_ms: u64,
}
impl TestScheduler {
pub fn new() -> Self {
TestScheduler {
duration_history: HashMap::new(),
default_duration_ms: 1000, }
}
pub fn update_duration(&mut self, node_id: &str, duration_ms: u64) {
let history = self
.duration_history
.entry(node_id.to_string())
.or_default();
history.push(duration_ms);
if history.len() > 10 {
*history = history[history.len() - 10..].to_vec();
}
}
pub fn get_estimated_duration(&self, node_id: &str) -> u64 {
if let Some(durations) = self.duration_history.get(node_id) {
if durations.is_empty() {
return self.default_duration_ms;
}
if durations.len() == 1 {
return durations[0];
}
let len = durations.len();
let mut weighted_sum: f64 = 0.0;
let mut total_weight: f64 = 0.0;
for (i, &duration) in durations.iter().enumerate() {
let weight = 0.5_f64.powi((len - 1 - i) as i32);
weighted_sum += duration as f64 * weight;
total_weight += weight;
}
(weighted_sum / total_weight) as u64
} else {
self.default_duration_ms
}
}
pub fn schedule(
&self,
node_ids: &[String],
failed_first: bool,
recent_failures: &[String],
) -> Vec<String> {
if node_ids.is_empty() {
return Vec::new();
}
let recent_failures_set: std::collections::HashSet<&str> =
recent_failures.iter().map(|s| s.as_str()).collect();
let mut scheduled: Vec<ScheduledTest> = Vec::with_capacity(node_ids.len());
for node_id in node_ids {
let est_duration = self.get_estimated_duration(node_id);
let mut priority = est_duration;
if failed_first && recent_failures_set.contains(node_id.as_str()) {
priority += 1_000_000;
}
scheduled.push(ScheduledTest {
node_id: node_id.clone(),
estimated_duration_ms: est_duration,
priority,
});
}
scheduled.sort_by(|a, b| b.priority.cmp(&a.priority));
scheduled.into_iter().map(|s| s.node_id).collect()
}
pub fn split_balanced(&self, node_ids: &[String], worker_count: usize) -> Vec<Vec<String>> {
if worker_count <= 1 || node_ids.is_empty() {
return vec![node_ids.to_vec()];
}
let mut sorted: Vec<_> = node_ids
.iter()
.map(|id| (id.clone(), self.get_estimated_duration(id)))
.collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1));
let mut batches: Vec<Vec<String>> = vec![Vec::new(); worker_count];
let mut batch_durations = vec![0u64; worker_count];
for (test, duration) in sorted {
let min_idx = batch_durations
.iter()
.enumerate()
.min_by_key(|(_, d)| *d)
.map(|(i, _)| i)
.unwrap_or(0);
batches[min_idx].push(test);
batch_durations[min_idx] += duration;
}
batches.into_iter().filter(|b| !b.is_empty()).collect()
}
pub fn clear_history(&mut self) {
self.duration_history.clear();
}
pub fn get_history(&self, node_id: &str) -> Option<&Vec<u64>> {
self.duration_history.get(node_id)
}
pub fn tracked_count(&self) -> usize {
self.duration_history.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schedule_ordering() {
let scheduler = TestScheduler::new();
let node_ids = vec![
"test_a".to_string(),
"test_b".to_string(),
"test_c".to_string(),
];
let result = scheduler.schedule(&node_ids, false, &[]);
assert_eq!(result, node_ids);
}
#[test]
fn test_failed_first_priority() {
let scheduler = TestScheduler::new();
let node_ids = vec![
"test_a".to_string(),
"test_b".to_string(),
"test_c".to_string(),
];
let recent_failures = vec!["test_b".to_string()];
let result = scheduler.schedule(&node_ids, true, &recent_failures);
assert_eq!(result[0], "test_b");
}
#[test]
fn test_duration_update() {
let mut scheduler = TestScheduler::new();
scheduler.update_duration("test_a", 100);
scheduler.update_duration("test_a", 200);
let duration = scheduler.get_estimated_duration("test_a");
assert!(duration >= 150 && duration <= 200);
}
#[test]
fn test_split_balanced_empty() {
let scheduler = TestScheduler::new();
let result = scheduler.split_balanced(&[], 4);
assert_eq!(result.len(), 1);
assert!(result[0].is_empty());
}
#[test]
fn test_split_balanced_single_worker() {
let scheduler = TestScheduler::new();
let node_ids = vec!["a".to_string(), "b".to_string(), "c".to_string()];
let result = scheduler.split_balanced(&node_ids, 1);
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 3);
}
#[test]
fn test_split_balanced_even_distribution() {
let mut scheduler = TestScheduler::new();
scheduler.update_duration("slow", 1000);
scheduler.update_duration("medium1", 500);
scheduler.update_duration("medium2", 500);
scheduler.update_duration("fast1", 100);
scheduler.update_duration("fast2", 100);
let node_ids = vec![
"slow".to_string(),
"medium1".to_string(),
"medium2".to_string(),
"fast1".to_string(),
"fast2".to_string(),
];
let batches = scheduler.split_balanced(&node_ids, 2);
assert_eq!(batches.len(), 2);
let batch1_duration: u64 = batches[0]
.iter()
.map(|id| scheduler.get_estimated_duration(id))
.sum();
let batch2_duration: u64 = batches[1]
.iter()
.map(|id| scheduler.get_estimated_duration(id))
.sum();
let max_dur = batch1_duration.max(batch2_duration);
let min_dur = batch1_duration.min(batch2_duration);
assert!(
max_dur <= min_dur * 2,
"Batches not balanced: {} vs {}",
batch1_duration,
batch2_duration
);
}
}