forge/cmd/test/
summary.rs1use crate::cmd::test::TestOutcome;
2use comfy_table::{
3 Cell, Color, Row, Table, modifiers::UTF8_ROUND_CORNERS, presets::ASCII_MARKDOWN,
4};
5use foundry_common::shell;
6use foundry_evm::executors::invariant::InvariantMetrics;
7use itertools::Itertools;
8use serde_json::json;
9use std::{collections::HashMap, fmt::Display};
10
11pub struct TestSummaryReport {
13 is_detailed: bool,
15 outcome: TestOutcome,
17}
18
19impl TestSummaryReport {
20 pub fn new(is_detailed: bool, outcome: TestOutcome) -> Self {
21 Self { is_detailed, outcome }
22 }
23}
24
25impl Display for TestSummaryReport {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
27 if shell::is_json() {
28 writeln!(f, "{}", &self.format_json_output(&self.is_detailed, &self.outcome))?;
29 } else {
30 writeln!(f, "\n{}", &self.format_table_output(&self.is_detailed, &self.outcome))?;
31 }
32 Ok(())
33 }
34}
35
36impl TestSummaryReport {
37 fn format_json_output(&self, is_detailed: &bool, outcome: &TestOutcome) -> String {
39 let output = json!({
40 "results": outcome.results.iter().map(|(contract, suite)| {
41 let (suite_path, suite_name) = contract.split_once(':').unwrap();
42 let passed = suite.successes().count();
43 let failed = suite.failures().count();
44 let skipped = suite.skips().count();
45 let mut result = json!({
46 "suite": suite_name,
47 "passed": passed,
48 "failed": failed,
49 "skipped": skipped,
50 });
51
52 if *is_detailed {
53 result["file_path"] = serde_json::Value::String(suite_path.to_string());
54 result["duration"] = serde_json::Value::String(format!("{:.2?}", suite.duration));
55 }
56
57 result
58 }).collect::<Vec<serde_json::Value>>(),
59 });
60
61 serde_json::to_string_pretty(&output).unwrap()
62 }
63
64 fn format_table_output(&self, is_detailed: &bool, outcome: &TestOutcome) -> Table {
65 let mut table = Table::new();
66 if shell::is_markdown() {
67 table.load_preset(ASCII_MARKDOWN);
68 } else {
69 table.apply_modifier(UTF8_ROUND_CORNERS);
70 }
71
72 let mut row = Row::from(vec![
73 Cell::new("Test Suite"),
74 Cell::new("Passed").fg(Color::Green),
75 Cell::new("Failed").fg(Color::Red),
76 Cell::new("Skipped").fg(Color::Yellow),
77 ]);
78 if *is_detailed {
79 row.add_cell(Cell::new("File Path").fg(Color::Cyan));
80 row.add_cell(Cell::new("Duration").fg(Color::Cyan));
81 }
82 table.set_header(row);
83
84 for (contract, suite) in &outcome.results {
86 let mut row = Row::new();
87 let (suite_path, suite_name) = contract.split_once(':').unwrap();
88
89 let passed = suite.successes().count();
90 let mut passed_cell = Cell::new(passed);
91
92 let failed = suite.failures().count();
93 let mut failed_cell = Cell::new(failed);
94
95 let skipped = suite.skips().count();
96 let mut skipped_cell = Cell::new(skipped);
97
98 row.add_cell(Cell::new(suite_name));
99
100 if passed > 0 {
101 passed_cell = passed_cell.fg(Color::Green);
102 }
103 row.add_cell(passed_cell);
104
105 if failed > 0 {
106 failed_cell = failed_cell.fg(Color::Red);
107 }
108 row.add_cell(failed_cell);
109
110 if skipped > 0 {
111 skipped_cell = skipped_cell.fg(Color::Yellow);
112 }
113 row.add_cell(skipped_cell);
114
115 if self.is_detailed {
116 row.add_cell(Cell::new(suite_path));
117 row.add_cell(Cell::new(format!("{:.2?}", suite.duration).to_string()));
118 }
119
120 table.add_row(row);
121 }
122
123 table
124 }
125}
126
127pub(crate) fn format_invariant_metrics_table(
141 test_metrics: &HashMap<String, InvariantMetrics>,
142) -> Table {
143 let mut table = Table::new();
144 if shell::is_markdown() {
145 table.load_preset(ASCII_MARKDOWN);
146 } else {
147 table.apply_modifier(UTF8_ROUND_CORNERS);
148 }
149
150 table.set_header(vec![
151 Cell::new("Contract"),
152 Cell::new("Selector"),
153 Cell::new("Calls").fg(Color::Green),
154 Cell::new("Reverts").fg(Color::Red),
155 Cell::new("Discards").fg(Color::Yellow),
156 ]);
157
158 for name in test_metrics.keys().sorted() {
159 if let Some((contract, selector)) =
160 name.split_once(':').map_or(name.as_str(), |(_, contract)| contract).split_once('.')
161 {
162 let mut row = Row::new();
163 row.add_cell(Cell::new(contract));
164 row.add_cell(Cell::new(selector));
165
166 if let Some(metrics) = test_metrics.get(name) {
167 let calls_cell = Cell::new(metrics.calls).fg(if metrics.calls > 0 {
168 Color::Green
169 } else {
170 Color::White
171 });
172
173 let reverts_cell = Cell::new(metrics.reverts).fg(if metrics.reverts > 0 {
174 Color::Red
175 } else {
176 Color::White
177 });
178
179 let discards_cell = Cell::new(metrics.discards).fg(if metrics.discards > 0 {
180 Color::Yellow
181 } else {
182 Color::White
183 });
184
185 row.add_cell(calls_cell);
186 row.add_cell(reverts_cell);
187 row.add_cell(discards_cell);
188 }
189
190 table.add_row(row);
191 }
192 }
193 table
194}
195
196#[cfg(test)]
197mod tests {
198 use crate::cmd::test::summary::format_invariant_metrics_table;
199 use foundry_evm::executors::invariant::InvariantMetrics;
200 use std::collections::HashMap;
201
202 #[test]
203 fn test_invariant_metrics_table() {
204 let mut test_metrics = HashMap::new();
205 test_metrics.insert(
206 "SystemConfig.setGasLimit".to_string(),
207 InvariantMetrics { calls: 10, reverts: 1, discards: 1 },
208 );
209 test_metrics.insert(
210 "src/universal/Proxy.sol:Proxy.changeAdmin".to_string(),
211 InvariantMetrics { calls: 20, reverts: 2, discards: 2 },
212 );
213 let table = format_invariant_metrics_table(&test_metrics);
214 assert_eq!(table.row_count(), 2);
215
216 let mut first_row_content = table.row(0).unwrap().cell_iter();
217 assert_eq!(first_row_content.next().unwrap().content(), "SystemConfig");
218 assert_eq!(first_row_content.next().unwrap().content(), "setGasLimit");
219 assert_eq!(first_row_content.next().unwrap().content(), "10");
220 assert_eq!(first_row_content.next().unwrap().content(), "1");
221 assert_eq!(first_row_content.next().unwrap().content(), "1");
222
223 let mut second_row_content = table.row(1).unwrap().cell_iter();
224 assert_eq!(second_row_content.next().unwrap().content(), "Proxy");
225 assert_eq!(second_row_content.next().unwrap().content(), "changeAdmin");
226 assert_eq!(second_row_content.next().unwrap().content(), "20");
227 assert_eq!(second_row_content.next().unwrap().content(), "2");
228 assert_eq!(second_row_content.next().unwrap().content(), "2");
229 }
230}