1use std::path::PathBuf;
2use std::time::Duration;
3
4use crate::adapters::{TestCase, TestRunResult, TestSuite};
5
6#[derive(Debug, Clone)]
8pub enum TestEvent {
9 RunStarted {
11 adapter: String,
12 framework: String,
13 project_dir: PathBuf,
14 },
15
16 SuiteStarted { name: String },
18
19 TestStarted { suite: String, name: String },
21
22 TestFinished { suite: String, test: TestCase },
24
25 SuiteFinished { suite: TestSuite },
27
28 RunFinished { result: TestRunResult },
30
31 RawOutput { stream: Stream, line: String },
33
34 WatchRerun { changed_files: Vec<PathBuf> },
36
37 RetryStarted {
39 test_name: String,
40 attempt: u32,
41 max_attempts: u32,
42 },
43
44 RetryFinished {
46 test_name: String,
47 attempt: u32,
48 passed: bool,
49 },
50
51 FilterApplied {
53 pattern: String,
54 matched_count: usize,
55 },
56
57 ParallelAdapterStarted { adapter: String },
59
60 ParallelAdapterFinished {
62 adapter: String,
63 result: TestRunResult,
64 },
65
66 Warning { message: String },
68
69 Progress {
71 message: String,
72 current: usize,
73 total: usize,
74 },
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum Stream {
80 Stdout,
81 Stderr,
82}
83
84pub trait EventHandler: Send {
86 fn handle(&mut self, event: &TestEvent);
88
89 fn flush(&mut self) {}
91}
92
93pub 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 pub fn subscribe(&mut self, handler: Box<dyn EventHandler>) {
113 self.handlers.push(handler);
114 }
115
116 pub fn emit(&mut self, event: TestEvent) {
118 for handler in &mut self.handlers {
119 handler.handle(&event);
120 }
121 }
122
123 pub fn flush(&mut self) {
125 for handler in &mut self.handlers {
126 handler.flush();
127 }
128 }
129
130 pub fn handler_count(&self) -> usize {
132 self.handlers.len()
133 }
134}
135
136pub 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#[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
190pub 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
230pub 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 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 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(); }
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}