use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BudgetAllocation {
pub primary: usize,
pub tests: usize,
pub callers: usize,
pub callees: usize,
pub config: usize,
}
impl BudgetAllocation {
pub fn total(&self) -> usize {
self.primary + self.tests + self.callers + self.callees + self.config
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UsageStats {
pub budget: usize,
pub used: usize,
pub remaining: usize,
pub by_category: HashMap<String, usize>,
}
#[derive(Debug, Clone)]
pub struct TokenBudgetManager {
budget: usize,
used: usize,
reserved: HashMap<String, usize>,
}
impl TokenBudgetManager {
pub fn new(budget: usize) -> Self {
Self {
budget,
used: 0,
reserved: HashMap::new(),
}
}
pub fn reserve(&mut self, category: &str, tokens: usize) -> bool {
let existing = self.reserved.get(category).copied().unwrap_or(0);
let new_used = self.used - existing + tokens;
if new_used > self.budget {
return false;
}
self.reserved.insert(category.to_string(), tokens);
self.used = new_used;
true
}
pub fn release(&mut self, category: &str) {
if let Some(tokens) = self.reserved.remove(category) {
self.used -= tokens;
}
}
pub fn allocate(&self) -> BudgetAllocation {
BudgetAllocation {
primary: (self.budget * 40) / 100,
tests: (self.budget * 20) / 100,
callers: (self.budget * 15) / 100,
callees: (self.budget * 15) / 100,
config: (self.budget * 10) / 100,
}
}
pub fn remaining(&self) -> usize {
self.budget.saturating_sub(self.used)
}
pub fn budget(&self) -> usize {
self.budget
}
pub fn used(&self) -> usize {
self.used
}
pub fn usage_stats(&self) -> UsageStats {
UsageStats {
budget: self.budget,
used: self.used,
remaining: self.remaining(),
by_category: self.reserved.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct SharedBudgetManager {
inner: Arc<Mutex<TokenBudgetManager>>,
}
impl SharedBudgetManager {
pub fn new(budget: usize) -> Self {
Self {
inner: Arc::new(Mutex::new(TokenBudgetManager::new(budget))),
}
}
pub fn from_manager(manager: TokenBudgetManager) -> Self {
Self {
inner: Arc::new(Mutex::new(manager)),
}
}
pub fn try_reserve(&self, category: &str, tokens: usize) -> bool {
self.inner
.lock()
.map(|mut mgr| mgr.reserve(category, tokens))
.unwrap_or(false)
}
pub fn release(&self, category: &str) {
if let Ok(mut mgr) = self.inner.lock() {
mgr.release(category);
}
}
pub fn remaining(&self) -> usize {
self.inner.lock().map(|mgr| mgr.remaining()).unwrap_or(0)
}
pub fn budget(&self) -> usize {
self.inner.lock().map(|mgr| mgr.budget()).unwrap_or(0)
}
pub fn used(&self) -> usize {
self.inner.lock().map(|mgr| mgr.used()).unwrap_or(0)
}
pub fn allocate(&self) -> Option<BudgetAllocation> {
self.inner.lock().ok().map(|mgr| mgr.allocate())
}
pub fn usage_stats(&self) -> Option<UsageStats> {
self.inner.lock().ok().map(|mgr| mgr.usage_stats())
}
pub fn with_manager<F, R>(&self, f: F) -> Option<R>
where
F: FnOnce(&mut TokenBudgetManager) -> R,
{
self.inner.lock().ok().map(|mut mgr| f(&mut mgr))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_budget_manager() {
let manager = TokenBudgetManager::new(8000);
assert_eq!(manager.budget(), 8000);
assert_eq!(manager.used(), 0);
assert_eq!(manager.remaining(), 8000);
}
#[test]
fn test_reserve_within_budget() {
let mut manager = TokenBudgetManager::new(1000);
assert!(manager.reserve("primary", 400));
assert_eq!(manager.used(), 400);
assert_eq!(manager.remaining(), 600);
assert!(manager.reserve("tests", 300));
assert_eq!(manager.used(), 700);
assert_eq!(manager.remaining(), 300);
}
#[test]
fn test_reserve_exceeds_budget() {
let mut manager = TokenBudgetManager::new(1000);
manager.reserve("primary", 600);
assert!(!manager.reserve("tests", 500));
assert_eq!(manager.used(), 600);
}
#[test]
fn test_reserve_replaces_existing() {
let mut manager = TokenBudgetManager::new(1000);
manager.reserve("primary", 300);
assert_eq!(manager.used(), 300);
manager.reserve("primary", 500);
assert_eq!(manager.used(), 500);
manager.reserve("primary", 200);
assert_eq!(manager.used(), 200);
}
#[test]
fn test_reserve_exact_budget() {
let mut manager = TokenBudgetManager::new(1000);
assert!(manager.reserve("primary", 1000));
assert_eq!(manager.remaining(), 0);
assert!(!manager.reserve("tests", 1));
}
#[test]
fn test_release_reservation() {
let mut manager = TokenBudgetManager::new(1000);
manager.reserve("primary", 400);
manager.reserve("tests", 300);
assert_eq!(manager.used(), 700);
manager.release("primary");
assert_eq!(manager.used(), 300);
assert_eq!(manager.remaining(), 700);
manager.release("tests");
assert_eq!(manager.used(), 0);
assert_eq!(manager.remaining(), 1000);
}
#[test]
fn test_release_nonexistent_category() {
let mut manager = TokenBudgetManager::new(1000);
manager.reserve("primary", 400);
manager.release("tests"); assert_eq!(manager.used(), 400); }
#[test]
fn test_allocate_percentages() {
let manager = TokenBudgetManager::new(10000);
let allocation = manager.allocate();
assert_eq!(allocation.primary, 4000); assert_eq!(allocation.tests, 2000); assert_eq!(allocation.callers, 1500); assert_eq!(allocation.callees, 1500); assert_eq!(allocation.config, 1000); assert_eq!(allocation.total(), 10000);
}
#[test]
fn test_allocate_rounding() {
let manager = TokenBudgetManager::new(1000);
let allocation = manager.allocate();
assert_eq!(allocation.primary, 400); assert_eq!(allocation.tests, 200); assert_eq!(allocation.callers, 150); assert_eq!(allocation.callees, 150); assert_eq!(allocation.config, 100); }
#[test]
fn test_allocate_small_budget() {
let manager = TokenBudgetManager::new(100);
let allocation = manager.allocate();
assert_eq!(allocation.primary, 40);
assert_eq!(allocation.tests, 20);
assert_eq!(allocation.callers, 15);
assert_eq!(allocation.callees, 15);
assert_eq!(allocation.config, 10);
}
#[test]
fn test_usage_stats() {
let mut manager = TokenBudgetManager::new(1000);
manager.reserve("primary", 400);
manager.reserve("tests", 200);
let stats = manager.usage_stats();
assert_eq!(stats.budget, 1000);
assert_eq!(stats.used, 600);
assert_eq!(stats.remaining, 400);
assert_eq!(stats.by_category.len(), 2);
assert_eq!(stats.by_category.get("primary"), Some(&400));
assert_eq!(stats.by_category.get("tests"), Some(&200));
}
#[test]
fn test_usage_stats_empty() {
let manager = TokenBudgetManager::new(1000);
let stats = manager.usage_stats();
assert_eq!(stats.budget, 1000);
assert_eq!(stats.used, 0);
assert_eq!(stats.remaining, 1000);
assert!(stats.by_category.is_empty());
}
#[test]
fn test_multiple_categories() {
let mut manager = TokenBudgetManager::new(2000);
manager.reserve("primary", 600);
manager.reserve("tests", 300);
manager.reserve("callers", 200);
manager.reserve("callees", 200);
manager.reserve("config", 100);
assert_eq!(manager.used(), 1400);
assert_eq!(manager.remaining(), 600);
let stats = manager.usage_stats();
assert_eq!(stats.by_category.len(), 5);
}
#[test]
fn test_budget_allocation_total() {
let allocation = BudgetAllocation {
primary: 1000,
tests: 500,
callers: 300,
callees: 300,
config: 200,
};
assert_eq!(allocation.total(), 2300);
}
#[test]
fn test_remaining_with_underflow_protection() {
let mut manager = TokenBudgetManager::new(100);
manager.reserve("primary", 100);
assert_eq!(manager.remaining(), 0);
let remaining = manager.remaining();
assert_eq!(remaining, 0);
}
#[test]
fn test_shared_budget_manager() {
let shared = SharedBudgetManager::new(1000);
assert_eq!(shared.budget(), 1000);
assert_eq!(shared.remaining(), 1000);
assert!(shared.try_reserve("primary", 400));
assert_eq!(shared.remaining(), 600);
assert!(shared.try_reserve("tests", 300));
assert_eq!(shared.remaining(), 300);
shared.release("primary");
assert_eq!(shared.remaining(), 700);
}
#[test]
fn test_shared_budget_manager_clone() {
let shared = SharedBudgetManager::new(1000);
let clone = shared.clone();
assert!(shared.try_reserve("primary", 500));
assert_eq!(clone.remaining(), 500);
assert!(clone.try_reserve("tests", 300));
assert_eq!(shared.remaining(), 200);
}
#[tokio::test]
async fn test_shared_budget_manager_concurrent() {
let shared = SharedBudgetManager::new(1000);
let clone1 = shared.clone();
let clone2 = shared.clone();
let handle1 = tokio::spawn(async move { clone1.try_reserve("task1", 300) });
let handle2 = tokio::spawn(async move { clone2.try_reserve("task2", 400) });
let (r1, r2) = tokio::join!(handle1, handle2);
assert!(r1.unwrap());
assert!(r2.unwrap());
assert_eq!(shared.used(), 700);
assert_eq!(shared.remaining(), 300);
}
#[test]
fn test_shared_budget_manager_with_manager() {
let shared = SharedBudgetManager::new(1000);
let result = shared.with_manager(|mgr| {
mgr.reserve("primary", 400);
mgr.reserve("tests", 200);
mgr.remaining()
});
assert_eq!(result, Some(400));
}
#[test]
fn test_shared_budget_allocation() {
let shared = SharedBudgetManager::new(10000);
let allocation = shared.allocate().unwrap();
assert_eq!(allocation.primary, 4000);
assert_eq!(allocation.tests, 2000);
assert_eq!(allocation.callers, 1500);
assert_eq!(allocation.callees, 1500);
assert_eq!(allocation.config, 1000);
}
}