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}