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