pub mod allure;
pub mod bdd;
pub mod blob;
pub mod dot;
pub mod empty;
pub mod github;
pub mod html;
pub mod json;
pub mod junit;
pub mod progress;
pub mod rerun;
pub mod terminal;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
use crate::model::{StepCategory, TestId, TestOutcome};
#[derive(Debug, Clone)]
pub struct StepStartedEvent {
pub test_id: TestId,
pub step_id: String,
pub parent_step_id: Option<String>,
pub title: String,
pub category: StepCategory,
}
#[derive(Debug, Clone)]
pub struct StepFinishedEvent {
pub test_id: TestId,
pub step_id: String,
pub title: String,
pub category: StepCategory,
pub duration: Duration,
pub error: Option<String>,
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub enum ReporterEvent {
RunStarted {
total_tests: usize,
num_workers: u32,
metadata: serde_json::Value,
},
WorkerStarted { worker_id: u32 },
TestStarted { test_id: TestId, attempt: u32 },
StepStarted(Box<StepStartedEvent>),
StepFinished(Box<StepFinishedEvent>),
TestFinished { test_id: TestId, outcome: TestOutcome },
WorkerFinished { worker_id: u32 },
RunFinished {
total: usize,
passed: usize,
failed: usize,
skipped: usize,
flaky: usize,
duration: Duration,
},
}
#[async_trait::async_trait]
pub trait Reporter: Send + Sync {
async fn on_event(&mut self, event: &ReporterEvent);
async fn finalize(&mut self) -> ferridriver::error::Result<()> {
Ok(())
}
}
pub struct ReporterSet {
reporters: Vec<Box<dyn Reporter>>,
}
impl Default for ReporterSet {
fn default() -> Self {
Self { reporters: Vec::new() }
}
}
impl ReporterSet {
pub fn new(reporters: Vec<Box<dyn Reporter>>) -> Self {
Self { reporters }
}
pub fn is_empty(&self) -> bool {
self.reporters.is_empty()
}
pub fn add(&mut self, reporter: Box<dyn Reporter>) {
self.reporters.push(reporter);
}
pub fn replace(&mut self, reporters: Vec<Box<dyn Reporter>>) {
self.reporters = reporters;
}
pub async fn emit(&mut self, event: &ReporterEvent) {
for reporter in &mut self.reporters {
reporter.on_event(event).await;
}
}
pub async fn finalize(&mut self) {
for reporter in &mut self.reporters {
if let Err(e) = reporter.finalize().await {
tracing::error!("reporter finalize error: {e}");
}
}
}
}
pub struct EventBusBuilder {
subscribers: Vec<mpsc::UnboundedSender<ReporterEvent>>,
}
impl Default for EventBusBuilder {
fn default() -> Self {
Self::new()
}
}
impl EventBusBuilder {
pub fn new() -> Self {
Self {
subscribers: Vec::new(),
}
}
pub fn subscribe(&mut self) -> Subscription {
let (tx, rx) = mpsc::unbounded_channel();
self.subscribers.push(tx);
Subscription { rx }
}
pub fn build(self) -> EventBus {
let has_subscribers = !self.subscribers.is_empty();
EventBus {
inner: Arc::new(EventBusInner {
has_subscribers,
subscribers: std::sync::RwLock::new(self.subscribers),
}),
}
}
}
pub struct Subscription {
pub rx: mpsc::UnboundedReceiver<ReporterEvent>,
}
#[derive(Clone)]
pub struct EventBus {
inner: Arc<EventBusInner>,
}
struct EventBusInner {
has_subscribers: bool,
subscribers: std::sync::RwLock<Vec<mpsc::UnboundedSender<ReporterEvent>>>,
}
impl EventBus {
pub fn has_subscribers(&self) -> bool {
self.inner.has_subscribers
}
pub fn emit(&self, event: ReporterEvent) {
if !self.inner.has_subscribers {
return;
}
let subs = self.inner.subscribers.read().expect("EventBus RwLock poisoned");
if subs.is_empty() {
return;
}
let last = subs.len() - 1;
for sub in &subs[..last] {
let _ = sub.send(event.clone());
}
let _ = subs[last].send(event);
}
pub fn close(&self) {
self
.inner
.subscribers
.write()
.expect("EventBus RwLock poisoned")
.clear();
}
}
pub struct ReporterDriver {
reporters: ReporterSet,
subscription: Subscription,
}
impl ReporterDriver {
pub fn new(reporters: ReporterSet, subscription: Subscription) -> Self {
Self {
reporters,
subscription,
}
}
pub async fn run(mut self) -> ReporterSet {
while let Some(event) = self.subscription.rx.recv().await {
self.reporters.emit(&event).await;
}
self.reporters.finalize().await;
self.reporters
}
}
pub fn create_reporters_pub(
names: &[crate::config::ReporterConfig],
output_dir: &std::path::Path,
has_bdd: bool,
quiet: bool,
report_slow_tests: Option<crate::config::ReportSlowTestsConfig>,
) -> ReporterSet {
create_reporters(names, output_dir, has_bdd, quiet, report_slow_tests)
}
pub(crate) fn create_reporters(
names: &[crate::config::ReporterConfig],
output_dir: &std::path::Path,
_has_bdd: bool,
quiet: bool,
report_slow_tests: Option<crate::config::ReportSlowTestsConfig>,
) -> ReporterSet {
if names.len() == 1 && matches!(names[0].name.as_str(), "none" | "null" | "empty") {
return ReporterSet::default();
}
let mut reporters: Vec<Box<dyn Reporter>> = Vec::new();
let mut has_terminal = false;
for config in names {
match config.name.as_str() {
"terminal" | "list" | "bdd" | "default" | "" => {
if !has_terminal && !quiet {
reporters.push(Box::new(
terminal::TerminalReporter::new().with_slow_tests_config(report_slow_tests.clone()),
));
has_terminal = true;
}
},
"json" => {
reporters.push(Box::new(json::JsonReporter::new(output_dir.join("results.json"))));
},
"junit" => {
reporters.push(Box::new(junit::JUnitReporter::new(output_dir.join("junit.xml"))));
},
"dot" => {
reporters.push(Box::new(dot::DotReporter::new()));
},
"null" | "empty" => {
reporters.push(Box::new(empty::EmptyReporter));
},
"blob" => {
let path = config
.options
.get("path")
.and_then(|v| v.as_str())
.map(std::path::PathBuf::from)
.unwrap_or_else(|| output_dir.join("report.zip"));
let mut reporter = blob::BlobReporter::new(path);
if let (Some(current), Some(total)) = (
config
.options
.get("shard_index")
.and_then(|v| v.as_u64())
.and_then(|v| u32::try_from(v).ok()),
config
.options
.get("shard_total")
.and_then(|v| v.as_u64())
.and_then(|v| u32::try_from(v).ok()),
) {
reporter = reporter.with_shard(current, total);
}
reporters.push(Box::new(reporter));
},
"github" => {
let inner: Box<dyn Reporter> = if quiet {
Box::new(empty::EmptyReporter)
} else {
Box::new(terminal::TerminalReporter::new().with_slow_tests_config(report_slow_tests.clone()))
};
let mut reporter = github::GithubReporter::new(inner);
if let Some(force) = config.options.get("enabled").and_then(|v| v.as_bool()) {
reporter = reporter.with_enabled(force);
}
reporters.push(Box::new(reporter));
},
"html" => {
reporters.push(Box::new(html::HtmlReporter::new(output_dir.join("report.html"))));
},
"allure" => {
let dir = config
.options
.get("output_dir")
.and_then(|v| v.as_str())
.map(std::path::PathBuf::from)
.unwrap_or_else(|| output_dir.join("allure-results"));
let mut reporter = allure::AllureReporter::new(dir);
if let Some(title) = config.options.get("suite_title").and_then(|v| v.as_str()) {
reporter = reporter.with_suite_title(title.to_string());
}
reporters.push(Box::new(reporter));
},
"progress" => {
reporters.push(Box::new(progress::ProgressReporter::new()));
},
"rerun" => {
reporters.push(Box::new(rerun::RerunReporter::new(output_dir.join("@rerun.txt"))));
},
"cucumber-json" | "cucumber" => {
reporters.push(Box::new(bdd::cucumber_json::CucumberJsonReporter::new(
output_dir.join("cucumber.json"),
)));
},
"messages" | "ndjson" => {
reporters.push(Box::new(bdd::messages::CucumberMessagesReporter::new(
output_dir.join("cucumber-messages.ndjson"),
)));
},
"usage" => {
reporters.push(Box::new(bdd::usage::UsageReporter::new()));
},
other => {
tracing::warn!("unknown reporter: {other}, skipping");
},
}
}
if reporters.is_empty() {
reporters.push(Box::new(terminal::TerminalReporter::new()));
}
let has_rerun = names.iter().any(|c| c.name == "rerun");
if !has_rerun {
reporters.push(Box::new(rerun::RerunReporter::new(output_dir.join("@rerun.txt"))));
}
ReporterSet::new(reporters)
}