Skip to main content

dscode_dap/
manager.rs

1use crate::types::{Breakpoint, DebugSession, DebugState, SourceBreakpoint};
2use std::collections::HashMap;
3use std::sync::{Arc, Mutex};
4use tracing::info;
5use uuid::Uuid;
6
7pub struct DebugManager {
8    sessions: Arc<Mutex<HashMap<String, DebugSession>>>,
9    breakpoints: Arc<Mutex<HashMap<String, Vec<Breakpoint>>>>,
10}
11
12impl Default for DebugManager {
13    fn default() -> Self {
14        Self::new()
15    }
16}
17
18impl DebugManager {
19    pub fn new() -> Self {
20        DebugManager {
21            sessions: Arc::new(Mutex::new(HashMap::new())),
22            breakpoints: Arc::new(Mutex::new(HashMap::new())),
23        }
24    }
25
26    pub fn create_session(&self, name: String, adapter_type: String) -> Result<String, String> {
27        let session_id = Uuid::new_v4().to_string();
28        let session =
29            DebugSession { id: session_id.clone(), name, state: DebugState::Stopped, adapter_type };
30
31        let mut sessions = self.sessions.lock().map_err(|e| e.to_string())?;
32        sessions.insert(session_id.clone(), session);
33
34        info!(id = %session_id, "Created debug session");
35        Ok(session_id)
36    }
37
38    pub fn get_session(&self, session_id: &str) -> Result<DebugSession, String> {
39        let sessions = self.sessions.lock().map_err(|e| e.to_string())?;
40        sessions.get(session_id).cloned().ok_or_else(|| format!("Session {} not found", session_id))
41    }
42
43    pub fn list_sessions(&self) -> Result<Vec<DebugSession>, String> {
44        let sessions = self.sessions.lock().map_err(|e| e.to_string())?;
45        Ok(sessions.values().cloned().collect())
46    }
47
48    pub fn update_session_state(&self, session_id: &str, state: DebugState) -> Result<(), String> {
49        let mut sessions = self.sessions.lock().map_err(|e| e.to_string())?;
50        if let Some(session) = sessions.get_mut(session_id) {
51            session.state = state;
52            info!(id = %session_id, "Updated session state");
53            Ok(())
54        } else {
55            Err(format!("Session {} not found", session_id))
56        }
57    }
58
59    pub fn terminate_session(&self, session_id: &str) -> Result<(), String> {
60        let mut sessions = self.sessions.lock().map_err(|e| e.to_string())?;
61        sessions.remove(session_id);
62        info!(id = %session_id, "Terminated debug session");
63        Ok(())
64    }
65
66    pub fn set_breakpoints(
67        &self, file_path: String, breakpoints: Vec<SourceBreakpoint>,
68    ) -> Result<(), String> {
69        // Convert SourceBreakpoint to Breakpoint
70        let converted_bps: Vec<Breakpoint> = breakpoints
71            .into_iter()
72            .map(|sbp| Breakpoint {
73                id: None,
74                verified: false,
75                message: None,
76                source: None,
77                line: Some(sbp.line),
78                column: sbp.column,
79            })
80            .collect();
81
82        let mut bps = self.breakpoints.lock().map_err(|e| e.to_string())?;
83        bps.insert(file_path.clone(), converted_bps);
84        info!(path = %file_path, "Set breakpoints");
85        Ok(())
86    }
87
88    pub fn get_breakpoints(&self, file_path: &str) -> Result<Vec<Breakpoint>, String> {
89        let bps = self.breakpoints.lock().map_err(|e| e.to_string())?;
90        Ok(bps.get(file_path).cloned().unwrap_or_default())
91    }
92
93    pub fn clear_breakpoints(&self, file_path: &str) -> Result<(), String> {
94        let mut bps = self.breakpoints.lock().map_err(|e| e.to_string())?;
95        bps.remove(file_path);
96        info!(path = %file_path, "Cleared breakpoints");
97        Ok(())
98    }
99
100    pub fn get_all_breakpoints(&self) -> Result<HashMap<String, Vec<Breakpoint>>, String> {
101        let bps = self.breakpoints.lock().map_err(|e| e.to_string())?;
102        Ok(bps.clone())
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::types::DebugState;
110
111    #[test]
112    fn test_debug_manager_new() {
113        let manager = DebugManager::new();
114        // A new manager should have no sessions
115        let sessions = manager.list_sessions().unwrap();
116        assert!(sessions.is_empty(), "New manager should have no sessions");
117    }
118
119    #[test]
120    fn test_debug_manager_create_session() {
121        let manager = DebugManager::new();
122        let session_id = manager
123            .create_session("Test Session".to_string(), "cppdbg".to_string())
124            .unwrap();
125        assert!(!session_id.is_empty(), "Session ID should not be empty");
126    }
127
128    #[test]
129    fn test_debug_manager_get_session() {
130        let manager = DebugManager::new();
131        let session_id = manager
132            .create_session("My Debug".to_string(), "python".to_string())
133            .unwrap();
134
135        let session = manager.get_session(&session_id).unwrap();
136        assert_eq!(session.id, session_id);
137        assert_eq!(session.name, "My Debug");
138        assert_eq!(session.adapter_type, "python");
139        assert_eq!(session.state, DebugState::Stopped);
140    }
141
142    #[test]
143    fn test_debug_manager_get_nonexistent_session() {
144        let manager = DebugManager::new();
145        let result = manager.get_session("nonexistent-id");
146        assert!(result.is_err());
147        assert!(result.unwrap_err().contains("not found"));
148    }
149
150    #[test]
151    fn test_debug_manager_list_sessions() {
152        let manager = DebugManager::new();
153
154        // Initially empty
155        assert!(manager.list_sessions().unwrap().is_empty());
156
157        // Create multiple sessions
158        let id1 = manager.create_session("S1".to_string(), "cppdbg".to_string()).unwrap();
159        let id2 = manager.create_session("S2".to_string(), "python".to_string()).unwrap();
160
161        let sessions = manager.list_sessions().unwrap();
162        assert_eq!(sessions.len(), 2);
163
164        let ids: Vec<&str> = sessions.iter().map(|s| s.id.as_str()).collect();
165        assert!(ids.contains(&id1.as_str()));
166        assert!(ids.contains(&id2.as_str()));
167    }
168
169    #[test]
170    fn test_debug_manager_update_session_state() {
171        let manager = DebugManager::new();
172        let session_id = manager
173            .create_session("State Test".to_string(), "go".to_string())
174            .unwrap();
175
176        // Initially Stopped
177        let session = manager.get_session(&session_id).unwrap();
178        assert_eq!(session.state, DebugState::Stopped);
179
180        // Update to Running
181        manager
182            .update_session_state(&session_id, DebugState::Running)
183            .unwrap();
184        let session = manager.get_session(&session_id).unwrap();
185        assert_eq!(session.state, DebugState::Running);
186
187        // Update to Paused
188        manager
189            .update_session_state(&session_id, DebugState::Paused)
190            .unwrap();
191        let session = manager.get_session(&session_id).unwrap();
192        assert_eq!(session.state, DebugState::Paused);
193
194        // Update to Terminated
195        manager
196            .update_session_state(&session_id, DebugState::Terminated)
197            .unwrap();
198        let session = manager.get_session(&session_id).unwrap();
199        assert_eq!(session.state, DebugState::Terminated);
200    }
201
202    #[test]
203    fn test_debug_manager_update_nonexistent_session_state() {
204        let manager = DebugManager::new();
205        let result = manager.update_session_state("no-such-id", DebugState::Running);
206        assert!(result.is_err());
207        assert!(result.unwrap_err().contains("not found"));
208    }
209
210    #[test]
211    fn test_debug_manager_terminate_session() {
212        let manager = DebugManager::new();
213        let session_id = manager
214            .create_session("To Terminate".to_string(), "rust".to_string())
215            .unwrap();
216
217        // Verify session exists
218        assert!(manager.get_session(&session_id).is_ok());
219
220        // Terminate it
221        manager.terminate_session(&session_id).unwrap();
222
223        // Verify it's gone
224        assert!(manager.get_session(&session_id).is_err());
225    }
226
227    #[test]
228    fn test_debug_manager_terminate_nonexistent_session() {
229        let manager = DebugManager::new();
230        // terminate_session on a nonexistent ID is a no-op (HashMap::remove)
231        let result = manager.terminate_session("nonexistent");
232        assert!(result.is_ok());
233    }
234
235    #[test]
236    fn test_debug_manager_set_and_get_breakpoints() {
237        let manager = DebugManager::new();
238
239        let breakpoints = vec![SourceBreakpoint {
240            line: 10,
241            column: None,
242            condition: None,
243            hit_condition: None,
244            log_message: None,
245        }];
246
247        manager
248            .set_breakpoints("/src/main.rs".to_string(), breakpoints)
249            .unwrap();
250
251        let result = manager.get_breakpoints("/src/main.rs").unwrap();
252        assert_eq!(result.len(), 1);
253        assert_eq!(result[0].line, Some(10));
254        assert!(!result[0].verified);
255    }
256
257    #[test]
258    fn test_debug_manager_get_breakpoints_missing_file() {
259        let manager = DebugManager::new();
260        let result = manager.get_breakpoints("/nonexistent/file.rs").unwrap();
261        assert!(result.is_empty(), "Missing file should return empty breakpoints");
262    }
263
264    #[test]
265    fn test_debug_manager_clear_breakpoints() {
266        let manager = DebugManager::new();
267
268        let breakpoints = vec![SourceBreakpoint {
269            line: 5,
270            column: Some(2),
271            condition: Some("x > 0".to_string()),
272            hit_condition: None,
273            log_message: None,
274        }];
275
276        manager
277            .set_breakpoints("/app.py".to_string(), breakpoints)
278            .unwrap();
279        assert_eq!(manager.get_breakpoints("/app.py").unwrap().len(), 1);
280
281        manager.clear_breakpoints("/app.py").unwrap();
282        assert!(manager.get_breakpoints("/app.py").unwrap().is_empty());
283    }
284
285    #[test]
286    fn test_debug_manager_get_all_breakpoints() {
287        let manager = DebugManager::new();
288
289        assert!(manager.get_all_breakpoints().unwrap().is_empty());
290
291        manager
292            .set_breakpoints(
293                "/a.rs".to_string(),
294                vec![SourceBreakpoint {
295                    line: 1,
296                    column: None,
297                    condition: None,
298                    hit_condition: None,
299                    log_message: None,
300                }],
301            )
302            .unwrap();
303
304        manager
305            .set_breakpoints(
306                "/b.rs".to_string(),
307                vec![SourceBreakpoint {
308                    line: 2,
309                    column: None,
310                    condition: None,
311                    hit_condition: None,
312                    log_message: None,
313                }],
314            )
315            .unwrap();
316
317        let all = manager.get_all_breakpoints().unwrap();
318        assert_eq!(all.len(), 2);
319        assert!(all.contains_key("/a.rs"));
320        assert!(all.contains_key("/b.rs"));
321    }
322
323    #[test]
324    fn test_debug_manager_set_breakpoints_overwrites() {
325        let manager = DebugManager::new();
326
327        let bp1 = vec![SourceBreakpoint {
328            line: 1,
329            column: None,
330            condition: None,
331            hit_condition: None,
332            log_message: None,
333        }];
334        let bp2 = vec![
335            SourceBreakpoint {
336                line: 10,
337                column: None,
338                condition: None,
339                hit_condition: None,
340                log_message: None,
341            },
342            SourceBreakpoint {
343                line: 20,
344                column: Some(4),
345                condition: None,
346                hit_condition: None,
347                log_message: None,
348            },
349        ];
350
351        manager.set_breakpoints("/file.rs".to_string(), bp1).unwrap();
352        manager.set_breakpoints("/file.rs".to_string(), bp2).unwrap();
353
354        let result = manager.get_breakpoints("/file.rs").unwrap();
355        assert_eq!(result.len(), 2);
356    }
357
358    #[test]
359    fn test_debug_manager_multiple_sessions_independent() {
360        let manager = DebugManager::new();
361
362        let id1 = manager.create_session("S1".to_string(), "cppdbg".to_string()).unwrap();
363        let id2 = manager.create_session("S2".to_string(), "python".to_string()).unwrap();
364
365        manager.update_session_state(&id1, DebugState::Running).unwrap();
366        manager.update_session_state(&id2, DebugState::Paused).unwrap();
367
368        let s1 = manager.get_session(&id1).unwrap();
369        let s2 = manager.get_session(&id2).unwrap();
370        assert_eq!(s1.state, DebugState::Running);
371        assert_eq!(s2.state, DebugState::Paused);
372
373        // Terminating one should not affect the other
374        manager.terminate_session(&id1).unwrap();
375        assert!(manager.get_session(&id1).is_err());
376        assert!(manager.get_session(&id2).is_ok());
377    }
378}