1#[derive(Clone, Debug, PartialEq)]
6pub enum ReplayEvent {
7 Assert { term: String, rule_set: String },
9 Retract { term: String },
11 Query { goal: String, max_depth: usize },
13 QueryResult {
15 goal: String,
16 success: bool,
17 bindings_count: usize,
18 },
19 RuleLoaded { rule_id: String, head: String },
21 SessionStart {
23 session_id: String,
24 timestamp_secs: u64,
25 },
26 SessionEnd {
28 session_id: String,
29 timestamp_secs: u64,
30 },
31}
32
33#[derive(Clone, Debug)]
35pub struct ReplaySession {
36 pub session_id: String,
38 pub events: Vec<ReplayEvent>,
40 pub started_at_secs: u64,
42}
43
44impl ReplaySession {
45 fn new(session_id: String) -> Self {
47 Self {
48 session_id,
49 events: Vec::new(),
50 started_at_secs: 0,
51 }
52 }
53
54 pub fn duration_secs(&self) -> u64 {
57 let start = self.events.iter().find_map(|e| {
58 if let ReplayEvent::SessionStart { timestamp_secs, .. } = e {
59 Some(*timestamp_secs)
60 } else {
61 None
62 }
63 });
64
65 let end = self.events.iter().find_map(|e| {
66 if let ReplayEvent::SessionEnd { timestamp_secs, .. } = e {
67 Some(*timestamp_secs)
68 } else {
69 None
70 }
71 });
72
73 match (start, end) {
74 (Some(s), Some(e)) => e.saturating_sub(s),
75 _ => 0,
76 }
77 }
78
79 pub fn event_count(&self) -> usize {
81 self.events.len()
82 }
83
84 pub fn query_count(&self) -> usize {
86 self.events
87 .iter()
88 .filter(|e| matches!(e, ReplayEvent::Query { .. }))
89 .count()
90 }
91
92 pub fn success_rate(&self) -> f64 {
95 let results: Vec<bool> = self
96 .events
97 .iter()
98 .filter_map(|e| {
99 if let ReplayEvent::QueryResult { success, .. } = e {
100 Some(*success)
101 } else {
102 None
103 }
104 })
105 .collect();
106
107 if results.is_empty() {
108 return 0.0;
109 }
110
111 let successes = results.iter().filter(|&&s| s).count();
112 successes as f64 / results.len() as f64
113 }
114}
115
116#[derive(Clone, Debug, PartialEq)]
118pub enum ReplayFilter {
119 All,
121 QueriesOnly,
123 AssertionsOnly,
125 SessionId(String),
127 TimeRange { from_secs: u64, to_secs: u64 },
129}
130
131#[derive(Clone, Debug, PartialEq)]
133pub struct ReplayStats {
134 pub total_sessions: usize,
136 pub total_events: usize,
138 pub total_queries: usize,
140 pub total_assertions: usize,
142 pub avg_session_duration_secs: f64,
144}
145
146pub struct SessionReplayEngine {
148 pub sessions: Vec<ReplaySession>,
150 pub max_sessions: usize,
152}
153
154impl SessionReplayEngine {
155 pub fn new(max_sessions: usize) -> Self {
157 Self {
158 sessions: Vec::new(),
159 max_sessions,
160 }
161 }
162
163 pub fn record_event(&mut self, session_id: &str, event: ReplayEvent) {
172 let pos = self
174 .sessions
175 .iter()
176 .position(|s| s.session_id == session_id);
177
178 if let Some(idx) = pos {
179 if let ReplayEvent::SessionStart { timestamp_secs, .. } = &event {
181 if self.sessions[idx].started_at_secs == 0 {
182 self.sessions[idx].started_at_secs = *timestamp_secs;
183 }
184 }
185 self.sessions[idx].events.push(event);
186 } else {
187 if self.sessions.len() >= self.max_sessions {
189 self.sessions.remove(0);
190 }
191
192 let mut session = ReplaySession::new(session_id.to_string());
193
194 if let ReplayEvent::SessionStart { timestamp_secs, .. } = &event {
196 session.started_at_secs = *timestamp_secs;
197 }
198
199 session.events.push(event);
200 self.sessions.push(session);
201 }
202 }
203
204 pub fn get_session(&self, session_id: &str) -> Option<&ReplaySession> {
206 self.sessions.iter().find(|s| s.session_id == session_id)
207 }
208
209 pub fn filter_sessions(&self, filter: &ReplayFilter) -> Vec<&ReplaySession> {
211 self.sessions
212 .iter()
213 .filter(|s| match filter {
214 ReplayFilter::All => true,
215 ReplayFilter::QueriesOnly => s.events.iter().any(|e| {
216 matches!(
217 e,
218 ReplayEvent::Query { .. } | ReplayEvent::QueryResult { .. }
219 )
220 }),
221 ReplayFilter::AssertionsOnly => s
222 .events
223 .iter()
224 .any(|e| matches!(e, ReplayEvent::Assert { .. })),
225 ReplayFilter::SessionId(id) => &s.session_id == id,
226 ReplayFilter::TimeRange { from_secs, to_secs } => {
227 s.started_at_secs >= *from_secs && s.started_at_secs <= *to_secs
228 }
229 })
230 .collect()
231 }
232
233 pub fn replay_queries(&self, session_id: &str) -> Vec<&ReplayEvent> {
235 match self.get_session(session_id) {
236 None => Vec::new(),
237 Some(session) => session
238 .events
239 .iter()
240 .filter(|e| {
241 matches!(
242 e,
243 ReplayEvent::Query { .. } | ReplayEvent::QueryResult { .. }
244 )
245 })
246 .collect(),
247 }
248 }
249
250 pub fn stats(&self) -> ReplayStats {
252 let total_sessions = self.sessions.len();
253 let total_events: usize = self.sessions.iter().map(|s| s.events.len()).sum();
254
255 let total_queries: usize = self.sessions.iter().map(|s| s.query_count()).sum();
256
257 let total_assertions: usize = self
258 .sessions
259 .iter()
260 .map(|s| {
261 s.events
262 .iter()
263 .filter(|e| matches!(e, ReplayEvent::Assert { .. }))
264 .count()
265 })
266 .sum();
267
268 let avg_session_duration_secs = if total_sessions == 0 {
269 0.0
270 } else {
271 let total_dur: u64 = self.sessions.iter().map(|s| s.duration_secs()).sum();
272 total_dur as f64 / total_sessions as f64
273 };
274
275 ReplayStats {
276 total_sessions,
277 total_events,
278 total_queries,
279 total_assertions,
280 avg_session_duration_secs,
281 }
282 }
283
284 pub fn export_session(&self, session_id: &str) -> Option<String> {
288 let session = self.get_session(session_id)?;
289 let events = session.event_count();
290 let queries = session.query_count();
291 let success_rate = session.success_rate();
292 Some(format!(
293 r#"{{"session_id":"{id}","events":{events},"queries":{queries},"success_rate":{success_rate}}}"#,
294 id = session.session_id,
295 events = events,
296 queries = queries,
297 success_rate = success_rate,
298 ))
299 }
300
301 pub fn clear(&mut self) {
303 self.sessions.clear();
304 }
305}
306
307#[cfg(test)]
312mod tests {
313 use super::*;
314
315 fn make_engine() -> SessionReplayEngine {
316 SessionReplayEngine::new(1000)
317 }
318
319 fn session_start(session_id: &str, ts: u64) -> ReplayEvent {
320 ReplayEvent::SessionStart {
321 session_id: session_id.to_string(),
322 timestamp_secs: ts,
323 }
324 }
325
326 fn session_end(session_id: &str, ts: u64) -> ReplayEvent {
327 ReplayEvent::SessionEnd {
328 session_id: session_id.to_string(),
329 timestamp_secs: ts,
330 }
331 }
332
333 fn query(goal: &str) -> ReplayEvent {
334 ReplayEvent::Query {
335 goal: goal.to_string(),
336 max_depth: 10,
337 }
338 }
339
340 fn query_result(goal: &str, success: bool, bindings_count: usize) -> ReplayEvent {
341 ReplayEvent::QueryResult {
342 goal: goal.to_string(),
343 success,
344 bindings_count,
345 }
346 }
347
348 fn assert_event(term: &str) -> ReplayEvent {
349 ReplayEvent::Assert {
350 term: term.to_string(),
351 rule_set: "default".to_string(),
352 }
353 }
354
355 #[test]
357 fn test_new_empty() {
358 let engine = make_engine();
359 assert_eq!(engine.sessions.len(), 0);
360 assert_eq!(engine.max_sessions, 1000);
361 }
362
363 #[test]
365 fn test_record_event_creates_session() {
366 let mut engine = make_engine();
367 engine.record_event("s1", session_start("s1", 100));
368 assert_eq!(engine.sessions.len(), 1);
369 assert_eq!(engine.sessions[0].session_id, "s1");
370 }
371
372 #[test]
374 fn test_record_event_appends() {
375 let mut engine = make_engine();
376 engine.record_event("s1", session_start("s1", 100));
377 engine.record_event("s1", query("parent(X, Y)"));
378 engine.record_event("s1", query_result("parent(X, Y)", true, 2));
379 assert_eq!(engine.sessions.len(), 1);
380 assert_eq!(engine.sessions[0].events.len(), 3);
381 }
382
383 #[test]
385 fn test_record_event_max_sessions_drops_oldest() {
386 let mut engine = SessionReplayEngine::new(2);
387 engine.record_event("s1", session_start("s1", 10));
388 engine.record_event("s2", session_start("s2", 20));
389 engine.record_event("s3", session_start("s3", 30));
391 assert_eq!(engine.sessions.len(), 2);
392 assert!(engine.get_session("s1").is_none());
393 assert!(engine.get_session("s2").is_some());
394 assert!(engine.get_session("s3").is_some());
395 }
396
397 #[test]
399 fn test_get_session_found() {
400 let mut engine = make_engine();
401 engine.record_event("s1", session_start("s1", 100));
402 let s = engine.get_session("s1");
403 assert!(s.is_some());
404 assert_eq!(s.expect("test: should succeed").session_id, "s1");
405 }
406
407 #[test]
409 fn test_get_session_not_found() {
410 let engine = make_engine();
411 assert!(engine.get_session("missing").is_none());
412 }
413
414 #[test]
416 fn test_filter_all() {
417 let mut engine = make_engine();
418 engine.record_event("s1", session_start("s1", 1));
419 engine.record_event("s2", session_start("s2", 2));
420 let results = engine.filter_sessions(&ReplayFilter::All);
421 assert_eq!(results.len(), 2);
422 }
423
424 #[test]
426 fn test_filter_queries_only() {
427 let mut engine = make_engine();
428 engine.record_event("s1", session_start("s1", 1));
429 engine.record_event("s1", query("foo(X)"));
430 engine.record_event("s2", session_start("s2", 2));
431 engine.record_event("s2", assert_event("bar(a)"));
432 let results = engine.filter_sessions(&ReplayFilter::QueriesOnly);
433 assert_eq!(results.len(), 1);
434 assert_eq!(results[0].session_id, "s1");
435 }
436
437 #[test]
439 fn test_filter_assertions_only() {
440 let mut engine = make_engine();
441 engine.record_event("s1", session_start("s1", 1));
442 engine.record_event("s1", assert_event("parent(a, b)"));
443 engine.record_event("s2", session_start("s2", 2));
444 engine.record_event("s2", query("foo(X)"));
445 let results = engine.filter_sessions(&ReplayFilter::AssertionsOnly);
446 assert_eq!(results.len(), 1);
447 assert_eq!(results[0].session_id, "s1");
448 }
449
450 #[test]
452 fn test_filter_session_id() {
453 let mut engine = make_engine();
454 engine.record_event("s1", session_start("s1", 1));
455 engine.record_event("s2", session_start("s2", 2));
456 let results = engine.filter_sessions(&ReplayFilter::SessionId("s2".to_string()));
457 assert_eq!(results.len(), 1);
458 assert_eq!(results[0].session_id, "s2");
459 }
460
461 #[test]
463 fn test_filter_time_range() {
464 let mut engine = make_engine();
465 engine.record_event("s1", session_start("s1", 100));
466 engine.record_event("s2", session_start("s2", 200));
467 engine.record_event("s3", session_start("s3", 300));
468 let results = engine.filter_sessions(&ReplayFilter::TimeRange {
469 from_secs: 150,
470 to_secs: 250,
471 });
472 assert_eq!(results.len(), 1);
473 assert_eq!(results[0].session_id, "s2");
474 }
475
476 #[test]
478 fn test_replay_queries_filters_correctly() {
479 let mut engine = make_engine();
480 engine.record_event("s1", session_start("s1", 1));
481 engine.record_event("s1", assert_event("foo(a)"));
482 engine.record_event("s1", query("foo(X)"));
483 engine.record_event("s1", query_result("foo(X)", true, 1));
484 engine.record_event("s1", session_end("s1", 100));
485
486 let events = engine.replay_queries("s1");
487 assert_eq!(events.len(), 2);
488 assert!(matches!(events[0], ReplayEvent::Query { .. }));
489 assert!(matches!(events[1], ReplayEvent::QueryResult { .. }));
490 }
491
492 #[test]
494 fn test_duration_secs() {
495 let mut engine = make_engine();
496 engine.record_event("s1", session_start("s1", 1000));
497 engine.record_event("s1", session_end("s1", 1045));
498 let session = engine.get_session("s1").expect("test: should succeed");
499 assert_eq!(session.duration_secs(), 45);
500 }
501
502 #[test]
504 fn test_success_rate_half() {
505 let mut engine = make_engine();
506 engine.record_event("s1", session_start("s1", 1));
507 engine.record_event("s1", query_result("a(X)", true, 1));
508 engine.record_event("s1", query_result("b(X)", false, 0));
509 let session = engine.get_session("s1").expect("test: should succeed");
510 let rate = session.success_rate();
511 assert!((rate - 0.5).abs() < f64::EPSILON);
512 }
513
514 #[test]
516 fn test_query_count() {
517 let mut engine = make_engine();
518 engine.record_event("s1", session_start("s1", 1));
519 engine.record_event("s1", query("a(X)"));
520 engine.record_event("s1", query("b(X)"));
521 engine.record_event("s1", query_result("a(X)", true, 1));
522 let session = engine.get_session("s1").expect("test: should succeed");
523 assert_eq!(session.query_count(), 2);
524 }
525
526 #[test]
528 fn test_stats_totals() {
529 let mut engine = make_engine();
530 engine.record_event("s1", session_start("s1", 0));
532 engine.record_event("s1", assert_event("foo(a)"));
533 engine.record_event("s1", query("foo(X)"));
534 engine.record_event("s1", session_end("s1", 50));
535
536 engine.record_event("s2", session_start("s2", 200));
538 engine.record_event("s2", assert_event("bar(b)"));
539 engine.record_event("s2", assert_event("baz(c)"));
540 engine.record_event("s2", query("bar(X)"));
541 engine.record_event("s2", query("baz(X)"));
542 engine.record_event("s2", session_end("s2", 300));
543
544 let stats = engine.stats();
545 assert_eq!(stats.total_sessions, 2);
546 assert_eq!(stats.total_events, 4 + 6); assert_eq!(stats.total_queries, 3);
548 assert_eq!(stats.total_assertions, 3);
549 assert!((stats.avg_session_duration_secs - 75.0).abs() < f64::EPSILON);
551 }
552
553 #[test]
555 fn test_export_session() {
556 let mut engine = make_engine();
557 engine.record_event("s1", session_start("s1", 1));
558 engine.record_event("s1", query("foo(X)"));
559 engine.record_event("s1", query_result("foo(X)", true, 2));
560
561 let exported = engine.export_session("s1");
562 assert!(exported.is_some());
563 let s = exported.expect("test: should succeed");
564 assert!(s.contains("\"session_id\":\"s1\""));
565 assert!(s.contains("\"events\":3"));
566 assert!(s.contains("\"queries\":1"));
567
568 assert!(engine.export_session("nonexistent").is_none());
569 }
570
571 #[test]
573 fn test_clear() {
574 let mut engine = make_engine();
575 engine.record_event("s1", session_start("s1", 1));
576 engine.record_event("s2", session_start("s2", 2));
577 assert_eq!(engine.sessions.len(), 2);
578 engine.clear();
579 assert_eq!(engine.sessions.len(), 0);
580 assert!(engine.get_session("s1").is_none());
581 }
582}