1pub mod reporters;
7pub mod script_adapter;
8
9use crate::adapters::TestRunResult;
10use crate::error;
11use crate::events::TestEvent;
12
13pub trait Plugin: Send {
18 fn name(&self) -> &str;
20
21 fn version(&self) -> &str;
23
24 fn on_event(&mut self, event: &TestEvent) -> error::Result<()>;
26
27 fn on_result(&mut self, result: &TestRunResult) -> error::Result<()>;
29
30 fn shutdown(&mut self) -> error::Result<()> {
32 Ok(())
33 }
34}
35
36pub struct PluginManager {
38 plugins: Vec<Box<dyn Plugin>>,
39 errors: Vec<PluginError>,
40}
41
42#[derive(Debug, Clone)]
44pub struct PluginError {
45 pub plugin_name: String,
47 pub message: String,
49 pub fatal: bool,
51}
52
53impl PluginError {
54 fn new(plugin_name: &str, message: String, fatal: bool) -> Self {
55 Self {
56 plugin_name: plugin_name.to_string(),
57 message,
58 fatal,
59 }
60 }
61}
62
63impl std::fmt::Display for PluginError {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 write!(
66 f,
67 "[plugin:{}] {}{}",
68 self.plugin_name,
69 self.message,
70 if self.fatal { " (fatal)" } else { "" }
71 )
72 }
73}
74
75impl PluginManager {
76 pub fn new() -> Self {
78 Self {
79 plugins: Vec::new(),
80 errors: Vec::new(),
81 }
82 }
83
84 pub fn register(&mut self, plugin: Box<dyn Plugin>) {
86 self.plugins.push(plugin);
87 }
88
89 pub fn plugin_count(&self) -> usize {
91 self.plugins.len()
92 }
93
94 pub fn plugin_names(&self) -> Vec<&str> {
96 self.plugins.iter().map(|p| p.name()).collect()
97 }
98
99 pub fn has_fatal_error(&self) -> bool {
101 self.errors.iter().any(|e| e.fatal)
102 }
103
104 pub fn errors(&self) -> &[PluginError] {
106 &self.errors
107 }
108
109 pub fn clear_errors(&mut self) {
111 self.errors.clear();
112 }
113
114 pub fn dispatch_event(&mut self, event: &TestEvent) {
118 for plugin in &mut self.plugins {
119 if let Err(e) = plugin.on_event(event) {
120 self.errors.push(PluginError::new(
121 plugin.name(),
122 format!("on_event error: {e}"),
123 false,
124 ));
125 }
126 }
127 }
128
129 pub fn dispatch_result(&mut self, result: &TestRunResult) {
131 for plugin in &mut self.plugins {
132 if let Err(e) = plugin.on_result(result) {
133 self.errors.push(PluginError::new(
134 plugin.name(),
135 format!("on_result error: {e}"),
136 false,
137 ));
138 }
139 }
140 }
141
142 pub fn shutdown_all(&mut self) {
144 for plugin in &mut self.plugins {
145 if let Err(e) = plugin.shutdown() {
146 self.errors.push(PluginError::new(
147 plugin.name(),
148 format!("shutdown error: {e}"),
149 false,
150 ));
151 }
152 }
153 }
154
155 pub fn remove(&mut self, name: &str) -> bool {
157 let len_before = self.plugins.len();
158 self.plugins.retain(|p| p.name() != name);
159 self.plugins.len() < len_before
160 }
161}
162
163impl Default for PluginManager {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169#[derive(Debug, Clone)]
171pub struct PluginInfo {
172 pub name: String,
173 pub version: String,
174 pub description: String,
175}
176
177impl PluginInfo {
178 pub fn new(name: &str, version: &str, description: &str) -> Self {
179 Self {
180 name: name.to_string(),
181 version: version.to_string(),
182 description: description.to_string(),
183 }
184 }
185}
186
187pub struct PluginRegistry {
189 available: Vec<PluginInfo>,
190}
191
192impl PluginRegistry {
193 pub fn new() -> Self {
194 Self {
195 available: Vec::new(),
196 }
197 }
198
199 pub fn register_available(&mut self, info: PluginInfo) {
201 self.available.push(info);
202 }
203
204 pub fn list_available(&self) -> &[PluginInfo] {
206 &self.available
207 }
208
209 pub fn builtin() -> Self {
211 let mut registry = Self::new();
212 registry.register_available(PluginInfo::new(
213 "markdown",
214 "1.0.0",
215 "Generates a Markdown test report",
216 ));
217 registry.register_available(PluginInfo::new(
218 "github",
219 "1.0.0",
220 "Emits GitHub Actions annotations",
221 ));
222 registry.register_available(PluginInfo::new(
223 "html",
224 "1.0.0",
225 "Generates a self-contained HTML test report",
226 ));
227 registry.register_available(PluginInfo::new(
228 "notify",
229 "1.0.0",
230 "Sends desktop notifications on test completion",
231 ));
232 registry
233 }
234
235 pub fn find(&self, name: &str) -> Option<&PluginInfo> {
237 self.available.iter().find(|p| p.name == name)
238 }
239}
240
241impl Default for PluginRegistry {
242 fn default() -> Self {
243 Self::new()
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use crate::adapters::{TestCase, TestRunResult, TestStatus, TestSuite};
251 use std::time::Duration;
252
253 struct MockPlugin {
255 name: String,
256 events_received: Vec<String>,
257 result_received: bool,
258 shutdown_called: bool,
259 should_error: bool,
260 }
261
262 impl MockPlugin {
263 fn new(name: &str) -> Self {
264 Self {
265 name: name.to_string(),
266 events_received: Vec::new(),
267 result_received: false,
268 shutdown_called: false,
269 should_error: false,
270 }
271 }
272
273 fn failing(name: &str) -> Self {
274 Self {
275 name: name.to_string(),
276 events_received: Vec::new(),
277 result_received: false,
278 shutdown_called: false,
279 should_error: true,
280 }
281 }
282 }
283
284 impl Plugin for MockPlugin {
285 fn name(&self) -> &str {
286 &self.name
287 }
288
289 fn version(&self) -> &str {
290 "0.1.0"
291 }
292
293 fn on_event(&mut self, event: &TestEvent) -> error::Result<()> {
294 if self.should_error {
295 return Err(error::TestxError::PluginError {
296 message: "mock error".into(),
297 });
298 }
299 self.events_received.push(format!("{event:?}"));
300 Ok(())
301 }
302
303 fn on_result(&mut self, _result: &TestRunResult) -> error::Result<()> {
304 if self.should_error {
305 return Err(error::TestxError::PluginError {
306 message: "mock result error".into(),
307 });
308 }
309 self.result_received = true;
310 Ok(())
311 }
312
313 fn shutdown(&mut self) -> error::Result<()> {
314 self.shutdown_called = true;
315 Ok(())
316 }
317 }
318
319 fn make_result() -> TestRunResult {
320 TestRunResult {
321 suites: vec![TestSuite {
322 name: "test".into(),
323 tests: vec![TestCase {
324 name: "test_a".into(),
325 status: TestStatus::Passed,
326 duration: Duration::from_millis(10),
327 error: None,
328 }],
329 }],
330 duration: Duration::from_millis(100),
331 raw_exit_code: 0,
332 }
333 }
334
335 #[test]
336 fn manager_new_is_empty() {
337 let mgr = PluginManager::new();
338 assert_eq!(mgr.plugin_count(), 0);
339 assert!(mgr.plugin_names().is_empty());
340 }
341
342 #[test]
343 fn manager_register() {
344 let mut mgr = PluginManager::new();
345 mgr.register(Box::new(MockPlugin::new("test-plugin")));
346 assert_eq!(mgr.plugin_count(), 1);
347 assert_eq!(mgr.plugin_names(), vec!["test-plugin"]);
348 }
349
350 #[test]
351 fn manager_dispatch_event() {
352 let mut mgr = PluginManager::new();
353 mgr.register(Box::new(MockPlugin::new("p1")));
354 mgr.register(Box::new(MockPlugin::new("p2")));
355
356 mgr.dispatch_event(&TestEvent::Warning {
357 message: "test warning".into(),
358 });
359
360 assert!(mgr.errors().is_empty());
361 }
362
363 #[test]
364 fn manager_dispatch_result() {
365 let mut mgr = PluginManager::new();
366 mgr.register(Box::new(MockPlugin::new("p1")));
367
368 mgr.dispatch_result(&make_result());
369 assert!(mgr.errors().is_empty());
370 }
371
372 #[test]
373 fn manager_collects_errors() {
374 let mut mgr = PluginManager::new();
375 mgr.register(Box::new(MockPlugin::failing("bad-plugin")));
376 mgr.register(Box::new(MockPlugin::new("good-plugin")));
377
378 mgr.dispatch_event(&TestEvent::Warning {
379 message: "test".into(),
380 });
381
382 assert_eq!(mgr.errors().len(), 1);
383 assert_eq!(mgr.errors()[0].plugin_name, "bad-plugin");
384 }
385
386 #[test]
387 fn manager_shutdown() {
388 let mut mgr = PluginManager::new();
389 mgr.register(Box::new(MockPlugin::new("p1")));
390 mgr.shutdown_all();
391 assert!(mgr.errors().is_empty());
393 }
394
395 #[test]
396 fn manager_remove() {
397 let mut mgr = PluginManager::new();
398 mgr.register(Box::new(MockPlugin::new("p1")));
399 mgr.register(Box::new(MockPlugin::new("p2")));
400
401 assert!(mgr.remove("p1"));
402 assert_eq!(mgr.plugin_count(), 1);
403 assert_eq!(mgr.plugin_names(), vec!["p2"]);
404 }
405
406 #[test]
407 fn manager_remove_nonexistent() {
408 let mut mgr = PluginManager::new();
409 assert!(!mgr.remove("nope"));
410 }
411
412 #[test]
413 fn manager_clear_errors() {
414 let mut mgr = PluginManager::new();
415 mgr.register(Box::new(MockPlugin::failing("bad")));
416 mgr.dispatch_event(&TestEvent::Warning {
417 message: "x".into(),
418 });
419 assert_eq!(mgr.errors().len(), 1);
420 mgr.clear_errors();
421 assert!(mgr.errors().is_empty());
422 }
423
424 #[test]
425 fn manager_has_fatal_error() {
426 let mgr = PluginManager::new();
427 assert!(!mgr.has_fatal_error());
428 }
429
430 #[test]
431 fn plugin_error_display() {
432 let err = PluginError::new("test", "something broke".into(), false);
433 assert_eq!(format!("{err}"), "[plugin:test] something broke");
434
435 let fatal = PluginError::new("test", "critical".into(), true);
436 assert!(format!("{fatal}").contains("(fatal)"));
437 }
438
439 #[test]
440 fn registry_builtin() {
441 let registry = PluginRegistry::builtin();
442 assert_eq!(registry.list_available().len(), 4);
443 assert!(registry.find("markdown").is_some());
444 assert!(registry.find("github").is_some());
445 assert!(registry.find("html").is_some());
446 assert!(registry.find("notify").is_some());
447 }
448
449 #[test]
450 fn registry_find_missing() {
451 let registry = PluginRegistry::builtin();
452 assert!(registry.find("nonexistent").is_none());
453 }
454
455 #[test]
456 fn registry_custom() {
457 let mut registry = PluginRegistry::new();
458 registry.register_available(PluginInfo::new("custom", "0.1.0", "A custom plugin"));
459 assert_eq!(registry.list_available().len(), 1);
460 assert_eq!(registry.find("custom").unwrap().version, "0.1.0");
461 }
462
463 #[test]
464 fn plugin_info_new() {
465 let info = PluginInfo::new("test", "1.0.0", "Test plugin");
466 assert_eq!(info.name, "test");
467 assert_eq!(info.version, "1.0.0");
468 assert_eq!(info.description, "Test plugin");
469 }
470
471 #[test]
472 fn manager_multiple_events() {
473 let mut mgr = PluginManager::new();
474 mgr.register(Box::new(MockPlugin::new("p1")));
475
476 for i in 0..10 {
477 mgr.dispatch_event(&TestEvent::Progress {
478 message: format!("step {i}"),
479 current: i,
480 total: 10,
481 });
482 }
483
484 assert!(mgr.errors().is_empty());
485 }
486
487 #[test]
488 fn manager_error_on_result() {
489 let mut mgr = PluginManager::new();
490 mgr.register(Box::new(MockPlugin::failing("bad")));
491
492 mgr.dispatch_result(&make_result());
493 assert_eq!(mgr.errors().len(), 1);
494 assert!(mgr.errors()[0].message.contains("on_result"));
495 }
496
497 #[test]
500 fn manager_dispatch_event_to_empty() {
501 let mut mgr = PluginManager::new();
502 mgr.dispatch_event(&TestEvent::Warning {
503 message: "test".into(),
504 });
505 assert!(mgr.errors().is_empty());
506 }
507
508 #[test]
509 fn manager_dispatch_result_to_empty() {
510 let mut mgr = PluginManager::new();
511 mgr.dispatch_result(&make_result());
512 assert!(mgr.errors().is_empty());
513 }
514
515 #[test]
516 fn manager_shutdown_empty() {
517 let mut mgr = PluginManager::new();
518 mgr.shutdown_all();
519 assert!(mgr.errors().is_empty());
520 }
521
522 #[test]
523 fn manager_failing_plugin_does_not_block_others() {
524 let mut mgr = PluginManager::new();
525 mgr.register(Box::new(MockPlugin::failing("bad")));
526 mgr.register(Box::new(MockPlugin::new("good")));
527
528 mgr.dispatch_result(&make_result());
529 assert_eq!(mgr.errors().len(), 1);
531 assert_eq!(mgr.errors()[0].plugin_name, "bad");
532 assert_eq!(mgr.plugin_count(), 2);
534 }
535
536 #[test]
537 fn manager_multiple_failing_plugins() {
538 let mut mgr = PluginManager::new();
539 mgr.register(Box::new(MockPlugin::failing("bad1")));
540 mgr.register(Box::new(MockPlugin::failing("bad2")));
541 mgr.register(Box::new(MockPlugin::new("good")));
542
543 mgr.dispatch_event(&TestEvent::Warning {
544 message: "x".into(),
545 });
546 assert_eq!(mgr.errors().len(), 2);
547 assert_eq!(mgr.errors()[0].plugin_name, "bad1");
548 assert_eq!(mgr.errors()[1].plugin_name, "bad2");
549 }
550
551 #[test]
552 fn manager_errors_accumulate_across_dispatches() {
553 let mut mgr = PluginManager::new();
554 mgr.register(Box::new(MockPlugin::failing("bad")));
555
556 mgr.dispatch_event(&TestEvent::Warning {
557 message: "1".into(),
558 });
559 mgr.dispatch_event(&TestEvent::Warning {
560 message: "2".into(),
561 });
562 mgr.dispatch_result(&make_result());
563
564 assert_eq!(mgr.errors().len(), 3);
565 }
566
567 #[test]
568 fn manager_clear_errors_then_new_errors() {
569 let mut mgr = PluginManager::new();
570 mgr.register(Box::new(MockPlugin::failing("bad")));
571
572 mgr.dispatch_event(&TestEvent::Warning {
573 message: "x".into(),
574 });
575 assert_eq!(mgr.errors().len(), 1);
576
577 mgr.clear_errors();
578 assert!(mgr.errors().is_empty());
579
580 mgr.dispatch_event(&TestEvent::Warning {
581 message: "y".into(),
582 });
583 assert_eq!(mgr.errors().len(), 1);
584 }
585
586 #[test]
587 fn manager_remove_all_plugins() {
588 let mut mgr = PluginManager::new();
589 mgr.register(Box::new(MockPlugin::new("a")));
590 mgr.register(Box::new(MockPlugin::new("b")));
591
592 assert!(mgr.remove("a"));
593 assert!(mgr.remove("b"));
594 assert_eq!(mgr.plugin_count(), 0);
595 assert!(!mgr.remove("a")); }
597
598 #[test]
599 fn manager_register_duplicate_names() {
600 let mut mgr = PluginManager::new();
601 mgr.register(Box::new(MockPlugin::new("dup")));
602 mgr.register(Box::new(MockPlugin::new("dup")));
603 assert_eq!(mgr.plugin_count(), 2);
604 assert_eq!(mgr.plugin_names(), vec!["dup", "dup"]);
605
606 assert!(mgr.remove("dup"));
608 assert_eq!(mgr.plugin_count(), 0);
609 }
610
611 #[test]
612 fn manager_default_trait() {
613 let mgr = PluginManager::default();
614 assert_eq!(mgr.plugin_count(), 0);
615 }
616
617 #[test]
618 fn manager_has_fatal_error_with_non_fatal_errors() {
619 let mut mgr = PluginManager::new();
620 mgr.register(Box::new(MockPlugin::failing("bad")));
621 mgr.dispatch_event(&TestEvent::Warning {
622 message: "x".into(),
623 });
624 assert!(!mgr.has_fatal_error()); }
626
627 #[test]
628 fn plugin_error_non_fatal_display() {
629 let err = PluginError::new("plug", "oops".into(), false);
630 let display = format!("{err}");
631 assert_eq!(display, "[plugin:plug] oops");
632 assert!(!display.contains("fatal"));
633 }
634
635 #[test]
636 fn plugin_error_fatal_display() {
637 let err = PluginError::new("plug", "critical".into(), true);
638 let display = format!("{err}");
639 assert!(display.contains("(fatal)"));
640 assert!(display.contains("critical"));
641 }
642
643 #[test]
644 fn plugin_error_clone() {
645 let err = PluginError::new("test", "msg".into(), true);
646 let cloned = err.clone();
647 assert_eq!(cloned.plugin_name, "test");
648 assert_eq!(cloned.message, "msg");
649 assert!(cloned.fatal);
650 }
651
652 #[test]
653 fn plugin_error_debug() {
654 let err = PluginError::new("test", "msg".into(), false);
655 let debug = format!("{err:?}");
656 assert!(debug.contains("test"));
657 assert!(debug.contains("msg"));
658 }
659
660 #[test]
661 fn registry_empty() {
662 let registry = PluginRegistry::new();
663 assert!(registry.list_available().is_empty());
664 assert!(registry.find("anything").is_none());
665 }
666
667 #[test]
668 fn registry_default_trait() {
669 let registry = PluginRegistry::default();
670 assert!(registry.list_available().is_empty());
671 }
672
673 #[test]
674 fn registry_builtin_count_and_names() {
675 let registry = PluginRegistry::builtin();
676 let names: Vec<&str> = registry
677 .list_available()
678 .iter()
679 .map(|p| p.name.as_str())
680 .collect();
681 assert_eq!(names.len(), 4);
682 assert!(names.contains(&"markdown"));
683 assert!(names.contains(&"github"));
684 assert!(names.contains(&"html"));
685 assert!(names.contains(&"notify"));
686 }
687
688 #[test]
689 fn registry_builtin_versions() {
690 let registry = PluginRegistry::builtin();
691 for info in registry.list_available() {
692 assert_eq!(info.version, "1.0.0");
693 assert!(!info.description.is_empty());
694 }
695 }
696
697 #[test]
698 fn registry_multiple_custom() {
699 let mut registry = PluginRegistry::new();
700 registry.register_available(PluginInfo::new("a", "0.1", "Plugin A"));
701 registry.register_available(PluginInfo::new("b", "0.2", "Plugin B"));
702 assert_eq!(registry.list_available().len(), 2);
703 assert_eq!(registry.find("a").unwrap().version, "0.1");
704 assert_eq!(registry.find("b").unwrap().version, "0.2");
705 }
706
707 #[test]
708 fn manager_dispatch_all_event_types() {
709 let mut mgr = PluginManager::new();
710 mgr.register(Box::new(MockPlugin::new("p1")));
711
712 let events = vec![
713 TestEvent::RunStarted {
714 adapter: "rust".into(),
715 framework: "cargo test".into(),
716 project_dir: std::path::PathBuf::from("/tmp"),
717 },
718 TestEvent::SuiteStarted {
719 name: "math".into(),
720 },
721 TestEvent::TestStarted {
722 suite: "math".into(),
723 name: "add".into(),
724 },
725 TestEvent::TestFinished {
726 suite: "math".into(),
727 test: TestCase {
728 name: "add".into(),
729 status: TestStatus::Passed,
730 duration: Duration::from_millis(1),
731 error: None,
732 },
733 },
734 TestEvent::SuiteFinished {
735 suite: TestSuite {
736 name: "math".into(),
737 tests: vec![],
738 },
739 },
740 TestEvent::RunFinished {
741 result: make_result(),
742 },
743 TestEvent::Warning {
744 message: "warn".into(),
745 },
746 TestEvent::Progress {
747 message: "step".into(),
748 current: 1,
749 total: 10,
750 },
751 ];
752
753 for event in &events {
754 mgr.dispatch_event(event);
755 }
756 assert!(mgr.errors().is_empty());
757 }
758
759 #[test]
760 fn manager_dispatch_result_with_empty_result() {
761 let mut mgr = PluginManager::new();
762 mgr.register(Box::new(MockPlugin::new("p")));
763
764 let empty_result = TestRunResult {
765 suites: vec![],
766 duration: Duration::ZERO,
767 raw_exit_code: 0,
768 };
769 mgr.dispatch_result(&empty_result);
770 assert!(mgr.errors().is_empty());
771 }
772
773 #[test]
774 fn manager_dispatch_result_with_large_result() {
775 let mut mgr = PluginManager::new();
776 mgr.register(Box::new(MockPlugin::new("p")));
777
778 let tests: Vec<TestCase> = (0..1000)
779 .map(|i| TestCase {
780 name: format!("test_{i}"),
781 status: if i % 10 == 0 {
782 TestStatus::Failed
783 } else {
784 TestStatus::Passed
785 },
786 duration: Duration::from_millis(i as u64),
787 error: None,
788 })
789 .collect();
790
791 let large_result = TestRunResult {
792 suites: vec![TestSuite {
793 name: "big".into(),
794 tests,
795 }],
796 duration: Duration::from_secs(60),
797 raw_exit_code: 1,
798 };
799 mgr.dispatch_result(&large_result);
800 assert!(mgr.errors().is_empty());
801 }
802}