Skip to main content

fastmcp_rust/testing/
timing.rs

1//! Timing utilities for test measurements.
2//!
3//! Provides stopwatch functionality for measuring operation durations
4//! in tests and benchmarks.
5
6use std::time::{Duration, Instant};
7
8/// Measures the duration of a closure execution.
9///
10/// # Example
11///
12/// ```ignore
13/// use fastmcp_rust::testing::prelude::*;
14///
15/// let (result, duration) = measure_duration(|| {
16///     // Some operation
17///     42
18/// });
19///
20/// assert_eq!(result, 42);
21/// println!("Operation took {:?}", duration);
22/// ```
23pub fn measure_duration<T, F: FnOnce() -> T>(f: F) -> (T, Duration) {
24    let start = Instant::now();
25    let result = f();
26    let duration = start.elapsed();
27    (result, duration)
28}
29
30/// A stopwatch for measuring elapsed time with lap support.
31///
32/// # Example
33///
34/// ```ignore
35/// use fastmcp_rust::testing::prelude::*;
36///
37/// let mut stopwatch = Stopwatch::new();
38///
39/// // Do first operation
40/// stopwatch.lap("operation_1");
41///
42/// // Do second operation
43/// stopwatch.lap("operation_2");
44///
45/// println!("Total time: {:?}", stopwatch.elapsed());
46/// for (name, duration) in stopwatch.laps() {
47///     println!("{}: {:?}", name, duration);
48/// }
49/// ```
50#[derive(Debug, Clone)]
51pub struct Stopwatch {
52    /// Start time.
53    start: Instant,
54    /// Last lap time.
55    last_lap: Instant,
56    /// Recorded laps with names.
57    laps: Vec<(String, Duration)>,
58}
59
60impl Default for Stopwatch {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl Stopwatch {
67    /// Creates a new stopwatch and starts it immediately.
68    #[must_use]
69    pub fn new() -> Self {
70        let now = Instant::now();
71        Self {
72            start: now,
73            last_lap: now,
74            laps: Vec::new(),
75        }
76    }
77
78    /// Creates a stopped stopwatch that starts when `start()` is called.
79    #[must_use]
80    pub fn stopped() -> Self {
81        // Use a sentinel value; will be overwritten by start()
82        let now = Instant::now();
83        Self {
84            start: now,
85            last_lap: now,
86            laps: Vec::new(),
87        }
88    }
89
90    /// Restarts the stopwatch, clearing all laps.
91    pub fn restart(&mut self) {
92        let now = Instant::now();
93        self.start = now;
94        self.last_lap = now;
95        self.laps.clear();
96    }
97
98    /// Records a lap with the given name.
99    ///
100    /// The duration is measured from the last lap (or start if no laps).
101    pub fn lap(&mut self, name: impl Into<String>) {
102        let now = Instant::now();
103        let duration = now - self.last_lap;
104        self.laps.push((name.into(), duration));
105        self.last_lap = now;
106    }
107
108    /// Returns the total elapsed time since the stopwatch started.
109    #[must_use]
110    pub fn elapsed(&self) -> Duration {
111        self.start.elapsed()
112    }
113
114    /// Returns the time since the last lap (or start if no laps).
115    #[must_use]
116    pub fn since_last_lap(&self) -> Duration {
117        self.last_lap.elapsed()
118    }
119
120    /// Returns all recorded laps.
121    #[must_use]
122    pub fn laps(&self) -> &[(String, Duration)] {
123        &self.laps
124    }
125
126    /// Returns the number of recorded laps.
127    #[must_use]
128    pub fn lap_count(&self) -> usize {
129        self.laps.len()
130    }
131
132    /// Returns the lap with the given name, if it exists.
133    #[must_use]
134    pub fn get_lap(&self, name: &str) -> Option<Duration> {
135        self.laps.iter().find(|(n, _)| n == name).map(|(_, d)| *d)
136    }
137
138    /// Returns the sum of all lap durations.
139    #[must_use]
140    pub fn total_lap_time(&self) -> Duration {
141        self.laps.iter().map(|(_, d)| *d).sum()
142    }
143
144    /// Returns timing statistics.
145    #[must_use]
146    pub fn stats(&self) -> TimingStats {
147        if self.laps.is_empty() {
148            return TimingStats {
149                count: 0,
150                total: Duration::ZERO,
151                min: None,
152                max: None,
153                mean: None,
154            };
155        }
156
157        let durations: Vec<_> = self.laps.iter().map(|(_, d)| *d).collect();
158        let total: Duration = durations.iter().sum();
159        let min = durations.iter().min().copied();
160        let max = durations.iter().max().copied();
161        let mean = Some(total / durations.len() as u32);
162
163        TimingStats {
164            count: durations.len(),
165            total,
166            min,
167            max,
168            mean,
169        }
170    }
171
172    /// Formats the stopwatch results as a human-readable report.
173    #[must_use]
174    pub fn report(&self) -> String {
175        let mut lines = Vec::new();
176        lines.push(format!("Total elapsed: {:?}", self.elapsed()));
177        lines.push(format!("Laps: {}", self.lap_count()));
178
179        if !self.laps.is_empty() {
180            lines.push(String::new());
181            for (name, duration) in &self.laps {
182                lines.push(format!("  {name}: {duration:?}"));
183            }
184
185            let stats = self.stats();
186            lines.push(String::new());
187            lines.push("Statistics:".to_string());
188            if let Some(min) = stats.min {
189                lines.push(format!("  Min: {min:?}"));
190            }
191            if let Some(max) = stats.max {
192                lines.push(format!("  Max: {max:?}"));
193            }
194            if let Some(mean) = stats.mean {
195                lines.push(format!("  Mean: {mean:?}"));
196            }
197        }
198
199        lines.join("\n")
200    }
201}
202
203/// Timing statistics for a set of measurements.
204#[derive(Debug, Clone)]
205pub struct TimingStats {
206    /// Number of measurements.
207    pub count: usize,
208    /// Total duration.
209    pub total: Duration,
210    /// Minimum duration.
211    pub min: Option<Duration>,
212    /// Maximum duration.
213    pub max: Option<Duration>,
214    /// Mean duration.
215    pub mean: Option<Duration>,
216}
217
218impl TimingStats {
219    /// Returns whether there are any measurements.
220    #[must_use]
221    pub fn is_empty(&self) -> bool {
222        self.count == 0
223    }
224}
225
226/// A simple timer that fires after a duration.
227///
228/// Useful for timeout testing.
229#[derive(Debug, Clone)]
230pub struct Timer {
231    /// Start time.
232    start: Instant,
233    /// Target duration.
234    duration: Duration,
235}
236
237impl Timer {
238    /// Creates a new timer with the given duration.
239    #[must_use]
240    pub fn new(duration: Duration) -> Self {
241        Self {
242            start: Instant::now(),
243            duration,
244        }
245    }
246
247    /// Creates a timer from seconds.
248    #[must_use]
249    pub fn from_secs(secs: u64) -> Self {
250        Self::new(Duration::from_secs(secs))
251    }
252
253    /// Creates a timer from milliseconds.
254    #[must_use]
255    pub fn from_millis(ms: u64) -> Self {
256        Self::new(Duration::from_millis(ms))
257    }
258
259    /// Returns whether the timer has expired.
260    #[must_use]
261    pub fn is_expired(&self) -> bool {
262        self.start.elapsed() >= self.duration
263    }
264
265    /// Returns the remaining time, or zero if expired.
266    #[must_use]
267    pub fn remaining(&self) -> Duration {
268        let elapsed = self.start.elapsed();
269        if elapsed >= self.duration {
270            Duration::ZERO
271        } else {
272            self.duration - elapsed
273        }
274    }
275
276    /// Resets the timer.
277    pub fn reset(&mut self) {
278        self.start = Instant::now();
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use std::thread::sleep;
286
287    #[test]
288    fn test_measure_duration() {
289        let (result, duration) = measure_duration(|| {
290            sleep(Duration::from_millis(10));
291            42
292        });
293
294        assert_eq!(result, 42);
295        assert!(duration >= Duration::from_millis(10));
296    }
297
298    #[test]
299    fn test_stopwatch_basic() {
300        let mut sw = Stopwatch::new();
301        sleep(Duration::from_millis(10));
302        sw.lap("first");
303        sleep(Duration::from_millis(10));
304        sw.lap("second");
305
306        assert_eq!(sw.lap_count(), 2);
307        assert!(sw.elapsed() >= Duration::from_millis(20));
308    }
309
310    #[test]
311    fn test_stopwatch_get_lap() {
312        let mut sw = Stopwatch::new();
313        sleep(Duration::from_millis(5));
314        sw.lap("test_lap");
315
316        let lap = sw.get_lap("test_lap");
317        assert!(lap.is_some());
318        assert!(lap.unwrap() >= Duration::from_millis(5));
319
320        assert!(sw.get_lap("nonexistent").is_none());
321    }
322
323    #[test]
324    fn test_stopwatch_stats() {
325        let mut sw = Stopwatch::new();
326        sw.lap("a");
327        sw.lap("b");
328        sw.lap("c");
329
330        let stats = sw.stats();
331        assert_eq!(stats.count, 3);
332        assert!(stats.min.is_some());
333        assert!(stats.max.is_some());
334        assert!(stats.mean.is_some());
335    }
336
337    #[test]
338    fn test_stopwatch_empty_stats() {
339        let sw = Stopwatch::new();
340        let stats = sw.stats();
341        assert_eq!(stats.count, 0);
342        assert!(stats.is_empty());
343    }
344
345    #[test]
346    fn test_stopwatch_restart() {
347        let mut sw = Stopwatch::new();
348        sw.lap("first");
349        sw.restart();
350
351        assert_eq!(sw.lap_count(), 0);
352    }
353
354    #[test]
355    fn test_timer_basic() {
356        let timer = Timer::from_millis(50);
357        assert!(!timer.is_expired());
358        assert!(timer.remaining() > Duration::ZERO);
359
360        sleep(Duration::from_millis(60));
361        assert!(timer.is_expired());
362        assert_eq!(timer.remaining(), Duration::ZERO);
363    }
364
365    #[test]
366    fn test_timer_reset() {
367        let mut timer = Timer::from_millis(100);
368        sleep(Duration::from_millis(50));
369        timer.reset();
370
371        // After reset, remaining should be close to full duration
372        assert!(timer.remaining() > Duration::from_millis(80));
373    }
374
375    #[test]
376    fn test_stopwatch_report() {
377        let mut sw = Stopwatch::new();
378        sw.lap("setup");
379        sw.lap("execute");
380        sw.lap("teardown");
381
382        let report = sw.report();
383        assert!(report.contains("Total elapsed"));
384        assert!(report.contains("Laps: 3"));
385        assert!(report.contains("setup"));
386        assert!(report.contains("execute"));
387        assert!(report.contains("teardown"));
388    }
389}