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