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
9/// Represents a test summary report.
10pub struct TestSummaryReport {
11    /// The kind of report to generate.
12    report_kind: ReportKind,
13    /// Whether the report should be detailed.
14    is_detailed: bool,
15    /// The test outcome to report.
16    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    // Helper function to format the JSON output.
42    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        // Traverse the test_results vector and build the table
85        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
127/// Helper function to create the invariant metrics table.
128///
129/// ╭-----------------------+----------------+-------+---------+----------╮
130/// | Contract              | Selector       | Calls | Reverts | Discards |
131/// +=====================================================================+
132/// | AnotherCounterHandler | doWork         | 7451  | 123     | 4941     |
133/// |-----------------------+----------------+-------+---------+----------|
134/// | AnotherCounterHandler | doWorkThing    | 7279  | 137     | 4849     |
135/// |-----------------------+----------------+-------+---------+----------|
136/// | CounterHandler        | doAnotherThing | 7302  | 150     | 4794     |
137/// |-----------------------+----------------+-------+---------+----------|
138/// | CounterHandler        | doSomething    | 7382  | 160     |4794      |
139/// ╰-----------------------+----------------+-------+---------+----------╯
140pub(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}