ferridriver_test/reporter/
mod.rs1pub mod allure;
4pub mod bdd;
5pub mod html;
6pub mod json;
7pub mod junit;
8pub mod progress;
9pub mod rerun;
10pub mod terminal;
11
12use std::sync::Arc;
13use std::time::Duration;
14
15use tokio::sync::mpsc;
16
17use crate::model::{StepCategory, TestId, TestOutcome};
18
19#[derive(Debug, Clone)]
22pub struct StepStartedEvent {
23 pub test_id: TestId,
24 pub step_id: String,
25 pub parent_step_id: Option<String>,
26 pub title: String,
27 pub category: StepCategory,
28}
29
30#[derive(Debug, Clone)]
31pub struct StepFinishedEvent {
32 pub test_id: TestId,
33 pub step_id: String,
34 pub title: String,
35 pub category: StepCategory,
36 pub duration: Duration,
37 pub error: Option<String>,
38 pub metadata: Option<serde_json::Value>,
40}
41
42#[derive(Debug, Clone)]
44pub enum ReporterEvent {
45 RunStarted {
47 total_tests: usize,
48 num_workers: u32,
49 metadata: serde_json::Value,
51 },
52 WorkerStarted { worker_id: u32 },
54 TestStarted { test_id: TestId, attempt: u32 },
56 StepStarted(Box<StepStartedEvent>),
58 StepFinished(Box<StepFinishedEvent>),
60 TestFinished { test_id: TestId, outcome: TestOutcome },
62 WorkerFinished { worker_id: u32 },
64 RunFinished {
66 total: usize,
67 passed: usize,
68 failed: usize,
69 skipped: usize,
70 flaky: usize,
71 duration: Duration,
72 },
73}
74
75#[async_trait::async_trait]
79pub trait Reporter: Send + Sync {
80 async fn on_event(&mut self, event: &ReporterEvent);
82
83 async fn finalize(&mut self) -> Result<(), String> {
85 Ok(())
86 }
87}
88
89pub struct ReporterSet {
93 reporters: Vec<Box<dyn Reporter>>,
94}
95
96impl Default for ReporterSet {
97 fn default() -> Self {
98 Self { reporters: Vec::new() }
99 }
100}
101
102impl ReporterSet {
103 pub fn new(reporters: Vec<Box<dyn Reporter>>) -> Self {
104 Self { reporters }
105 }
106
107 pub fn add(&mut self, reporter: Box<dyn Reporter>) {
109 self.reporters.push(reporter);
110 }
111
112 pub fn replace(&mut self, reporters: Vec<Box<dyn Reporter>>) {
114 self.reporters = reporters;
115 }
116
117 pub async fn emit(&mut self, event: &ReporterEvent) {
118 for reporter in &mut self.reporters {
119 reporter.on_event(event).await;
120 }
121 }
122
123 pub async fn finalize(&mut self) {
124 for reporter in &mut self.reporters {
125 if let Err(e) = reporter.finalize().await {
126 tracing::error!("reporter finalize error: {e}");
127 }
128 }
129 }
130}
131
132pub struct EventBusBuilder {
140 subscribers: Vec<mpsc::UnboundedSender<ReporterEvent>>,
141}
142
143impl Default for EventBusBuilder {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149impl EventBusBuilder {
150 pub fn new() -> Self {
151 Self {
152 subscribers: Vec::new(),
153 }
154 }
155
156 pub fn subscribe(&mut self) -> Subscription {
159 let (tx, rx) = mpsc::unbounded_channel();
160 self.subscribers.push(tx);
161 Subscription { rx }
162 }
163
164 pub fn build(self) -> EventBus {
166 EventBus {
167 inner: Arc::new(EventBusInner {
168 subscribers: std::sync::RwLock::new(self.subscribers),
169 }),
170 }
171 }
172}
173
174pub struct Subscription {
176 pub rx: mpsc::UnboundedReceiver<ReporterEvent>,
177}
178
179#[derive(Clone)]
184pub struct EventBus {
185 inner: Arc<EventBusInner>,
186}
187
188struct EventBusInner {
189 subscribers: std::sync::RwLock<Vec<mpsc::UnboundedSender<ReporterEvent>>>,
192}
193
194impl EventBus {
195 pub async fn emit(&self, event: ReporterEvent) {
198 let subs = self.inner.subscribers.read().expect("EventBus RwLock poisoned");
199 if subs.is_empty() {
200 return;
201 }
202 let last = subs.len() - 1;
203 for sub in &subs[..last] {
204 let _ = sub.send(event.clone());
205 }
206 let _ = subs[last].send(event);
207 }
208
209 pub fn close(&self) {
211 self
212 .inner
213 .subscribers
214 .write()
215 .expect("EventBus RwLock poisoned")
216 .clear();
217 }
218}
219
220pub struct ReporterDriver {
229 reporters: ReporterSet,
230 subscription: Subscription,
231}
232
233impl ReporterDriver {
234 pub fn new(reporters: ReporterSet, subscription: Subscription) -> Self {
235 Self {
236 reporters,
237 subscription,
238 }
239 }
240
241 pub async fn run(mut self) -> ReporterSet {
243 while let Some(event) = self.subscription.rx.recv().await {
244 self.reporters.emit(&event).await;
245 }
246 self.reporters.finalize().await;
247 self.reporters
248 }
249}
250
251pub(crate) fn create_reporters(
256 names: &[crate::config::ReporterConfig],
257 output_dir: &std::path::Path,
258 _has_bdd: bool,
259 quiet: bool,
260 report_slow_tests: Option<crate::config::ReportSlowTestsConfig>,
261) -> ReporterSet {
262 let mut reporters: Vec<Box<dyn Reporter>> = Vec::new();
263 let mut has_terminal = false;
264
265 for config in names {
266 match config.name.as_str() {
267 "terminal" | "list" | "bdd" | "default" | "" => {
269 if !has_terminal && !quiet {
270 reporters.push(Box::new(
271 terminal::TerminalReporter::new().with_slow_tests_config(report_slow_tests.clone()),
272 ));
273 has_terminal = true;
274 }
275 },
276 "json" => {
277 reporters.push(Box::new(json::JsonReporter::new(output_dir.join("results.json"))));
278 },
279 "junit" => {
280 reporters.push(Box::new(junit::JUnitReporter::new(output_dir.join("junit.xml"))));
281 },
282
283 "html" => {
285 reporters.push(Box::new(html::HtmlReporter::new(output_dir.join("report.html"))));
286 },
287 "allure" => {
288 let dir = config
289 .options
290 .get("output_dir")
291 .and_then(|v| v.as_str())
292 .map(std::path::PathBuf::from)
293 .unwrap_or_else(|| output_dir.join("allure-results"));
294 let mut reporter = allure::AllureReporter::new(dir);
295 if let Some(title) = config.options.get("suite_title").and_then(|v| v.as_str()) {
296 reporter = reporter.with_suite_title(title.to_string());
297 }
298 reporters.push(Box::new(reporter));
299 },
300 "progress" => {
301 reporters.push(Box::new(progress::ProgressReporter::new()));
302 },
303 "rerun" => {
304 reporters.push(Box::new(rerun::RerunReporter::new(output_dir.join("@rerun.txt"))));
305 },
306
307 "cucumber-json" | "cucumber" => {
309 reporters.push(Box::new(bdd::cucumber_json::CucumberJsonReporter::new(
310 output_dir.join("cucumber.json"),
311 )));
312 },
313 "messages" | "ndjson" => {
314 reporters.push(Box::new(bdd::messages::CucumberMessagesReporter::new(
315 output_dir.join("cucumber-messages.ndjson"),
316 )));
317 },
318 "usage" => {
319 reporters.push(Box::new(bdd::usage::UsageReporter::new()));
320 },
321
322 other => {
323 tracing::warn!("unknown reporter: {other}, skipping");
324 },
325 }
326 }
327
328 if reporters.is_empty() {
329 reporters.push(Box::new(terminal::TerminalReporter::new()));
330 }
331
332 let has_rerun = names.iter().any(|c| c.name == "rerun");
334 if !has_rerun {
335 reporters.push(Box::new(rerun::RerunReporter::new(output_dir.join("@rerun.txt"))));
336 }
337
338 ReporterSet::new(reporters)
339}