ryo_mutations/debugger/
marker.rs1use serde::{Deserialize, Serialize};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12pub const MARKER_PREFIX: &str = "ryo-debug";
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct DebugMarker {
34 pub session_id: String,
36 pub timestamp: u64,
38 pub description: Option<String>,
40}
41
42impl DebugMarker {
43 pub fn new() -> Self {
45 Self {
46 session_id: Self::generate_session_id(),
47 timestamp: Self::current_timestamp(),
48 description: None,
49 }
50 }
51
52 pub fn with_session(session_id: impl Into<String>) -> Self {
57 Self {
58 session_id: session_id.into(),
59 timestamp: Self::current_timestamp(),
60 description: None,
61 }
62 }
63
64 pub fn with_description(mut self, description: impl Into<String>) -> Self {
66 self.description = Some(description.into());
67 self
68 }
69
70 pub fn to_comment(&self) -> String {
74 let desc = self.description.as_deref().unwrap_or("");
75 format!(
76 "/* {}:{}:{}:{} */",
77 MARKER_PREFIX, self.session_id, self.timestamp, desc
78 )
79 }
80
81 pub fn to_marker_string(&self) -> String {
87 let desc = self.description.as_deref().unwrap_or("");
88 format!(
89 "{}:{}:{}:{}",
90 MARKER_PREFIX, self.session_id, self.timestamp, desc
91 )
92 }
93
94 pub fn from_comment(s: &str) -> Option<Self> {
102 let s = s.trim();
103
104 let inner = if s.starts_with("/*") && s.ends_with("*/") {
106 s.strip_prefix("/*")?.strip_suffix("*/")?.trim()
107 } else {
108 s
109 };
110
111 let rest = inner.strip_prefix(MARKER_PREFIX)?.strip_prefix(':')?;
113
114 let parts: Vec<&str> = rest.splitn(3, ':').collect();
116 if parts.len() < 2 {
117 return None;
118 }
119
120 let session_id = parts[0].to_string();
121 let timestamp = parts[1].parse().ok()?;
122 let description = parts
123 .get(2)
124 .filter(|s| !s.is_empty())
125 .map(|s| s.to_string());
126
127 Some(Self {
128 session_id,
129 timestamp,
130 description,
131 })
132 }
133
134 pub fn from_string_literal(s: &str) -> Option<Self> {
138 let s = s.trim();
139 let inner = s.strip_prefix('"')?.strip_suffix('"')?;
140 Self::from_comment(inner)
141 }
142
143 pub fn contains_marker(s: &str) -> bool {
147 s.contains(&format!("/* {}:", MARKER_PREFIX))
148 || s.contains(&format!("\"{}:", MARKER_PREFIX))
149 }
150
151 pub fn extract_markers(s: &str) -> Vec<Self> {
153 let mut markers = Vec::new();
154 let prefix = format!("/* {}:", MARKER_PREFIX);
155
156 let mut search_start = 0;
157 while let Some(start) = s[search_start..].find(&prefix) {
158 let abs_start = search_start + start;
159 if let Some(end_offset) = s[abs_start..].find("*/") {
160 let end = abs_start + end_offset + 2;
161 if let Some(marker) = Self::from_comment(&s[abs_start..end]) {
162 markers.push(marker);
163 }
164 search_start = end;
165 } else {
166 break;
167 }
168 }
169
170 markers
171 }
172
173 fn generate_session_id() -> String {
175 use std::collections::hash_map::RandomState;
176 use std::hash::{BuildHasher, Hasher};
177
178 let state = RandomState::new();
179 let mut hasher = state.build_hasher();
180 hasher.write_u64(Self::current_timestamp());
181 hasher.write_usize(std::process::id() as usize);
182
183 format!("{:08x}", hasher.finish() as u32)
184 }
185
186 fn current_timestamp() -> u64 {
188 SystemTime::now()
189 .duration_since(UNIX_EPOCH)
190 .map(|d| d.as_secs())
191 .unwrap_or(0)
192 }
193}
194
195impl Default for DebugMarker {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201#[derive(Debug, Clone)]
206pub struct DebugSession {
207 session_id: String,
208}
209
210impl DebugSession {
211 pub fn new() -> Self {
213 Self {
214 session_id: DebugMarker::generate_session_id(),
215 }
216 }
217
218 pub fn with_id(session_id: impl Into<String>) -> Self {
220 Self {
221 session_id: session_id.into(),
222 }
223 }
224
225 pub fn id(&self) -> &str {
227 &self.session_id
228 }
229
230 pub fn marker(&self) -> DebugMarker {
232 DebugMarker::with_session(&self.session_id)
233 }
234
235 pub fn marker_with_desc(&self, description: impl Into<String>) -> DebugMarker {
237 self.marker().with_description(description)
238 }
239}
240
241impl Default for DebugSession {
242 fn default() -> Self {
243 Self::new()
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_marker_roundtrip() {
253 let marker = DebugMarker::with_session("test123").with_description("after map");
254
255 let comment = marker.to_comment();
256 assert!(comment.contains("ryo-debug:test123:"));
257 assert!(comment.contains(":after map"));
258
259 let parsed = DebugMarker::from_comment(&comment).unwrap();
260 assert_eq!(parsed.session_id, "test123");
261 assert_eq!(parsed.description, Some("after map".to_string()));
262 }
263
264 #[test]
265 fn test_marker_without_description() {
266 let marker = DebugMarker::with_session("abc");
267 let comment = marker.to_comment();
268
269 let parsed = DebugMarker::from_comment(&comment).unwrap();
270 assert_eq!(parsed.session_id, "abc");
271 assert!(parsed.description.is_none());
272 }
273
274 #[test]
275 fn test_contains_marker() {
276 let code = r#"
277 items.iter()
278 .inspect(|x| { /* ryo-debug:abc:123:test */ dbg!(x); })
279 .collect()
280 "#;
281 assert!(DebugMarker::contains_marker(code));
282
283 let code_without = "items.iter().collect()";
284 assert!(!DebugMarker::contains_marker(code_without));
285 }
286
287 #[test]
288 fn test_extract_markers() {
289 let code = r#"
290 items /* ryo-debug:s1:100:first */ .iter()
291 .inspect(|x| { /* ryo-debug:s1:101:second */ dbg!(x); })
292 .collect()
293 "#;
294
295 let markers = DebugMarker::extract_markers(code);
296 assert_eq!(markers.len(), 2);
297 assert_eq!(markers[0].session_id, "s1");
298 assert_eq!(markers[0].description, Some("first".to_string()));
299 assert_eq!(markers[1].session_id, "s1");
300 assert_eq!(markers[1].description, Some("second".to_string()));
301 }
302
303 #[test]
304 fn test_session() {
305 let session = DebugSession::new();
306 let m1 = session.marker_with_desc("first");
307 let m2 = session.marker_with_desc("second");
308
309 assert_eq!(m1.session_id, m2.session_id);
310 assert_eq!(m1.session_id, session.id());
311 }
312}