Skip to main content

reinhardt_testkit/
debug.rs

1use reinhardt_core::security::escape_html;
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!("<li>{} ({:?})</li>", escape_html(&q.query), q.duration))
366				.collect::<Vec<_>>()
367				.join("\n"),
368			panels
369				.iter()
370				.map(|(id, panel)| format!(
371					"<div class='panel' id='{}'><h3>{}</h3></div>",
372					escape_html(id),
373					escape_html(&panel.title)
374				))
375				.collect::<Vec<_>>()
376				.join("\n")
377		)
378	}
379}
380
381impl Default for DebugToolbar {
382	fn default() -> Self {
383		Self::new()
384	}
385}
386
387#[cfg(test)]
388mod tests {
389	use super::*;
390
391	#[tokio::test]
392	async fn test_debug_toolbar() {
393		let toolbar = DebugToolbar::new();
394
395		toolbar
396			.record_sql_query("SELECT * FROM users".to_string(), Duration::from_millis(10))
397			.await;
398		toolbar.record_cache_hit().await;
399		toolbar.finalize().await;
400
401		let timing = toolbar.get_timing().await;
402		assert_eq!(timing.sql_queries, 1);
403		assert_eq!(timing.cache_hits, 1);
404
405		let queries = toolbar.get_sql_queries().await;
406		assert_eq!(queries.len(), 1);
407	}
408
409	#[tokio::test]
410	async fn test_debug_panel() {
411		let toolbar = DebugToolbar::new();
412
413		let panel = DebugPanel {
414			title: "Test Panel".to_string(),
415			content: vec![DebugEntry::KeyValue {
416				key: "key".to_string(),
417				value: "value".to_string(),
418			}],
419		};
420
421		toolbar.add_panel("test".to_string(), panel).await;
422
423		let panels = toolbar.get_panels().await;
424		assert_eq!(panels.len(), 1);
425		assert!(panels.contains_key("test"));
426	}
427
428	#[test]
429	fn test_escape_html() {
430		assert_eq!(escape_html("hello"), "hello");
431		assert_eq!(
432			escape_html("<script>alert('xss')</script>"),
433			"&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;"
434		);
435		assert_eq!(escape_html("a & b"), "a &amp; b");
436		assert_eq!(escape_html(r#"key="value""#), "key=&quot;value&quot;");
437	}
438
439	#[tokio::test]
440	async fn test_render_html_escapes_sql_queries() {
441		let toolbar = DebugToolbar::new();
442		toolbar
443			.record_sql_query(
444				"SELECT * FROM users WHERE name = '<script>alert(1)</script>'".to_string(),
445				Duration::from_millis(1),
446			)
447			.await;
448		toolbar.finalize().await;
449
450		let html = toolbar.render_html().await;
451		assert!(!html.contains("<script>"));
452		assert!(html.contains("&lt;script&gt;"));
453	}
454
455	#[tokio::test]
456	async fn test_render_html_escapes_panel_content() {
457		let toolbar = DebugToolbar::new();
458		let panel = DebugPanel {
459			title: "<img src=x onerror=alert(1)>".to_string(),
460			content: vec![],
461		};
462		toolbar.add_panel("<script>".to_string(), panel).await;
463		toolbar.finalize().await;
464
465		let html = toolbar.render_html().await;
466		assert!(!html.contains("<script>"));
467		assert!(!html.contains("<img src=x"));
468		assert!(html.contains("&lt;script&gt;"));
469		assert!(html.contains("&lt;img src=x onerror=alert(1)&gt;"));
470	}
471
472	#[tokio::test]
473	async fn test_disabled_toolbar() {
474		let mut toolbar = DebugToolbar::new();
475		toolbar.set_enabled(false);
476
477		toolbar
478			.record_sql_query("SELECT 1".to_string(), Duration::from_millis(1))
479			.await;
480
481		let timing = toolbar.get_timing().await;
482		assert_eq!(timing.sql_queries, 0);
483	}
484}