Skip to content
Merged
37 changes: 36 additions & 1 deletion crates/forge/bin/cmd/test/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{install, test::filter::ProjectPathsAwareFilter, watch::WatchArgs};
use alloy_primitives::U256;
use clap::Parser;

use eyre::Result;
use forge::{
decode::decode_console_logs,
Expand Down Expand Up @@ -40,6 +41,9 @@ use watchexec::config::{InitConfig, RuntimeConfig};
use yansi::Paint;

mod filter;
mod summary;
use summary::TestSummaryReporter;

pub use filter::FilterArgs;

// Loads project's figment and merges the build cli arguments into it
Expand Down Expand Up @@ -109,6 +113,14 @@ pub struct TestArgs {

#[clap(flatten)]
pub watch: WatchArgs,

/// Print test summary table
#[clap(long, help_heading = "Display options")]
pub summary: bool,

/// Print detailed test summary table
#[clap(long, help_heading = "Display options")]
pub detailed: bool,
}

impl TestArgs {
Expand Down Expand Up @@ -338,6 +350,11 @@ impl TestArgs {
} else if self.list {
list(runner, filter, self.json)
} else {
if self.detailed && !self.summary {
return Err(eyre::eyre!(
"Missing `--summary` option in your command. You must pass it along with the `--detailed` option to view detailed test summary."
));
}
test(
config,
runner,
Expand All @@ -348,6 +365,8 @@ impl TestArgs {
test_options,
self.gas_report,
self.fail_fast,
self.summary,
self.detailed,
)
.await
}
Expand Down Expand Up @@ -614,8 +633,11 @@ async fn test(
test_options: TestOptions,
gas_reporting: bool,
fail_fast: bool,
summary: bool,
detailed: bool,
) -> Result<TestOutcome> {
trace!(target: "forge::test", "running all tests");

if runner.matching_test_function_count(&filter) == 0 {
let filter_str = filter.to_string();
if filter_str.is_empty() {
Expand Down Expand Up @@ -663,6 +685,7 @@ async fn test(
let mut total_passed = 0;
let mut total_failed = 0;
let mut total_skipped = 0;
let mut suite_results: Vec<TestOutcome> = Vec::new();

'outer: for (contract_name, suite_result) in rx {
results.insert(contract_name.clone(), suite_result.clone());
Expand Down Expand Up @@ -754,13 +777,18 @@ async fn test(
gas_report.analyze(&result.traces);
}
}
let block_outcome = TestOutcome::new([(contract_name, suite_result)].into(), allow_failure);
let block_outcome =
TestOutcome::new([(contract_name.clone(), suite_result)].into(), allow_failure);

total_passed += block_outcome.successes().count();
total_failed += block_outcome.failures().count();
total_skipped += block_outcome.skips().count();

println!("{}", block_outcome.summary());

if summary {
suite_results.push(block_outcome.clone());
}
}

if gas_reporting {
Expand All @@ -774,12 +802,19 @@ async fn test(
"{}",
format_aggregated_summary(num_test_suites, total_passed, total_failed, total_skipped)
);

if summary {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering, can we move all the work for building and displaying the table to this if as it's only necessary if it's actually going to be displayed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would require storing all the test suite name and results to a separate object inside the loop and then later read inside the if block? I wanted to avoid adding new variable/storage.

We have to do the same to display the contract names in sorted order as well I guess. What do you guys think? Do you suggest storing the test results? @mds1 @Evalir @mattsse

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think in the end if we have to sort the contracts we have to do it—so feel free to go ahead!

let mut summary_table = TestSummaryReporter::new(detailed);
println!("\n\nTest Summary:");
summary_table.print_summary(suite_results);
}
}

// reattach the thread
let _results = handle.await?;

trace!(target: "forge::test", "received {} results", results.len());

Ok(TestOutcome::new(results, allow_failure))
}

Expand Down
111 changes: 111 additions & 0 deletions crates/forge/bin/cmd/test/summary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use crate::cmd::test::TestOutcome;
use comfy_table::{
modifiers::UTF8_ROUND_CORNERS, Attribute, Cell, CellAlignment, Color, Row, Table,
};

/// A simple summary reporter that prints the test results in a table.
pub struct TestSummaryReporter {
/// The test summary table.
pub(crate) table: Table,
pub(crate) is_detailed: bool,
}

impl TestSummaryReporter {
pub(crate) fn new(is_detailed: bool) -> Self {
let mut table = Table::new();
table.apply_modifier(UTF8_ROUND_CORNERS);
let mut row = Row::from(vec![
Cell::new("Test Suite")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold),
Cell::new("Passed")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold)
.fg(Color::Green),
Cell::new("Failed")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold)
.fg(Color::Red),
Cell::new("Skipped")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold)
.fg(Color::Yellow),
]);
if is_detailed {
row.add_cell(
Cell::new("File Path")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold),
);
row.add_cell(
Cell::new("Duration")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold),
);
}
table.set_header(row);

Self { table, is_detailed }
}

pub(crate) fn print_summary(&mut self, mut test_results: Vec<TestOutcome>) {
// Sort by suite name first

// Using `sort_by_cached_key` so that the key extraction logic runs only once
test_results.sort_by_cached_key(|test_outcome| {
test_outcome
.results
.keys()
.next()
.and_then(|suite| suite.split(':').nth(1))
.unwrap()
.to_string()
});

// Traverse the test_results vector and build the table
for suite in &test_results {
for contract in suite.results.keys() {
let mut row = Row::new();
let suite_name = contract.split(':').nth(1).unwrap();
let suite_path = contract.split(':').nth(0).unwrap();

let passed = suite.successes().count();
let mut passed_cell = Cell::new(passed).set_alignment(CellAlignment::Center);

let failed = suite.failures().count();
let mut failed_cell = Cell::new(failed).set_alignment(CellAlignment::Center);

let skipped = suite.skips().count();
let mut skipped_cell = Cell::new(skipped).set_alignment(CellAlignment::Center);

let duration = suite.duration();

row.add_cell(Cell::new(suite_name));

if passed > 0 {
passed_cell = passed_cell.fg(Color::Green);
}
row.add_cell(passed_cell);

if failed > 0 {
failed_cell = failed_cell.fg(Color::Red);
}
row.add_cell(failed_cell);

if skipped > 0 {
skipped_cell = skipped_cell.fg(Color::Yellow);
}
row.add_cell(skipped_cell);

if self.is_detailed {
row.add_cell(Cell::new(suite_path));
row.add_cell(Cell::new(format!("{:.2?}", duration).to_string()));
}

self.table.add_row(row);
}
}
// Print the summary table
println!("\n{}", self.table);
}
}