Skip to main content

reinhardt_test/
debug.rs

1use reinhardt_core::security::escape_html_content;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6use tokio::sync::RwLock;
7
8/// Debug panel information
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DebugPanel {
11	pub title: String,
12	pub content: Vec<DebugEntry>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type")]
17pub enum DebugEntry {
18	KeyValue {
19		key: String,
20		value: String,
21	},
22	Table {
23		headers: Vec<String>,
24		rows: Vec<Vec<String>>,
25	},
26	Code {
27		language: String,
28		code: String,
29	},
30	Text {
31		text: String,
32	},
33}
34
35/// Request/Response timing information
36#[derive(Debug, Clone, Serialize)]
37pub struct TimingInfo {
38	pub total_time: Duration,
39	pub sql_time: Duration,
40	pub sql_queries: usize,
41	pub cache_hits: usize,
42	pub cache_misses: usize,
43}
44
45/// SQL query record
46#[derive(Debug, Clone, Serialize)]
47pub struct SqlQuery {
48	pub query: String,
49	pub duration: Duration,
50	pub stack_trace: Vec<String>,
51}
52
53/// Debug toolbar
54pub struct DebugToolbar {
55	panels: Arc<RwLock<HashMap<String, DebugPanel>>>,
56	timing: Arc<RwLock<TimingInfo>>,
57	sql_queries: Arc<RwLock<Vec<SqlQuery>>>,
58	start_time: Instant,
59	enabled: bool,
60}
61
62impl DebugToolbar {
63	/// Create a new debug toolbar
64	///
65	/// # Examples
66	///
67	/// ```
68	/// use reinhardt_test::debug::DebugToolbar;
69	///
70	/// let toolbar = DebugToolbar::new();
71	/// assert!(toolbar.is_enabled());
72	/// ```
73	pub fn new() -> Self {
74		Self {
75			panels: Arc::new(RwLock::new(HashMap::new())),
76			timing: Arc::new(RwLock::new(TimingInfo {
77				total_time: Duration::from_secs(0),
78				sql_time: Duration::from_secs(0),
79				sql_queries: 0,
80				cache_hits: 0,
81				cache_misses: 0,
82			})),
83			sql_queries: Arc::new(RwLock::new(Vec::new())),
84			start_time: Instant::now(),
85			enabled: true,
86		}
87	}
88	/// Enable or disable the debug toolbar
89	///
90	/// # Examples
91	///
92	/// ```
93	/// use reinhardt_test::debug::DebugToolbar;
94	///
95	/// let mut toolbar = DebugToolbar::new();
96	/// toolbar.set_enabled(false);
97	/// assert!(!toolbar.is_enabled());
98	/// ```
99	pub fn set_enabled(&mut self, enabled: bool) {
100		self.enabled = enabled;
101	}
102	/// Check if the debug toolbar is enabled
103	///
104	/// # Examples
105	///
106	/// ```
107	/// use reinhardt_test::debug::DebugToolbar;
108	///
109	/// let toolbar = DebugToolbar::new();
110	/// assert!(toolbar.is_enabled());
111	/// ```
112	pub fn is_enabled(&self) -> bool {
113		self.enabled
114	}
115	/// Add a debug panel
116	///
117	/// # Examples
118	///
119	/// ```
120	/// use reinhardt_test::debug::{DebugToolbar, DebugPanel, DebugEntry};
121	///
122	/// # tokio_test::block_on(async {
123	/// let toolbar = DebugToolbar::new();
124	/// let panel = DebugPanel {
125	///     title: "Test Panel".to_string(),
126	///     content: vec![DebugEntry::Text { text: "Hello".to_string() }],
127	/// };
128	/// toolbar.add_panel("test".to_string(), panel).await;
129	/// let panels = toolbar.get_panels().await;
130	/// assert!(panels.contains_key("test"));
131	/// # });
132	/// ```
133	pub async fn add_panel(&self, id: String, panel: DebugPanel) {
134		if !self.enabled {
135			return;
136		}
137		self.panels.write().await.insert(id, panel);
138	}
139	/// Record SQL query
140	///
141	/// # Examples
142	///
143	/// ```
144	/// use reinhardt_test::debug::DebugToolbar;
145	/// use std::time::Duration;
146	///
147	/// # tokio_test::block_on(async {
148	/// let toolbar = DebugToolbar::new();
149	/// toolbar.record_sql_query("SELECT * FROM users".to_string(), Duration::from_millis(10)).await;
150	/// let timing = toolbar.get_timing().await;
151	/// assert_eq!(timing.sql_queries, 1);
152	/// assert!(timing.sql_time >= Duration::from_millis(10));
153	/// # });
154	/// ```
155	pub async fn record_sql_query(&self, query: String, duration: Duration) {
156		if !self.enabled {
157			return;
158		}
159
160		let sql_query = SqlQuery {
161			query,
162			duration,
163			stack_trace: vec![],
164		};
165
166		self.sql_queries.write().await.push(sql_query);
167
168		let mut timing = self.timing.write().await;
169		timing.sql_queries += 1;
170		timing.sql_time += duration;
171	}
172	/// Record cache hit
173	///
174	/// # Examples
175	///
176	/// ```
177	/// use reinhardt_test::debug::DebugToolbar;
178	///
179	/// # tokio_test::block_on(async {
180	/// let toolbar = DebugToolbar::new();
181	/// toolbar.record_cache_hit().await;
182	/// let timing = toolbar.get_timing().await;
183	/// assert_eq!(timing.cache_hits, 1);
184	/// # });
185	/// ```
186	pub async fn record_cache_hit(&self) {
187		if !self.enabled {
188			return;
189		}
190		self.timing.write().await.cache_hits += 1;
191	}
192	/// Record cache miss
193	///
194	/// # Examples
195	///
196	/// ```
197	/// use reinhardt_test::debug::DebugToolbar;
198	///
199	/// # tokio_test::block_on(async {
200	/// let toolbar = DebugToolbar::new();
201	/// toolbar.record_cache_miss().await;
202	/// let timing = toolbar.get_timing().await;
203	/// assert_eq!(timing.cache_misses, 1);
204	/// # });
205	/// ```
206	pub async fn record_cache_miss(&self) {
207		if !self.enabled {
208			return;
209		}
210		self.timing.write().await.cache_misses += 1;
211	}
212	/// Finalize timing information
213	///
214	/// # Examples
215	///
216	/// ```
217	/// use reinhardt_test::debug::DebugToolbar;
218	/// use std::time::Duration;
219	///
220	/// # tokio_test::block_on(async {
221	/// let toolbar = DebugToolbar::new();
222	/// // Simulate some work
223	/// tokio::time::sleep(Duration::from_millis(10)).await;
224	/// toolbar.finalize().await;
225	/// let timing = toolbar.get_timing().await;
226	/// assert!(timing.total_time >= Duration::from_millis(10));
227	/// # });
228	/// ```
229	pub async fn finalize(&self) {
230		if !self.enabled {
231			return;
232		}
233		self.timing.write().await.total_time = self.start_time.elapsed();
234	}
235	/// Get all panels
236	///
237	/// # Examples
238	///
239	/// ```
240	/// use reinhardt_test::debug::{DebugToolbar, DebugPanel, DebugEntry};
241	///
242	/// # tokio_test::block_on(async {
243	/// let toolbar = DebugToolbar::new();
244	/// let panel = DebugPanel {
245	///     title: "Test".to_string(),
246	///     content: vec![],
247	/// };
248	/// toolbar.add_panel("test".to_string(), panel).await;
249	/// let panels = toolbar.get_panels().await;
250	/// assert_eq!(panels.len(), 1);
251	/// # });
252	/// ```
253	pub async fn get_panels(&self) -> HashMap<String, DebugPanel> {
254		self.panels.read().await.clone()
255	}
256	/// Get timing info
257	///
258	/// # Examples
259	///
260	/// ```
261	/// use reinhardt_test::debug::DebugToolbar;
262	///
263	/// # tokio_test::block_on(async {
264	/// let toolbar = DebugToolbar::new();
265	/// let timing = toolbar.get_timing().await;
266	/// assert_eq!(timing.sql_queries, 0);
267	/// assert_eq!(timing.cache_hits, 0);
268	/// # });
269	/// ```
270	pub async fn get_timing(&self) -> TimingInfo {
271		self.timing.read().await.clone()
272	}
273	/// Get SQL queries
274	///
275	/// # Examples
276	///
277	/// ```
278	/// use reinhardt_test::debug::DebugToolbar;
279	/// use std::time::Duration;
280	///
281	/// # tokio_test::block_on(async {
282	/// let toolbar = DebugToolbar::new();
283	/// toolbar.record_sql_query("SELECT 1".to_string(), Duration::from_millis(5)).await;
284	/// let queries = toolbar.get_sql_queries().await;
285	/// assert_eq!(queries.len(), 1);
286	/// assert_eq!(queries[0].query, "SELECT 1");
287	/// # });
288	/// ```
289	pub async fn get_sql_queries(&self) -> Vec<SqlQuery> {
290		self.sql_queries.read().await.clone()
291	}
292	/// Render as HTML
293	///
294	/// # Examples
295	///
296	/// ```
297	/// use reinhardt_test::debug::DebugToolbar;
298	///
299	/// # tokio_test::block_on(async {
300	/// let toolbar = DebugToolbar::new();
301	/// toolbar.finalize().await;
302	/// let html = toolbar.render_html().await;
303	/// assert!(html.contains("debug-toolbar"));
304	/// assert!(html.contains("Timing"));
305	/// # });
306	/// ```
307	pub async fn render_html(&self) -> String {
308		if !self.enabled {
309			return String::new();
310		}
311
312		let panels = self.get_panels().await;
313		let timing = self.get_timing().await;
314		let queries = self.get_sql_queries().await;
315
316		format!(
317			r#"
318<div class="debug-toolbar">
319    <div class="timing">
320        <h3>Timing</h3>
321        <p>Total: {:?}</p>
322        <p>SQL: {:?} ({} queries)</p>
323        <p>Cache: {} hits, {} misses</p>
324    </div>
325    <div class="sql-queries">
326        <h3>SQL Queries</h3>
327        <ul>
328            {}
329        </ul>
330    </div>
331    <div class="panels">
332        {}
333    </div>
334</div>
335"#,
336			timing.total_time,
337			timing.sql_time,
338			timing.sql_queries,
339			timing.cache_hits,
340			timing.cache_misses,
341			queries
342				.iter()
343				.map(|q| format!(
344					"<li>{} ({:?})</li>",
345					escape_html_content(&q.query),
346					q.duration
347				))
348				.collect::<Vec<_>>()
349				.join("\n"),
350			panels
351				.iter()
352				.map(|(id, panel)| format!(
353					"<div class='panel' id='{}'><h3>{}</h3></div>",
354					escape_html_content(id),
355					escape_html_content(&panel.title)
356				))
357				.collect::<Vec<_>>()
358				.join("\n")
359		)
360	}
361}
362
363impl Default for DebugToolbar {
364	fn default() -> Self {
365		Self::new()
366	}
367}
368
369#[cfg(test)]
370mod tests {
371	use super::*;
372
373	#[tokio::test]
374	async fn test_debug_toolbar() {
375		let toolbar = DebugToolbar::new();
376
377		toolbar
378			.record_sql_query("SELECT * FROM users".to_string(), Duration::from_millis(10))
379			.await;
380		toolbar.record_cache_hit().await;
381		toolbar.finalize().await;
382
383		let timing = toolbar.get_timing().await;
384		assert_eq!(timing.sql_queries, 1);
385		assert_eq!(timing.cache_hits, 1);
386
387		let queries = toolbar.get_sql_queries().await;
388		assert_eq!(queries.len(), 1);
389	}
390
391	#[tokio::test]
392	async fn test_debug_panel() {
393		let toolbar = DebugToolbar::new();
394
395		let panel = DebugPanel {
396			title: "Test Panel".to_string(),
397			content: vec![DebugEntry::KeyValue {
398				key: "key".to_string(),
399				value: "value".to_string(),
400			}],
401		};
402
403		toolbar.add_panel("test".to_string(), panel).await;
404
405		let panels = toolbar.get_panels().await;
406		assert_eq!(panels.len(), 1);
407		assert!(panels.contains_key("test"));
408	}
409
410	#[test]
411	fn test_escape_html_content() {
412		assert_eq!(escape_html_content("hello"), "hello");
413		assert_eq!(
414			escape_html_content("<script>alert('xss')</script>"),
415			"&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;"
416		);
417		assert_eq!(escape_html_content("a & b"), "a &amp; b");
418		assert_eq!(
419			escape_html_content(r#"key="value""#),
420			"key=&quot;value&quot;"
421		);
422	}
423
424	#[tokio::test]
425	async fn test_render_html_escapes_sql_queries() {
426		let toolbar = DebugToolbar::new();
427		toolbar
428			.record_sql_query(
429				"SELECT * FROM users WHERE name = '<script>alert(1)</script>'".to_string(),
430				Duration::from_millis(1),
431			)
432			.await;
433		toolbar.finalize().await;
434
435		let html = toolbar.render_html().await;
436		assert!(!html.contains("<script>"));
437		assert!(html.contains("&lt;script&gt;"));
438	}
439
440	#[tokio::test]
441	async fn test_render_html_escapes_panel_content() {
442		let toolbar = DebugToolbar::new();
443		let panel = DebugPanel {
444			title: "<img src=x onerror=alert(1)>".to_string(),
445			content: vec![],
446		};
447		toolbar.add_panel("<script>".to_string(), panel).await;
448		toolbar.finalize().await;
449
450		let html = toolbar.render_html().await;
451		assert!(!html.contains("<script>"));
452		assert!(!html.contains("<img src=x"));
453		assert!(html.contains("&lt;script&gt;"));
454		assert!(html.contains("&lt;img src=x onerror=alert(1)&gt;"));
455	}
456
457	#[tokio::test]
458	async fn test_disabled_toolbar() {
459		let mut toolbar = DebugToolbar::new();
460		toolbar.set_enabled(false);
461
462		toolbar
463			.record_sql_query("SELECT 1".to_string(), Duration::from_millis(1))
464			.await;
465
466		let timing = toolbar.get_timing().await;
467		assert_eq!(timing.sql_queries, 0);
468	}
469}