Skip to main content

cli_speedtest/
cooldown.rs

1// src/cooldown.rs
2
3use std::path::PathBuf;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6pub const DEFAULT_COOLDOWN_SECS: u64 = 300; // 5 minutes
7
8/// Returns the platform-appropriate path for the last-run timestamp file.
9/// Linux/macOS: ~/.local/share/speedtest/last_run
10/// Windows:     %APPDATA%\speedtest\last_run
11pub fn last_run_path() -> Option<PathBuf> {
12    if let Ok(p) = std::env::var("SPEEDTEST_MOCK_DATA_DIR") {
13        return Some(PathBuf::from(p).join("speedtest").join("last_run"));
14    }
15    dirs::data_local_dir().map(|d| d.join("speedtest").join("last_run"))
16}
17
18/// Returns Some(seconds_remaining) if the cooldown is still active,
19/// or None if the cooldown has elapsed or no previous run was recorded.
20pub fn cooldown_remaining(cooldown_secs: u64) -> Option<u64> {
21    let path = last_run_path()?;
22    let contents = std::fs::read_to_string(&path).ok()?;
23    let last_run_ts: u64 = contents.trim().parse().ok()?;
24    let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
25    let elapsed = now.saturating_sub(last_run_ts);
26    if elapsed < cooldown_secs {
27        Some(cooldown_secs - elapsed)
28    } else {
29        None
30    }
31}
32
33/// Writes the current Unix timestamp to the last-run file.
34/// Creates the directory if it does not exist.
35/// Called only on successful test completion - failed runs do not reset
36/// the cooldown clock.
37pub fn record_successful_run() -> anyhow::Result<()> {
38    let path =
39        last_run_path().ok_or_else(|| anyhow::anyhow!("Could not determine data directory"))?;
40    if let Some(parent) = path.parent() {
41        std::fs::create_dir_all(parent)?;
42    }
43    let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
44    std::fs::write(&path, now.to_string())?;
45    Ok(())
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use std::fs;
52    use std::sync::Mutex;
53    use tempfile::TempDir;
54
55    static ENV_LOCK: Mutex<()> = Mutex::new(());
56
57    fn setup_test_env() -> TempDir {
58        let temp = TempDir::new().expect("Failed to create temp dir");
59        unsafe {
60            std::env::set_var("SPEEDTEST_MOCK_DATA_DIR", temp.path());
61        }
62        temp
63    }
64
65    #[test]
66    fn cooldown_none_when_no_file() {
67        let _guard = ENV_LOCK.lock().unwrap();
68        let _temp = setup_test_env();
69        assert_eq!(cooldown_remaining(DEFAULT_COOLDOWN_SECS), None);
70    }
71
72    #[test]
73    fn cooldown_none_when_elapsed() {
74        let _guard = ENV_LOCK.lock().unwrap();
75        let _temp = setup_test_env();
76        let path = last_run_path().unwrap();
77        fs::create_dir_all(path.parent().unwrap()).unwrap();
78
79        let old_time = SystemTime::now()
80            .duration_since(UNIX_EPOCH)
81            .unwrap()
82            .as_secs()
83            - 1000;
84        fs::write(&path, old_time.to_string()).unwrap();
85
86        assert_eq!(cooldown_remaining(DEFAULT_COOLDOWN_SECS), None);
87    }
88
89    #[test]
90    fn cooldown_some_when_active() {
91        let _guard = ENV_LOCK.lock().unwrap();
92        let _temp = setup_test_env();
93        let path = last_run_path().unwrap();
94        fs::create_dir_all(path.parent().unwrap()).unwrap();
95
96        let recent_time = SystemTime::now()
97            .duration_since(UNIX_EPOCH)
98            .unwrap()
99            .as_secs()
100            - 100;
101        fs::write(&path, recent_time.to_string()).unwrap();
102
103        let remaining = cooldown_remaining(DEFAULT_COOLDOWN_SECS);
104        assert!(remaining.is_some());
105        assert!(remaining.unwrap() <= 200); // 300 - 100
106    }
107
108    #[test]
109    fn record_run_creates_file() {
110        let _guard = ENV_LOCK.lock().unwrap();
111        let _temp = setup_test_env();
112
113        // Ensure file does not exist
114        let path = last_run_path().unwrap();
115        assert!(!path.exists());
116
117        record_successful_run().expect("Should record successfully");
118
119        assert!(path.exists());
120        let content = fs::read_to_string(&path).unwrap();
121        assert!(content.trim().parse::<u64>().is_ok());
122    }
123
124    #[test]
125    fn record_run_creates_missing_dirs() {
126        let _guard = ENV_LOCK.lock().unwrap();
127        let temp = setup_test_env();
128
129        // Remove the speedtest directory if it somehow exists to ensure we create it
130        let speedtest_dir = temp.path().join("speedtest");
131        if speedtest_dir.exists() {
132            fs::remove_dir_all(&speedtest_dir).unwrap();
133        }
134
135        record_successful_run().expect("Should record successfully with missing parent dir");
136
137        let path = last_run_path().unwrap();
138        assert!(path.exists());
139    }
140}