Skip to main content

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