use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use crate::TldrError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisLimits {
pub max_nodes: usize,
pub max_edges: usize,
pub max_diamond_paths: usize,
pub max_patterns: usize,
pub timeout_secs: u64,
}
impl Default for AnalysisLimits {
fn default() -> Self {
Self {
max_nodes: 50_000,
max_edges: 100_000,
max_diamond_paths: 1_000,
max_patterns: 10_000,
timeout_secs: 30,
}
}
}
impl AnalysisLimits {
pub fn with_timeout(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
pub fn with_max_nodes(mut self, max: usize) -> Self {
self.max_nodes = max;
self
}
pub fn with_max_edges(mut self, max: usize) -> Self {
self.max_edges = max;
self
}
pub fn unlimited() -> Self {
Self {
max_nodes: usize::MAX,
max_edges: usize::MAX,
max_diamond_paths: usize::MAX,
max_patterns: usize::MAX,
timeout_secs: 0,
}
}
pub fn check_nodes(&self, count: usize) -> Result<(), LimitExceeded> {
if count > self.max_nodes {
Err(LimitExceeded::MaxNodes {
limit: self.max_nodes,
actual: count,
})
} else {
Ok(())
}
}
pub fn check_edges(&self, count: usize) -> Result<(), LimitExceeded> {
if count > self.max_edges {
Err(LimitExceeded::MaxEdges {
limit: self.max_edges,
actual: count,
})
} else {
Ok(())
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LimitExceeded {
MaxNodes {
limit: usize,
actual: usize,
},
MaxEdges {
limit: usize,
actual: usize,
},
MaxDiamondPaths {
limit: usize,
actual: usize,
},
MaxPatterns {
limit: usize,
actual: usize,
},
Timeout {
elapsed_secs: u64,
limit_secs: u64,
},
}
impl std::fmt::Display for LimitExceeded {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LimitExceeded::MaxNodes { limit, actual } => {
write!(
f,
"Node limit exceeded: {} nodes found, limit is {}. Use --max-nodes or filter with --class",
actual, limit
)
}
LimitExceeded::MaxEdges { limit, actual } => {
write!(
f,
"Edge limit exceeded: {} edges found, limit is {}. Use --max-files to limit scope",
actual, limit
)
}
LimitExceeded::MaxDiamondPaths { limit, actual } => {
write!(
f,
"Diamond path limit exceeded: {} paths found, limit is {}. Use --no-patterns to skip diamond detection",
actual, limit
)
}
LimitExceeded::MaxPatterns { limit, actual } => {
write!(
f,
"Pattern limit exceeded: {} patterns found, limit is {}",
actual, limit
)
}
LimitExceeded::Timeout {
elapsed_secs,
limit_secs,
} => {
write!(
f,
"Analysis timed out after {}s (limit: {}s). Try:\n - Use --max-files to limit scope\n - Use --class filter for inheritance\n - Increase timeout with --timeout",
elapsed_secs, limit_secs
)
}
}
}
}
impl std::error::Error for LimitExceeded {}
#[derive(Clone)]
pub struct TimeoutContext {
start: Instant,
timeout: Duration,
cancelled: Arc<AtomicBool>,
nodes_processed: Arc<AtomicUsize>,
}
impl TimeoutContext {
pub fn new(timeout_secs: u64) -> Self {
Self {
start: Instant::now(),
timeout: if timeout_secs == 0 {
Duration::MAX
} else {
Duration::from_secs(timeout_secs)
},
cancelled: Arc::new(AtomicBool::new(false)),
nodes_processed: Arc::new(AtomicUsize::new(0)),
}
}
pub fn no_timeout() -> Self {
Self::new(0)
}
pub fn check(&self) -> Result<(), LimitExceeded> {
if self.cancelled.load(Ordering::Relaxed) {
return Err(LimitExceeded::Timeout {
elapsed_secs: self.start.elapsed().as_secs(),
limit_secs: self.timeout.as_secs(),
});
}
if self.start.elapsed() >= self.timeout {
return Err(LimitExceeded::Timeout {
elapsed_secs: self.start.elapsed().as_secs(),
limit_secs: self.timeout.as_secs(),
});
}
Ok(())
}
pub fn check_periodic(&self, check_interval: usize) -> Result<bool, LimitExceeded> {
let count = self.nodes_processed.fetch_add(1, Ordering::Relaxed);
if count.is_multiple_of(check_interval) {
self.check()?;
Ok(true)
} else {
Ok(false)
}
}
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::SeqCst);
}
pub fn elapsed(&self) -> Duration {
self.start.elapsed()
}
pub fn nodes_processed(&self) -> usize {
self.nodes_processed.load(Ordering::Relaxed)
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Relaxed)
}
}
impl Default for TimeoutContext {
fn default() -> Self {
Self::new(30) }
}
pub fn with_timeout<T, F>(timeout: Duration, f: F) -> Result<T, TldrError>
where
T: Send + 'static,
F: FnOnce() -> T + Send + 'static,
{
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let result = f();
let _ = tx.send(result);
});
match rx.recv_timeout(timeout) {
Ok(result) => Ok(result),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(TldrError::Timeout(format!(
"Operation timed out after {}s. Try:\n - Use --max-files to limit scope\n - Increase timeout with --timeout",
timeout.as_secs()
))),
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
Err(TldrError::Timeout("Analysis thread panicked".to_string()))
}
}
}
pub fn with_timeout_result<T, E, F>(timeout: Duration, f: F) -> Result<T, TldrError>
where
T: Send + 'static,
E: std::error::Error + Send + 'static,
F: FnOnce() -> Result<T, E> + Send + 'static,
{
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let result = f();
let _ = tx.send(result);
});
match rx.recv_timeout(timeout) {
Ok(Ok(result)) => Ok(result),
Ok(Err(e)) => Err(TldrError::Timeout(format!("Analysis failed: {}", e))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(TldrError::Timeout(format!(
"Operation timed out after {}s",
timeout.as_secs()
))),
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
Err(TldrError::Timeout("Analysis thread panicked".to_string()))
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnalysisProgress {
pub files_scanned: usize,
pub files_skipped: usize,
pub nodes_processed: usize,
pub edges_processed: usize,
pub truncated: bool,
pub truncation_reason: Option<String>,
pub elapsed_ms: u64,
}
impl AnalysisProgress {
pub fn new() -> Self {
Self::default()
}
pub fn truncate(&mut self, reason: impl Into<String>) {
self.truncated = true;
self.truncation_reason = Some(reason.into());
}
pub fn set_elapsed(&mut self, elapsed: Duration) {
self.elapsed_ms = elapsed.as_millis() as u64;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_limits_default() {
let limits = AnalysisLimits::default();
assert_eq!(limits.max_nodes, 50_000);
assert_eq!(limits.max_edges, 100_000);
assert_eq!(limits.timeout_secs, 30);
}
#[test]
fn test_limits_check_nodes() {
let limits = AnalysisLimits::default().with_max_nodes(100);
assert!(limits.check_nodes(50).is_ok());
assert!(limits.check_nodes(100).is_ok());
assert!(limits.check_nodes(101).is_err());
}
#[test]
fn test_limits_unlimited() {
let limits = AnalysisLimits::unlimited();
assert!(limits.check_nodes(1_000_000).is_ok());
assert!(limits.check_edges(1_000_000).is_ok());
}
#[test]
fn test_timeout_context_immediate() {
let ctx = TimeoutContext::new(30);
assert!(ctx.check().is_ok());
}
#[test]
fn test_timeout_context_cancelled() {
let ctx = TimeoutContext::new(30);
ctx.cancel();
assert!(ctx.check().is_err());
assert!(ctx.is_cancelled());
}
#[test]
fn test_timeout_context_no_timeout() {
let ctx = TimeoutContext::no_timeout();
assert!(ctx.check().is_ok());
}
#[test]
fn test_with_timeout_completes() {
let result = with_timeout(Duration::from_secs(5), || 42);
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_with_timeout_times_out() {
let result = with_timeout(Duration::from_millis(10), || {
std::thread::sleep(Duration::from_secs(5));
42
});
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, TldrError::Timeout(_)));
}
#[test]
fn test_limit_exceeded_display() {
let err = LimitExceeded::MaxNodes {
limit: 1000,
actual: 2000,
};
let msg = err.to_string();
assert!(msg.contains("2000"));
assert!(msg.contains("1000"));
assert!(msg.contains("--max-nodes"));
}
#[test]
fn test_progress_truncate() {
let mut progress = AnalysisProgress::new();
progress.files_scanned = 100;
progress.truncate("Max nodes exceeded");
assert!(progress.truncated);
assert_eq!(
progress.truncation_reason,
Some("Max nodes exceeded".to_string())
);
}
}