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
390    // =========================================================================
391    // Additional coverage tests (bd-1fnm)
392    // =========================================================================
393
394    #[test]
395    fn stopwatch_stopped_constructor() {
396        let sw = Stopwatch::stopped();
397        assert_eq!(sw.lap_count(), 0);
398        assert!(sw.laps().is_empty());
399    }
400
401    #[test]
402    fn stopwatch_default_matches_new() {
403        let def = Stopwatch::default();
404        let new = Stopwatch::new();
405        assert_eq!(def.lap_count(), new.lap_count());
406    }
407
408    #[test]
409    fn stopwatch_since_last_lap() {
410        let sw = Stopwatch::new();
411        let since = sw.since_last_lap();
412        assert!(since < Duration::from_secs(1));
413    }
414
415    #[test]
416    fn stopwatch_total_lap_time() {
417        let mut sw = Stopwatch::new();
418        sw.lap("a");
419        sw.lap("b");
420        let total = sw.total_lap_time();
421        assert!(total > Duration::ZERO);
422    }
423
424    #[test]
425    fn stopwatch_debug_and_clone() {
426        let mut sw = Stopwatch::new();
427        sw.lap("x");
428        let debug = format!("{sw:?}");
429        assert!(debug.contains("Stopwatch"));
430
431        let cloned = sw.clone();
432        assert_eq!(cloned.lap_count(), 1);
433    }
434
435    #[test]
436    fn timing_stats_debug_clone_and_is_empty() {
437        let mut sw = Stopwatch::new();
438        sw.lap("a");
439        let stats = sw.stats();
440        let debug = format!("{stats:?}");
441        assert!(debug.contains("TimingStats"));
442
443        let cloned = stats.clone();
444        assert_eq!(cloned.count, 1);
445        assert!(!cloned.is_empty());
446    }
447
448    #[test]
449    fn timer_from_secs() {
450        let timer = Timer::from_secs(60);
451        assert!(!timer.is_expired());
452        assert!(timer.remaining() > Duration::from_secs(59));
453    }
454
455    #[test]
456    fn timer_debug_and_clone() {
457        let timer = Timer::from_millis(100);
458        let debug = format!("{timer:?}");
459        assert!(debug.contains("Timer"));
460
461        let cloned = timer.clone();
462        assert!(!cloned.is_expired());
463    }
464
465    #[test]
466    fn report_empty_laps() {
467        let sw = Stopwatch::new();
468        let report = sw.report();
469        assert!(report.contains("Laps: 0"));
470        assert!(!report.contains("Statistics"));
471    }
472}