forge/cmd/test/
summary.rs
1use crate::cmd::test::TestOutcome;
2use comfy_table::{modifiers::UTF8_ROUND_CORNERS, Cell, Color, Row, Table};
3use foundry_common::reports::{report_kind, ReportKind};
4use foundry_evm::executors::invariant::InvariantMetrics;
5use itertools::Itertools;
6use serde_json::json;
7use std::{collections::HashMap, fmt::Display};
8
9pub struct TestSummaryReport {
11 report_kind: ReportKind,
13 is_detailed: bool,
15 outcome: TestOutcome,
17}
18
19impl TestSummaryReport {
20 pub fn new(is_detailed: bool, outcome: TestOutcome) -> Self {
21 Self { report_kind: report_kind(), is_detailed, outcome }
22 }
23}
24
25impl Display for TestSummaryReport {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
27 match self.report_kind {
28 ReportKind::Text => {
29 writeln!(f, "\n{}", &self.format_table_output(&self.is_detailed, &self.outcome))?;
30 }
31 ReportKind::JSON => {
32 writeln!(f, "{}", &self.format_json_output(&self.is_detailed, &self.outcome))?;
33 }
34 }
35
36 Ok(())
37 }
38}
39
40impl TestSummaryReport {
41 fn format_json_output(&self, is_detailed: &bool, outcome: &TestOutcome) -> String {
43 let output = json!({
44 "results": outcome.results.iter().map(|(contract, suite)| {
45 let (suite_path, suite_name) = contract.split_once(':').unwrap();
46 let passed = suite.successes().count();
47 let failed = suite.failures().count();
48 let skipped = suite.skips().count();
49 let mut result = json!({
50 "suite": suite_name,
51 "passed": passed,
52 "failed": failed,
53 "skipped": skipped,
54 });
55
56 if *is_detailed {
57 result["file_path"] = serde_json::Value::String(suite_path.to_string());
58 result["duration"] = serde_json::Value::String(format!("{:.2?}", suite.duration));
59 }
60
61 result
62 }).collect::<Vec<serde_json::Value>>(),
63 });
64
65 serde_json::to_string_pretty(&output).unwrap()
66 }
67
68 fn format_table_output(&self, is_detailed: &bool, outcome: &TestOutcome) -> Table {
69 let mut table = Table::new();
70 table.apply_modifier(UTF8_ROUND_CORNERS);
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 table.apply_modifier(UTF8_ROUND_CORNERS);
145
146 table.set_header(vec![
147 Cell::new("Contract"),
148 Cell::new("Selector"),
149 Cell::new("Calls").fg(Color::Green),
150 Cell::new("Reverts").fg(Color::Red),
151 Cell::new("Discards").fg(Color::Yellow),
152 ]);
153
154 for name in test_metrics.keys().sorted() {
155 if let Some((contract, selector)) =
156 name.split_once(':').map_or(name.as_str(), |(_, contract)| contract).split_once('.')
157 {
158 let mut row = Row::new();
159 row.add_cell(Cell::new(contract));
160 row.add_cell(Cell::new(selector));
161
162 if let Some(metrics) = test_metrics.get(name) {
163 let calls_cell = Cell::new(metrics.calls).fg(if metrics.calls > 0 {
164 Color::Green
165 } else {
166 Color::White
167 });
168
169 let reverts_cell = Cell::new(metrics.reverts).fg(if metrics.reverts > 0 {
170 Color::Red
171 } else {
172 Color::White
173 });
174
175 let discards_cell = Cell::new(metrics.discards).fg(if metrics.discards > 0 {
176 Color::Yellow
177 } else {
178 Color::White
179 });
180
181 row.add_cell(calls_cell);
182 row.add_cell(reverts_cell);
183 row.add_cell(discards_cell);
184 }
185
186 table.add_row(row);
187 }
188 }
189 table
190}
191
192#[cfg(test)]
193mod tests {
194 use crate::cmd::test::summary::format_invariant_metrics_table;
195 use foundry_evm::executors::invariant::InvariantMetrics;
196 use std::collections::HashMap;
197
198 #[test]
199 fn test_invariant_metrics_table() {
200 let mut test_metrics = HashMap::new();
201 test_metrics.insert(
202 "SystemConfig.setGasLimit".to_string(),
203 InvariantMetrics { calls: 10, reverts: 1, discards: 1 },
204 );
205 test_metrics.insert(
206 "src/universal/Proxy.sol:Proxy.changeAdmin".to_string(),
207 InvariantMetrics { calls: 20, reverts: 2, discards: 2 },
208 );
209 let table = format_invariant_metrics_table(&test_metrics);
210 assert_eq!(table.row_count(), 2);
211
212 let mut first_row_content = table.row(0).unwrap().cell_iter();
213 assert_eq!(first_row_content.next().unwrap().content(), "SystemConfig");
214 assert_eq!(first_row_content.next().unwrap().content(), "setGasLimit");
215 assert_eq!(first_row_content.next().unwrap().content(), "10");
216 assert_eq!(first_row_content.next().unwrap().content(), "1");
217 assert_eq!(first_row_content.next().unwrap().content(), "1");
218
219 let mut second_row_content = table.row(1).unwrap().cell_iter();
220 assert_eq!(second_row_content.next().unwrap().content(), "Proxy");
221 assert_eq!(second_row_content.next().unwrap().content(), "changeAdmin");
222 assert_eq!(second_row_content.next().unwrap().content(), "20");
223 assert_eq!(second_row_content.next().unwrap().content(), "2");
224 assert_eq!(second_row_content.next().unwrap().content(), "2");
225 }
226}