Skip to main content

testx/
events.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use crate::adapters::{TestCase, TestRunResult, TestSuite};
5
6/// Events emitted during test execution, enabling decoupled output rendering.
7#[derive(Debug, Clone)]
8pub enum TestEvent {
9    /// Test run is starting.
10    RunStarted {
11        adapter: String,
12        framework: String,
13        project_dir: PathBuf,
14    },
15
16    /// A test suite has started executing.
17    SuiteStarted { name: String },
18
19    /// A single test has started.
20    TestStarted { suite: String, name: String },
21
22    /// A single test has completed.
23    TestFinished { suite: String, test: TestCase },
24
25    /// An entire suite has completed.
26    SuiteFinished { suite: TestSuite },
27
28    /// The entire test run has completed.
29    RunFinished { result: TestRunResult },
30
31    /// Raw output line from the test process.
32    RawOutput { stream: Stream, line: String },
33
34    /// Watch mode: files changed, triggering re-run.
35    WatchRerun { changed_files: Vec<PathBuf> },
36
37    /// Retry: a failed test is being retried.
38    RetryStarted {
39        test_name: String,
40        attempt: u32,
41        max_attempts: u32,
42    },
43
44    /// Retry: attempt completed.
45    RetryFinished {
46        test_name: String,
47        attempt: u32,
48        passed: bool,
49    },
50
51    /// Filter applied to test run.
52    FilterApplied {
53        pattern: String,
54        matched_count: usize,
55    },
56
57    /// Parallel: an adapter run started.
58    ParallelAdapterStarted { adapter: String },
59
60    /// Parallel: an adapter run finished.
61    ParallelAdapterFinished {
62        adapter: String,
63        result: TestRunResult,
64    },
65
66    /// A warning message (non-fatal).
67    Warning { message: String },
68
69    /// A progress tick (for long-running operations).
70    Progress {
71        message: String,
72        current: usize,
73        total: usize,
74    },
75}
76
77/// Output stream identifier.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum Stream {
80    Stdout,
81    Stderr,
82}
83
84/// Handler for test events. Implement this to create custom output formatters.
85pub trait EventHandler: Send {
86    /// Handle a single test event.
87    fn handle(&mut self, event: &TestEvent);
88
89    /// Called when a batch of events is complete (e.g., end of run).
90    fn flush(&mut self) {}
91}
92
93/// Event bus that distributes events to all registered handlers.
94pub struct EventBus {
95    handlers: Vec<Box<dyn EventHandler>>,
96}
97
98impl Default for EventBus {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl EventBus {
105    pub fn new() -> Self {
106        Self {
107            handlers: Vec::new(),
108        }
109    }
110
111    /// Register an event handler.
112    pub fn subscribe(&mut self, handler: Box<dyn EventHandler>) {
113        self.handlers.push(handler);
114    }
115
116    /// Emit an event to all registered handlers.
117    pub fn emit(&mut self, event: TestEvent) {
118        for handler in &mut self.handlers {
119            handler.handle(&event);
120        }
121    }
122
123    /// Flush all handlers (call at end of run).
124    pub fn flush(&mut self) {
125        for handler in &mut self.handlers {
126            handler.flush();
127        }
128    }
129
130    /// Returns the number of registered handlers.
131    pub fn handler_count(&self) -> usize {
132        self.handlers.len()
133    }
134}
135
136/// A handler that collects all events into a vec for testing.
137pub struct CollectingHandler {
138    pub events: Vec<TestEvent>,
139}
140
141impl CollectingHandler {
142    pub fn new() -> Self {
143        Self { events: Vec::new() }
144    }
145}
146
147impl Default for CollectingHandler {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153impl EventHandler for CollectingHandler {
154    fn handle(&mut self, event: &TestEvent) {
155        self.events.push(event.clone());
156    }
157}
158
159/// A handler that counts events by type for quick assertions.
160#[derive(Debug, Default)]
161pub struct CountingHandler {
162    pub run_started: usize,
163    pub suite_started: usize,
164    pub test_started: usize,
165    pub test_finished: usize,
166    pub suite_finished: usize,
167    pub run_finished: usize,
168    pub raw_output: usize,
169    pub warnings: usize,
170    pub total: usize,
171}
172
173impl EventHandler for CountingHandler {
174    fn handle(&mut self, event: &TestEvent) {
175        self.total += 1;
176        match event {
177            TestEvent::RunStarted { .. } => self.run_started += 1,
178            TestEvent::SuiteStarted { .. } => self.suite_started += 1,
179            TestEvent::TestStarted { .. } => self.test_started += 1,
180            TestEvent::TestFinished { .. } => self.test_finished += 1,
181            TestEvent::SuiteFinished { .. } => self.suite_finished += 1,
182            TestEvent::RunFinished { .. } => self.run_finished += 1,
183            TestEvent::RawOutput { .. } => self.raw_output += 1,
184            TestEvent::Warning { .. } => self.warnings += 1,
185            _ => {}
186        }
187    }
188}
189
190/// A handler that writes raw output lines to a buffer.
191pub struct RawOutputCollector {
192    pub stdout_lines: Vec<String>,
193    pub stderr_lines: Vec<String>,
194}
195
196impl RawOutputCollector {
197    pub fn new() -> Self {
198        Self {
199            stdout_lines: Vec::new(),
200            stderr_lines: Vec::new(),
201        }
202    }
203
204    pub fn stdout(&self) -> String {
205        self.stdout_lines.join("\n")
206    }
207
208    pub fn stderr(&self) -> String {
209        self.stderr_lines.join("\n")
210    }
211}
212
213impl Default for RawOutputCollector {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219impl EventHandler for RawOutputCollector {
220    fn handle(&mut self, event: &TestEvent) {
221        if let TestEvent::RawOutput { stream, line } = event {
222            match stream {
223                Stream::Stdout => self.stdout_lines.push(line.clone()),
224                Stream::Stderr => self.stderr_lines.push(line.clone()),
225            }
226        }
227    }
228}
229
230/// An event handler that logs events with timestamps for debugging.
231pub struct TimestampedLogger {
232    start: std::time::Instant,
233    entries: Vec<(Duration, String)>,
234}
235
236impl TimestampedLogger {
237    pub fn new() -> Self {
238        Self {
239            start: std::time::Instant::now(),
240            entries: Vec::new(),
241        }
242    }
243
244    pub fn entries(&self) -> &[(Duration, String)] {
245        &self.entries
246    }
247}
248
249impl Default for TimestampedLogger {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255impl EventHandler for TimestampedLogger {
256    fn handle(&mut self, event: &TestEvent) {
257        let elapsed = self.start.elapsed();
258        let description = match event {
259            TestEvent::RunStarted { adapter, .. } => format!("run started: {}", adapter),
260            TestEvent::SuiteStarted { name } => format!("suite started: {}", name),
261            TestEvent::TestStarted { suite, name } => {
262                format!("test started: {}::{}", suite, name)
263            }
264            TestEvent::TestFinished { suite, test } => {
265                format!(
266                    "test finished: {}::{} ({:?})",
267                    suite, test.name, test.status
268                )
269            }
270            TestEvent::SuiteFinished { suite } => format!("suite finished: {}", suite.name),
271            TestEvent::RunFinished { result } => {
272                format!(
273                    "run finished: {} tests, {} passed, {} failed",
274                    result.total_tests(),
275                    result.total_passed(),
276                    result.total_failed()
277                )
278            }
279            TestEvent::RawOutput { stream, .. } => format!("raw output ({:?})", stream),
280            TestEvent::WatchRerun { changed_files } => {
281                format!("watch rerun: {} files changed", changed_files.len())
282            }
283            TestEvent::RetryStarted {
284                test_name, attempt, ..
285            } => format!("retry: {} attempt {}", test_name, attempt),
286            TestEvent::RetryFinished {
287                test_name, passed, ..
288            } => {
289                format!(
290                    "retry finished: {} {}",
291                    test_name,
292                    if *passed { "passed" } else { "failed" }
293                )
294            }
295            TestEvent::FilterApplied {
296                pattern,
297                matched_count,
298            } => format!("filter: '{}' matched {} tests", pattern, matched_count),
299            TestEvent::ParallelAdapterStarted { adapter } => {
300                format!("parallel started: {}", adapter)
301            }
302            TestEvent::ParallelAdapterFinished { adapter, .. } => {
303                format!("parallel finished: {}", adapter)
304            }
305            TestEvent::Warning { message } => format!("warning: {}", message),
306            TestEvent::Progress {
307                message,
308                current,
309                total,
310            } => format!("progress: {} ({}/{})", message, current, total),
311        };
312        self.entries.push((elapsed, description));
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::adapters::TestStatus;
320
321    fn make_test_case(name: &str, status: TestStatus) -> TestCase {
322        TestCase {
323            name: name.into(),
324            status,
325            duration: Duration::from_millis(10),
326            error: None,
327        }
328    }
329
330    #[test]
331    fn event_bus_empty() {
332        let mut bus = EventBus::new();
333        assert_eq!(bus.handler_count(), 0);
334        // Should not panic with no handlers
335        bus.emit(TestEvent::RunStarted {
336            adapter: "rust".into(),
337            framework: "cargo test".into(),
338            project_dir: PathBuf::from("."),
339        });
340    }
341
342    #[test]
343    fn event_bus_subscribe_and_emit() {
344        let mut bus = EventBus::new();
345        bus.subscribe(Box::new(CollectingHandler::new()));
346        assert_eq!(bus.handler_count(), 1);
347
348        bus.emit(TestEvent::RunStarted {
349            adapter: "rust".into(),
350            framework: "cargo test".into(),
351            project_dir: PathBuf::from("."),
352        });
353
354        bus.emit(TestEvent::Warning {
355            message: "something".into(),
356        });
357    }
358
359    #[test]
360    fn counting_handler_counts_events() {
361        let mut handler = CountingHandler::default();
362
363        handler.handle(&TestEvent::RunStarted {
364            adapter: "go".into(),
365            framework: "go test".into(),
366            project_dir: PathBuf::from("."),
367        });
368        handler.handle(&TestEvent::SuiteStarted {
369            name: "main".into(),
370        });
371        handler.handle(&TestEvent::TestStarted {
372            suite: "main".into(),
373            name: "TestFoo".into(),
374        });
375        handler.handle(&TestEvent::TestFinished {
376            suite: "main".into(),
377            test: make_test_case("TestFoo", TestStatus::Passed),
378        });
379        handler.handle(&TestEvent::SuiteFinished {
380            suite: TestSuite {
381                name: "main".into(),
382                tests: vec![make_test_case("TestFoo", TestStatus::Passed)],
383            },
384        });
385        handler.handle(&TestEvent::RunFinished {
386            result: TestRunResult {
387                suites: vec![],
388                duration: Duration::from_millis(100),
389                raw_exit_code: 0,
390            },
391        });
392        handler.handle(&TestEvent::Warning {
393            message: "slow".into(),
394        });
395
396        assert_eq!(handler.run_started, 1);
397        assert_eq!(handler.suite_started, 1);
398        assert_eq!(handler.test_started, 1);
399        assert_eq!(handler.test_finished, 1);
400        assert_eq!(handler.suite_finished, 1);
401        assert_eq!(handler.run_finished, 1);
402        assert_eq!(handler.warnings, 1);
403        assert_eq!(handler.total, 7);
404    }
405
406    #[test]
407    fn raw_output_collector() {
408        let mut collector = RawOutputCollector::new();
409
410        collector.handle(&TestEvent::RawOutput {
411            stream: Stream::Stdout,
412            line: "line 1".into(),
413        });
414        collector.handle(&TestEvent::RawOutput {
415            stream: Stream::Stderr,
416            line: "err 1".into(),
417        });
418        collector.handle(&TestEvent::RawOutput {
419            stream: Stream::Stdout,
420            line: "line 2".into(),
421        });
422        // Non-raw events ignored
423        collector.handle(&TestEvent::Warning {
424            message: "ignored".into(),
425        });
426
427        assert_eq!(collector.stdout_lines.len(), 2);
428        assert_eq!(collector.stderr_lines.len(), 1);
429        assert_eq!(collector.stdout(), "line 1\nline 2");
430        assert_eq!(collector.stderr(), "err 1");
431    }
432
433    #[test]
434    fn timestamped_logger() {
435        let mut logger = TimestampedLogger::new();
436
437        logger.handle(&TestEvent::RunStarted {
438            adapter: "python".into(),
439            framework: "pytest".into(),
440            project_dir: PathBuf::from("."),
441        });
442        logger.handle(&TestEvent::Warning {
443            message: "slow test".into(),
444        });
445
446        assert_eq!(logger.entries().len(), 2);
447        assert!(logger.entries()[0].1.contains("run started: python"));
448        assert!(logger.entries()[1].1.contains("warning: slow test"));
449    }
450
451    #[test]
452    fn collecting_handler_default() {
453        let handler = CollectingHandler::default();
454        assert!(handler.events.is_empty());
455    }
456
457    #[test]
458    fn raw_output_collector_default() {
459        let collector = RawOutputCollector::default();
460        assert!(collector.stdout_lines.is_empty());
461        assert!(collector.stderr_lines.is_empty());
462    }
463
464    #[test]
465    fn stream_equality() {
466        assert_eq!(Stream::Stdout, Stream::Stdout);
467        assert_eq!(Stream::Stderr, Stream::Stderr);
468        assert_ne!(Stream::Stdout, Stream::Stderr);
469    }
470
471    #[test]
472    fn event_bus_flush() {
473        let mut bus = EventBus::new();
474        bus.subscribe(Box::new(CountingHandler::default()));
475        bus.flush(); // Should not panic
476    }
477
478    #[test]
479    fn event_bus_multiple_handlers() {
480        let mut bus = EventBus::new();
481        bus.subscribe(Box::new(CountingHandler::default()));
482        bus.subscribe(Box::new(CollectingHandler::new()));
483        bus.subscribe(Box::new(RawOutputCollector::new()));
484        assert_eq!(bus.handler_count(), 3);
485
486        bus.emit(TestEvent::RawOutput {
487            stream: Stream::Stdout,
488            line: "hello".into(),
489        });
490    }
491
492    #[test]
493    fn timestamped_logger_all_event_types() {
494        let mut logger = TimestampedLogger::new();
495
496        logger.handle(&TestEvent::FilterApplied {
497            pattern: "test_*".into(),
498            matched_count: 5,
499        });
500        logger.handle(&TestEvent::ParallelAdapterStarted {
501            adapter: "rust".into(),
502        });
503        logger.handle(&TestEvent::ParallelAdapterFinished {
504            adapter: "rust".into(),
505            result: TestRunResult {
506                suites: vec![],
507                duration: Duration::ZERO,
508                raw_exit_code: 0,
509            },
510        });
511        logger.handle(&TestEvent::RetryStarted {
512            test_name: "test_foo".into(),
513            attempt: 2,
514            max_attempts: 3,
515        });
516        logger.handle(&TestEvent::RetryFinished {
517            test_name: "test_foo".into(),
518            attempt: 2,
519            passed: true,
520        });
521        logger.handle(&TestEvent::WatchRerun {
522            changed_files: vec![PathBuf::from("src/lib.rs")],
523        });
524        logger.handle(&TestEvent::Progress {
525            message: "running".into(),
526            current: 1,
527            total: 10,
528        });
529
530        assert_eq!(logger.entries().len(), 7);
531        assert!(logger.entries()[0].1.contains("filter"));
532        assert!(logger.entries()[1].1.contains("parallel started"));
533        assert!(logger.entries()[2].1.contains("parallel finished"));
534        assert!(logger.entries()[3].1.contains("retry: test_foo"));
535        assert!(logger.entries()[4].1.contains("retry finished"));
536        assert!(logger.entries()[5].1.contains("watch rerun"));
537        assert!(logger.entries()[6].1.contains("progress"));
538    }
539}