Skip to main content

rpytest_daemon/
fixtures.rs

1//! Session fixture reuse management.
2
3use crate::models::{FixtureConfig, FixtureScope, FixtureState};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::Path;
7use std::sync::{Arc, Mutex};
8use std::time::SystemTime;
9use tracing::debug;
10
11/// Session state for a pytest session with warm fixtures.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct SessionState {
14    pub session_id: String,
15    pub repo_path: String,
16    pub python_path: String,
17    pub fixtures: HashMap<String, FixtureState>,
18    pub created_at: f64,
19    pub last_run_at: f64,
20    pub total_runs: u32,
21    pub enabled: bool,
22}
23
24impl Default for SessionState {
25    fn default() -> Self {
26        let now = SystemTime::now()
27            .duration_since(SystemTime::UNIX_EPOCH)
28            .unwrap()
29            .as_secs_f64();
30
31        SessionState {
32            session_id: String::new(),
33            repo_path: String::new(),
34            python_path: String::new(),
35            fixtures: HashMap::new(),
36            created_at: now,
37            last_run_at: now,
38            total_runs: 0,
39            enabled: false,
40        }
41    }
42}
43
44/// Manages session fixture reuse across test runs.
45#[derive(Debug, Clone)]
46pub struct FixtureManager {
47    /// Sessions by context_id
48    sessions: Arc<Mutex<HashMap<String, SessionState>>>,
49    /// Default configuration
50    config: FixtureConfig,
51    /// Maximum fixture age in seconds
52    max_age_seconds: f64,
53}
54
55impl Default for FixtureManager {
56    fn default() -> Self {
57        FixtureManager {
58            sessions: Arc::new(Mutex::new(HashMap::new())),
59            config: FixtureConfig::default(),
60            max_age_seconds: 600.0, // 10 minutes
61        }
62    }
63}
64
65impl FixtureManager {
66    /// Create a new fixture manager.
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Create a new session state for a context.
72    pub fn create_session(
73        &self,
74        context_id: &str,
75        repo_path: &Path,
76        python_path: &Path,
77    ) -> SessionState {
78        let now = SystemTime::now()
79            .duration_since(SystemTime::UNIX_EPOCH)
80            .unwrap()
81            .as_secs_f64();
82
83        let session = SessionState {
84            session_id: format!("{}-{}", context_id, now as u64),
85            repo_path: repo_path.to_string_lossy().to_string(),
86            python_path: python_path.to_string_lossy().to_string(),
87            fixtures: HashMap::new(),
88            created_at: now,
89            last_run_at: now,
90            total_runs: 0,
91            enabled: false,
92        };
93
94        let mut sessions = self.sessions.lock().unwrap();
95        sessions.insert(context_id.to_string(), session.clone());
96
97        debug!("Created fixture session for {}", context_id);
98        session
99    }
100
101    /// Get session state for a context.
102    pub fn get_session(&self, context_id: &str) -> Option<SessionState> {
103        let sessions = self.sessions.lock().unwrap();
104        sessions.get(context_id).cloned()
105    }
106
107    /// Enable fixture reuse for a context.
108    pub fn enable_reuse(&self, context_id: &str) -> bool {
109        let mut sessions = self.sessions.lock().unwrap();
110        if let Some(session) = sessions.get_mut(context_id) {
111            session.enabled = true;
112            debug!("Enabled fixture reuse for {}", context_id);
113            true
114        } else {
115            false
116        }
117    }
118
119    /// Disable fixture reuse and teardown all fixtures.
120    pub fn disable_reuse(&self, context_id: &str) -> bool {
121        let mut sessions = self.sessions.lock().unwrap();
122        if let Some(session) = sessions.get_mut(context_id) {
123            session.enabled = false;
124            session.fixtures.clear();
125            debug!("Disabled fixture reuse for {}", context_id);
126            true
127        } else {
128            false
129        }
130    }
131
132    /// Mark a fixture as used in the current run.
133    pub fn mark_fixture_used(&self, context_id: &str, name: &str, scope: FixtureScope) {
134        let now = SystemTime::now()
135            .duration_since(SystemTime::UNIX_EPOCH)
136            .unwrap()
137            .as_secs_f64();
138
139        let mut sessions = self.sessions.lock().unwrap();
140        if let Some(session) = sessions.get_mut(context_id) {
141            if !session.enabled {
142                return;
143            }
144
145            let fixture =
146                session
147                    .fixtures
148                    .entry(name.to_string())
149                    .or_insert_with(|| FixtureState {
150                        name: name.to_string(),
151                        scope,
152                        created_at: now,
153                        last_used: now,
154                        use_count: 0,
155                        teardown_pending: false,
156                    });
157            fixture.last_used = now;
158            fixture.use_count += 1;
159        }
160    }
161
162    /// Mark a test run as complete.
163    pub fn mark_run_complete(&self, context_id: &str) {
164        let now = SystemTime::now()
165            .duration_since(SystemTime::UNIX_EPOCH)
166            .unwrap()
167            .as_secs_f64();
168
169        let mut sessions = self.sessions.lock().unwrap();
170        if let Some(session) = sessions.get_mut(context_id) {
171            session.last_run_at = now;
172            session.total_runs += 1;
173        }
174    }
175
176    /// Get fixtures that haven't been used recently.
177    pub fn get_stale_fixtures(&self, context_id: &str) -> Vec<String> {
178        let now = SystemTime::now()
179            .duration_since(SystemTime::UNIX_EPOCH)
180            .unwrap()
181            .as_secs_f64();
182
183        let mut stale = Vec::new();
184
185        let sessions = self.sessions.lock().unwrap();
186        if let Some(session) = sessions.get(context_id) {
187            for (name, state) in &session.fixtures {
188                if now - state.last_used > self.max_age_seconds {
189                    stale.push(name.clone());
190                }
191            }
192        }
193
194        stale
195    }
196
197    /// Clear all sessions.
198    pub fn clear_all(&self) {
199        let mut sessions = self.sessions.lock().unwrap();
200        sessions.clear();
201    }
202
203    /// Remove a specific context's session.
204    pub fn remove_session(&self, context_id: &str) {
205        let mut sessions = self.sessions.lock().unwrap();
206        sessions.remove(context_id);
207    }
208
209    /// Get session count.
210    pub fn session_count(&self) -> usize {
211        self.sessions.lock().unwrap().len()
212    }
213
214    /// Configure fixture reuse settings.
215    pub fn configure(&mut self, config: FixtureConfig) {
216        self.config = config.clone();
217        self.max_age_seconds = config.max_age_seconds;
218    }
219
220    /// Get current configuration.
221    pub fn get_config(&self) -> &FixtureConfig {
222        &self.config
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use std::path::PathBuf;
230
231    #[test]
232    fn test_create_session() {
233        let manager = FixtureManager::new();
234        let repo_path = PathBuf::from("/test/repo");
235        let python_path = PathBuf::from("/usr/bin/python");
236
237        let session = manager.create_session("ctx-1", &repo_path, &python_path);
238
239        assert!(!session.session_id.is_empty());
240        assert_eq!(session.repo_path, "/test/repo");
241        assert!(manager.session_count() == 1);
242    }
243
244    #[test]
245    fn test_enable_disable() {
246        let manager = FixtureManager::new();
247        let repo_path = PathBuf::from("/test/repo");
248        let python_path = PathBuf::from("/usr/bin/python");
249
250        manager.create_session("ctx-1", &repo_path, &python_path);
251        assert!(manager.enable_reuse("ctx-1"));
252        assert!(manager.disable_reuse("ctx-1"));
253    }
254
255    #[test]
256    fn test_mark_fixture_used() {
257        let manager = FixtureManager::new();
258        let repo_path = PathBuf::from("/test/repo");
259        let python_path = PathBuf::from("/usr/bin/python");
260
261        manager.create_session("ctx-1", &repo_path, &python_path);
262        manager.enable_reuse("ctx-1");
263        manager.mark_fixture_used("ctx-1", "db_connection", FixtureScope::Session);
264
265        let session = manager.get_session("ctx-1").unwrap();
266        assert!(session.fixtures.contains_key("db_connection"));
267        assert_eq!(session.fixtures["db_connection"].use_count, 1);
268    }
269}