Integrate the reporting infrastructure with the

CLI reporter used by the program.
This commit is contained in:
Omar Abdulla
2025-08-25 00:24:10 +03:00
parent 5d9075b389
commit 7d87cb9ee8
3 changed files with 118 additions and 61 deletions
+55 -57
View File
@@ -18,9 +18,11 @@ use futures::stream;
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
use indexmap::IndexMap; use indexmap::IndexMap;
use revive_dt_node_interaction::EthereumNode; use revive_dt_node_interaction::EthereumNode;
use revive_dt_report::{ReportAggregator, Reporter, TestSpecificReporter, TestSpecifier}; use revive_dt_report::{
ReportAggregator, Reporter, ReporterEvent, TestCaseStatus, TestSpecificReporter, TestSpecifier,
};
use temp_dir::TempDir; use temp_dir::TempDir;
use tokio::{join, sync::mpsc, try_join}; use tokio::{join, sync::broadcast::Receiver, try_join};
use tracing::{debug, info, info_span, instrument}; use tracing::{debug, info, info_span, instrument};
use tracing_appender::non_blocking::WorkerGuard; use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{EnvFilter, FmtSubscriber}; use tracing_subscriber::{EnvFilter, FmtSubscriber};
@@ -56,9 +58,6 @@ struct Test<'a> {
reporter: TestSpecificReporter, reporter: TestSpecificReporter,
} }
/// This represents the results that we gather from running test cases.
type CaseResult = Result<usize, anyhow::Error>;
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let (args, _guard) = init_cli()?; let (args, _guard) = init_cli()?;
info!( info!(
@@ -182,13 +181,13 @@ where
L::Blockchain: revive_dt_node::Node + Send + Sync + 'static, L::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
F::Blockchain: revive_dt_node::Node + Send + Sync + 'static, F::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
{ {
let (report_tx, report_rx) = mpsc::unbounded_channel::<(Test<'_>, CaseResult)>(); let reporter_events_rx = reporter.subscribe().await?;
let tests = prepare_tests::<L, F>(args, metadata_files, reporter); let tests = prepare_tests::<L, F>(args, metadata_files, reporter);
let driver_task = start_driver_task::<L, F>(args, tests, report_tx).await?; let driver_task = start_driver_task::<L, F>(args, tests).await?;
let status_reporter_task = start_reporter_task(report_rx); let cli_reporting_task = start_cli_reporting_task(reporter_events_rx);
let (_, _, rtn) = tokio::join!(status_reporter_task, driver_task, report_aggregator_task,); let (_, _, rtn) = tokio::join!(cli_reporting_task, driver_task, report_aggregator_task,);
rtn?; rtn?;
Ok(()) Ok(())
@@ -435,7 +434,6 @@ async fn does_compiler_support_mode<P: Platform>(
async fn start_driver_task<'a, L, F>( async fn start_driver_task<'a, L, F>(
args: &Arguments, args: &Arguments,
tests: impl Stream<Item = Test<'a>>, tests: impl Stream<Item = Test<'a>>,
_: mpsc::UnboundedSender<(Test<'a>, CaseResult)>,
) -> anyhow::Result<impl Future<Output = ()>> ) -> anyhow::Result<impl Future<Output = ()>>
where where
L: Platform, L: Platform,
@@ -510,74 +508,74 @@ where
)) ))
} }
async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test<'_>, CaseResult)>) { #[allow(clippy::uninlined_format_args)]
#[allow(irrefutable_let_patterns)]
async fn start_cli_reporting_task(mut aggregator_events_rx: Receiver<ReporterEvent>) {
let start = Instant::now(); let start = Instant::now();
const GREEN: &str = "\x1B[32m"; const GREEN: &str = "\x1B[32m";
const RED: &str = "\x1B[31m"; const RED: &str = "\x1B[31m";
const GREY: &str = "\x1B[90m";
const COLOR_RESET: &str = "\x1B[0m"; const COLOR_RESET: &str = "\x1B[0m";
const BOLD: &str = "\x1B[1m"; const BOLD: &str = "\x1B[1m";
const BOLD_RESET: &str = "\x1B[22m"; const BOLD_RESET: &str = "\x1B[22m";
let mut number_of_successes = 0; let mut number_of_successes = 0;
let mut number_of_failures = 0; let mut number_of_failures = 0;
let mut failures = vec![];
// Wait for reports to come from our test runner. When the channel closes, this ends.
let mut buf = BufWriter::new(stderr()); let mut buf = BufWriter::new(stderr());
while let Some((test, case_result)) = report_rx.recv().await { while let Ok(event) = aggregator_events_rx.recv().await {
let case_name = test.case.name.as_deref().unwrap_or("unnamed_case"); let ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted {
let case_idx = test.case_idx; metadata_file_path,
let test_path = test.metadata_file_path.display(); mode,
let test_mode = test.mode.clone(); case_status,
} = event
else {
continue;
};
match case_result { let _ = writeln!(buf, "{} - {}", mode, metadata_file_path.display());
Ok(_inputs) => { for (case_idx, case_status) in case_status.into_iter() {
number_of_successes += 1; let _ = write!(buf, "\tCase Index {case_idx:>3}: ");
let _ = writeln!( let _ = match case_status {
TestCaseStatus::Succeeded { steps_executed } => {
number_of_successes += 1;
writeln!(
buf,
"{}{}Case Succeeded{}{} - Steps Executed: {}",
GREEN, BOLD, BOLD_RESET, COLOR_RESET, steps_executed
)
}
TestCaseStatus::Failed { reason } => {
number_of_failures += 1;
writeln!(
buf,
"{}{}Case Failed{}{} - Reason: {}",
RED, BOLD, BOLD_RESET, COLOR_RESET, reason
)
}
TestCaseStatus::Ignored { reason, .. } => writeln!(
buf, buf,
"{GREEN}Case Succeeded:{COLOR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})" "{}{}Case Ignored{}{} - Reason: {}",
); GREY, BOLD, BOLD_RESET, COLOR_RESET, reason
} ),
Err(err) => { };
number_of_failures += 1;
let _ = writeln!(
buf,
"{RED}Case Failed:{COLOR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})"
);
failures.push((test, err));
}
}
}
let _ = writeln!(buf,);
let elapsed = start.elapsed();
// Now, log the failures with more complete errors at the bottom, like `cargo test` does, so
// that we don't have to scroll through the entire output to find them.
if !failures.is_empty() {
let _ = writeln!(buf, "{BOLD}Failures:{BOLD_RESET}\n");
for failure in failures {
let (test, err) = failure;
let case_name = test.case.name.as_deref().unwrap_or("unnamed_case");
let case_idx = test.case_idx;
let test_path = test.metadata_file_path.display();
let test_mode = test.mode.clone();
let _ = writeln!(
buf,
"---- {RED}Case Failed:{COLOR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode}) ----\n\n{err}\n"
);
} }
let _ = writeln!(buf);
} }
// Summary at the end. // Summary at the end.
let _ = writeln!( let _ = writeln!(
buf, buf,
"{} cases: {GREEN}{number_of_successes}{COLOR_RESET} cases succeeded, {RED}{number_of_failures}{COLOR_RESET} cases failed in {} seconds", "{} cases: {}{}{} cases succeeded, {}{}{} cases failed in {} seconds",
number_of_successes + number_of_failures, number_of_successes + number_of_failures,
elapsed.as_secs() GREEN,
number_of_successes,
COLOR_RESET,
RED,
number_of_failures,
COLOR_RESET,
start.elapsed().as_secs()
); );
} }
+44 -3
View File
@@ -126,7 +126,7 @@ impl ReportAggregator {
} }
fn handle_test_succeeded_event(&mut self, event: TestSucceededEvent) { fn handle_test_succeeded_event(&mut self, event: TestSucceededEvent) {
// Remove this from the set of cases we're tracking // Remove this from the set of cases we're tracking since it has completed.
self.remaining_cases self.remaining_cases
.entry(event.test_specifier.metadata_file_path.clone().into()) .entry(event.test_specifier.metadata_file_path.clone().into())
.or_default() .or_default()
@@ -139,10 +139,11 @@ impl ReportAggregator {
test_case_report.status = Some(TestCaseStatus::Succeeded { test_case_report.status = Some(TestCaseStatus::Succeeded {
steps_executed: event.steps_executed, steps_executed: event.steps_executed,
}); });
self.handle_post_test_case_status_update(&event.test_specifier);
} }
fn handle_test_failed_event(&mut self, event: TestFailedEvent) { fn handle_test_failed_event(&mut self, event: TestFailedEvent) {
// Remove this from the set of cases we're tracking // Remove this from the set of cases we're tracking since it has completed.
self.remaining_cases self.remaining_cases
.entry(event.test_specifier.metadata_file_path.clone().into()) .entry(event.test_specifier.metadata_file_path.clone().into())
.or_default() .or_default()
@@ -155,10 +156,11 @@ impl ReportAggregator {
test_case_report.status = Some(TestCaseStatus::Failed { test_case_report.status = Some(TestCaseStatus::Failed {
reason: event.reason, reason: event.reason,
}); });
self.handle_post_test_case_status_update(&event.test_specifier);
} }
fn handle_test_ignored_event(&mut self, event: TestIgnoredEvent) { fn handle_test_ignored_event(&mut self, event: TestIgnoredEvent) {
// Remove this from the set of cases we're tracking // Remove this from the set of cases we're tracking since it has completed.
self.remaining_cases self.remaining_cases
.entry(event.test_specifier.metadata_file_path.clone().into()) .entry(event.test_specifier.metadata_file_path.clone().into())
.or_default() .or_default()
@@ -172,6 +174,45 @@ impl ReportAggregator {
reason: event.reason, reason: event.reason,
additional_fields: event.additional_fields, additional_fields: event.additional_fields,
}); });
self.handle_post_test_case_status_update(&event.test_specifier);
}
fn handle_post_test_case_status_update(&mut self, specifier: &TestSpecifier) {
let remaining_cases = self
.remaining_cases
.entry(specifier.metadata_file_path.clone().into())
.or_default()
.entry(specifier.solc_mode.clone())
.or_default();
if !remaining_cases.is_empty() {
return;
}
let case_status = self
.report
.test_case_information
.entry(specifier.metadata_file_path.clone().into())
.or_default()
.entry(specifier.solc_mode.clone())
.or_default()
.iter()
.map(|(case_idx, case_report)| {
(
*case_idx,
case_report.status.clone().expect("Can't be uninitialized"),
)
})
.collect::<BTreeMap<_, _>>();
let event = ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted {
metadata_file_path: specifier.metadata_file_path.clone().into(),
mode: specifier.solc_mode.clone(),
case_status,
};
// According to the documentation on send, the sending fails if there are no more receiver
// handles. Therefore, this isn't an error that we want to bubble up or anything. If we fail
// to send then we ignore the error.
let _ = self.listener_tx.send(event);
} }
fn handle_leader_node_assigned_event(&mut self, event: LeaderNodeAssignedEvent) { fn handle_leader_node_assigned_event(&mut self, event: LeaderNodeAssignedEvent) {
+19 -1
View File
@@ -1,4 +1,22 @@
//! A reporter event sent by the report aggregator to the various listeners. //! A reporter event sent by the report aggregator to the various listeners.
use std::collections::BTreeMap;
use revive_dt_compiler::Mode;
use revive_dt_format::case::CaseIdx;
use crate::{MetadataFilePath, TestCaseStatus};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ReporterEvent {} pub enum ReporterEvent {
/// An event sent by the reporter once an entire metadata file and solc mode combination has
/// finished execution.
MetadataFileSolcModeCombinationExecutionCompleted {
/// The path of the metadata file.
metadata_file_path: MetadataFilePath,
/// The Solc mode that this metadata file was executed in.
mode: Mode,
/// The status of each one of the cases.
case_status: BTreeMap<CaseIdx, TestCaseStatus>,
},
}