forge/cmd/test/
summary.rs1use crate::cmd::test::TestOutcome;
2use comfy_table::{
3 Cell, Color, Row, Table,
4 presets::{ASCII_FULL, ASCII_MARKDOWN},
5};
6use foundry_common::shell;
7use foundry_evm::executors::invariant::InvariantMetrics;
8use itertools::Itertools;
9use serde_json::json;
10use std::{collections::HashMap, fmt::Display};
11
12pub struct TestSummaryReport<'a> {
14 is_detailed: bool,
16 outcome: &'a TestOutcome,
18}
19
20impl<'a> TestSummaryReport<'a> {
21 pub const fn new(is_detailed: bool, outcome: &'a TestOutcome) -> Self {
22 Self { is_detailed, outcome }
23 }
24}
25
26impl Display for TestSummaryReport<'_> {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
28 if shell::is_json() {
29 writeln!(f, "{}", self.format_json_output())
30 } else {
31 writeln!(f, "\n{}", self.format_table_output())
32 }
33 }
34}
35
36impl TestSummaryReport<'_> {
37 fn format_json_output(&self) -> String {
38 let results = self
39 .outcome
40 .results
41 .iter()
42 .map(|(contract, suite)| {
43 let (suite_path, suite_name) = contract.split_once(':').unwrap();
44 let mut result = json!({
45 "suite": suite_name,
46 "passed": suite.successes().count(),
47 "failed": suite.failures().count(),
48 "skipped": suite.skips().count(),
49 });
50 if self.is_detailed {
51 result["file_path"] = suite_path.into();
52 result["duration"] = format!("{:.2?}", suite.duration).into();
53 }
54 result
55 })
56 .collect::<Vec<_>>();
57 serde_json::to_string_pretty(&json!({ "results": results })).unwrap()
58 }
59
60 fn format_table_output(&self) -> Table {
61 let mut table = new_table();
62 let mut row = Row::from(vec![
63 Cell::new("Test Suite"),
64 Cell::new("Passed").fg(Color::Green),
65 Cell::new("Failed").fg(Color::Red),
66 Cell::new("Skipped").fg(Color::Yellow),
67 ]);
68 if self.is_detailed {
69 row.add_cell(Cell::new("File Path").fg(Color::Cyan));
70 row.add_cell(Cell::new("Duration").fg(Color::Cyan));
71 }
72 table.set_header(row);
73
74 for (contract, suite) in &self.outcome.results {
75 let (suite_path, suite_name) = contract.split_once(':').unwrap();
76 let count_cell = |count: usize, color| {
77 let cell = Cell::new(count);
78 if count > 0 { cell.fg(color) } else { cell }
79 };
80 let mut row = Row::from(vec![
81 Cell::new(suite_name),
82 count_cell(suite.successes().count(), Color::Green),
83 count_cell(suite.failures().count(), Color::Red),
84 count_cell(suite.skips().count(), Color::Yellow),
85 ]);
86 if self.is_detailed {
87 row.add_cell(Cell::new(suite_path));
88 row.add_cell(Cell::new(format!("{:.2?}", suite.duration)));
89 }
90 table.add_row(row);
91 }
92 table
93 }
94}
95
96fn new_table() -> Table {
98 let mut table = Table::new();
99 if shell::is_markdown() {
100 table.load_style(ASCII_MARKDOWN);
101 } else {
102 table.load_style(ASCII_FULL.with_rounded_corners());
103 }
104 table
105}
106
107pub(crate) fn format_invariant_metrics_table(
121 test_metrics: &HashMap<String, InvariantMetrics>,
122) -> Table {
123 let mut table = new_table();
124 table.set_header(vec![
125 Cell::new("Contract"),
126 Cell::new("Selector"),
127 Cell::new("Calls").fg(Color::Green),
128 Cell::new("Reverts").fg(Color::Red),
129 Cell::new("Discards").fg(Color::Yellow),
130 ]);
131
132 let count_cell =
133 |count: usize, color| Cell::new(count).fg(if count > 0 { color } else { Color::White });
134 for (name, metrics) in test_metrics.iter().sorted_by_key(|(name, _)| *name) {
135 let Some((contract, selector)) =
136 name.split_once(':').map_or(name.as_str(), |(_, contract)| contract).split_once('.')
137 else {
138 continue;
139 };
140 table.add_row(vec![
141 Cell::new(contract),
142 Cell::new(selector),
143 count_cell(metrics.calls, Color::Green),
144 count_cell(metrics.reverts, Color::Red),
145 count_cell(metrics.discards, Color::Yellow),
146 ]);
147 }
148 table
149}
150
151#[cfg(test)]
152mod tests {
153 use crate::cmd::test::summary::format_invariant_metrics_table;
154 use foundry_evm::executors::invariant::InvariantMetrics;
155 use std::collections::HashMap;
156
157 #[test]
158 fn test_invariant_metrics_table() {
159 let mut test_metrics = HashMap::new();
160 test_metrics.insert(
161 "SystemConfig.setGasLimit".to_string(),
162 InvariantMetrics { calls: 10, reverts: 1, discards: 1 },
163 );
164 test_metrics.insert(
165 "src/universal/Proxy.sol:Proxy.changeAdmin".to_string(),
166 InvariantMetrics { calls: 20, reverts: 2, discards: 2 },
167 );
168 let table = format_invariant_metrics_table(&test_metrics);
169 assert_eq!(table.row_count(), 2);
170
171 let mut first_row_content = table.row(0).unwrap().cell_iter();
172 assert_eq!(first_row_content.next().unwrap().content(), "SystemConfig");
173 assert_eq!(first_row_content.next().unwrap().content(), "setGasLimit");
174 assert_eq!(first_row_content.next().unwrap().content(), "10");
175 assert_eq!(first_row_content.next().unwrap().content(), "1");
176 assert_eq!(first_row_content.next().unwrap().content(), "1");
177
178 let mut second_row_content = table.row(1).unwrap().cell_iter();
179 assert_eq!(second_row_content.next().unwrap().content(), "Proxy");
180 assert_eq!(second_row_content.next().unwrap().content(), "changeAdmin");
181 assert_eq!(second_row_content.next().unwrap().content(), "20");
182 assert_eq!(second_row_content.next().unwrap().content(), "2");
183 assert_eq!(second_row_content.next().unwrap().content(), "2");
184 }
185}