mlua_probe_core/debug/
breakpoint.rs1use std::collections::HashMap;
4use std::sync::Arc;
5
6use super::error::DebugError;
7use super::types::BreakpointId;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Breakpoint {
12 pub id: BreakpointId,
13 pub source: Arc<str>,
18 pub line: usize,
20 pub condition: Option<String>,
23 pub enabled: bool,
25 #[allow(dead_code)] pub(crate) hit_count: u32,
28}
29
30pub(crate) struct BreakpointRegistry {
36 by_source: HashMap<Arc<str>, HashMap<usize, Breakpoint>>,
37 by_id: HashMap<BreakpointId, (Arc<str>, usize)>,
38 next_id: BreakpointId,
39}
40
41impl BreakpointRegistry {
42 pub fn new() -> Self {
43 Self {
44 by_source: HashMap::new(),
45 by_id: HashMap::new(),
46 next_id: BreakpointId::FIRST,
47 }
48 }
49
50 pub fn add(
63 &mut self,
64 source: String,
65 line: usize,
66 condition: Option<String>,
67 ) -> Result<BreakpointId, DebugError> {
68 let source: Arc<str> = Arc::from(source);
70
71 if let Some(lines) = self.by_source.get_mut(&*source) {
73 if let Some(existing) = lines.remove(&line) {
74 self.by_id.remove(&existing.id);
75 }
76 }
77
78 let id = self.next_id;
79 self.next_id = self.next_id.next().ok_or_else(|| {
80 DebugError::Internal(
81 "breakpoint ID space exhausted (u32::MAX breakpoints created)".into(),
82 )
83 })?;
84
85 let bp = Breakpoint {
86 id,
87 source: Arc::clone(&source),
88 line,
89 condition,
90 enabled: true,
91 hit_count: 0,
92 };
93
94 self.by_source
95 .entry(Arc::clone(&source))
96 .or_default()
97 .insert(line, bp);
98 self.by_id.insert(id, (source, line));
99
100 Ok(id)
101 }
102
103 pub fn remove(&mut self, id: BreakpointId) -> bool {
105 if let Some((source, line)) = self.by_id.remove(&id) {
106 if let Some(lines) = self.by_source.get_mut(&*source) {
107 lines.remove(&line);
108 if lines.is_empty() {
109 self.by_source.remove(&*source);
110 }
111 }
112 true
113 } else {
114 false
115 }
116 }
117
118 pub fn find(&self, source: &str, line: usize) -> Option<&Breakpoint> {
122 self.by_source.get(source).and_then(|m| m.get(&line))
123 }
124
125 #[allow(dead_code)] pub fn record_hit(&mut self, source: &str, line: usize) -> u32 {
128 if let Some(bp) = self
129 .by_source
130 .get_mut(source)
131 .and_then(|m| m.get_mut(&line))
132 {
133 bp.hit_count = bp.hit_count.saturating_add(1);
134 bp.hit_count
135 } else {
136 0
137 }
138 }
139
140 pub fn list(&self) -> Vec<Breakpoint> {
142 self.by_source
143 .values()
144 .flat_map(|m| m.values())
145 .cloned()
146 .collect()
147 }
148
149 #[allow(dead_code)] pub fn has_breakpoints_in(&self, source: &str) -> bool {
152 self.by_source.get(source).is_some_and(|m| !m.is_empty())
153 }
154
155 #[allow(dead_code)] pub fn is_empty(&self) -> bool {
158 self.by_source.is_empty()
159 }
160}
161
162impl Default for BreakpointRegistry {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn add_and_find() {
174 let mut registry = BreakpointRegistry::new();
175 let id = registry.add("@test.lua".into(), 10, None).unwrap();
176
177 let bp = registry.find("@test.lua", 10).unwrap();
178 assert_eq!(bp.id, id);
179 assert_eq!(bp.line, 10);
180 assert!(bp.enabled);
181 assert_eq!(bp.hit_count, 0);
182 }
183
184 #[test]
185 fn add_replaces_existing() {
186 let mut registry = BreakpointRegistry::new();
187 let id1 = registry.add("@test.lua".into(), 10, None).unwrap();
188 let id2 = registry
189 .add("@test.lua".into(), 10, Some("x > 5".into()))
190 .unwrap();
191
192 assert_ne!(id1, id2);
193 assert!(registry.find("@test.lua", 10).unwrap().condition.is_some());
194 assert_eq!(registry.list().len(), 1);
195 }
196
197 #[test]
198 fn remove_returns_false_for_missing() {
199 let mut registry = BreakpointRegistry::new();
200 assert!(!registry.remove(BreakpointId(999)));
201 }
202
203 #[test]
204 fn remove_existing() {
205 let mut registry = BreakpointRegistry::new();
206 let id = registry.add("@test.lua".into(), 5, None).unwrap();
207 assert!(registry.remove(id));
208 assert!(registry.find("@test.lua", 5).is_none());
209 assert!(registry.is_empty());
210 }
211
212 #[test]
213 fn record_hit() {
214 let mut registry = BreakpointRegistry::new();
215 registry.add("@test.lua".into(), 3, None).unwrap();
216 assert_eq!(registry.record_hit("@test.lua", 3), 1);
217 assert_eq!(registry.record_hit("@test.lua", 3), 2);
218 assert_eq!(registry.record_hit("@missing.lua", 1), 0);
219 }
220
221 #[test]
222 fn has_breakpoints_in() {
223 let mut registry = BreakpointRegistry::new();
224 assert!(!registry.has_breakpoints_in("@test.lua"));
225 registry.add("@test.lua".into(), 1, None).unwrap();
226 assert!(registry.has_breakpoints_in("@test.lua"));
227 assert!(!registry.has_breakpoints_in("@other.lua"));
228 }
229}