Skip to main content

huff_neo_test_runner/
report.rs

1use crate::prelude::{HuffFunction, Outcome, ReportKind, TestResult, TestStatus, decode_huff_internal_calls};
2use alloy_primitives::Address;
3use comfy_table::{Attribute, Cell, Color, ContentArrangement, Row, Table, modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL};
4use foundry_evm::decode::decode_console_logs;
5use foundry_evm::traces::{
6    CallTraceDecoderBuilder, SparsedTraceArena, decode_trace_arena, identifier::SignaturesIdentifier, render_trace_arena_inner,
7};
8use std::time::Instant;
9use yansi::Paint;
10
11/// Print a report of the test results, formatted according to the `report_kind` parameter.
12///
13/// The verbosity follows forge: call traces at 3 for failing tests and at 4 for every test,
14/// with storage changes at 5.
15pub async fn print_test_report(results: Vec<TestResult>, report_kind: ReportKind, verbosity: u8, start: Instant) -> eyre::Result<()> {
16    // Gather how many of our tests passed *before* generating our report,
17    // as we pass ownership of `results` to both the `ReportKind::Table`
18    // and `ReportKind::List` arms.
19    let n_passed = results.iter().filter(|r| r.status == TestStatus::Success).count();
20    let n_skipped = results.iter().filter(|r| r.status == TestStatus::Skipped).count();
21    let n_failed = results.len() - n_passed - n_skipped;
22
23    // Generate and print a report of the test results, formatted based on
24    // the `report_kind` input.
25    match report_kind {
26        ReportKind::Table => {
27            let mut table = Table::new();
28            table.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS);
29            table.set_header(Row::from(vec![
30                Cell::new("Name").fg(Color::Magenta),
31                Cell::new("Return Data").fg(Color::Yellow),
32                Cell::new("Gas").fg(Color::Cyan),
33                Cell::new("Status").fg(Color::Blue),
34            ]));
35            table.set_content_arrangement(ContentArrangement::DynamicFullWidth);
36            table.set_width(120);
37
38            for result in results {
39                table.add_row(Row::from(vec![
40                    Cell::new(result.name).add_attribute(Attribute::Bold).fg(Color::Cyan),
41                    Cell::new(result.return_data.unwrap_or_else(|| String::from("None"))),
42                    Cell::new(result.gas.to_string()),
43                    Cell::from(result.status),
44                ]));
45            }
46
47            println!("{table}");
48        }
49        ReportKind::List => {
50            for result in results {
51                println!(
52                    "[{0}] {1: <15} - {2} {3: <20}",
53                    String::from(result.status.clone()),
54                    result.name,
55                    Paint::yellow("Gas used:"),
56                    result.gas
57                );
58                if let Outcome::Skip { reason: Some(reason) } = &result.outcome {
59                    println!("{}", Paint::yellow(&format!("├─ skipped: {reason}")));
60                }
61                for failure in &result.failures {
62                    println!("{}", Paint::red(&format!("├─ {}: {}", failure.expectation, failure.message)));
63                }
64
65                let num_logs = result.logs.as_ref().map_or(0, Vec::len);
66
67                if let Some(return_data) = result.return_data.clone() {
68                    println!("├─ {}", Paint::cyan("RETURN DATA"));
69                    println!("{} {return_data}", if num_logs == 0 { "└─" } else { "├─" });
70                }
71
72                if let Some(logs) = &result.logs {
73                    let filtered_logs = decode_console_logs(logs);
74                    if !filtered_logs.is_empty() {
75                        println!("├─ {}", Paint::cyan("CONSOLE LOGS"));
76                        for (i, log) in filtered_logs.iter().enumerate() {
77                            println!("{} {log}", if filtered_logs.len() - 1 == i { "└─" } else { "├─" });
78                        }
79                    }
80                }
81
82                let show_traces = verbosity >= 4 || (verbosity == 3 && result.status == TestStatus::Revert);
83                if show_traces && let Some(traces) = result.traces {
84                    print_call_trace(traces, result.address, &result.functions, verbosity >= 5).await?;
85                }
86            }
87        }
88        ReportKind::Json => {
89            if let Ok(o) = serde_json::to_string_pretty(&results) {
90                println!("{o}");
91            } else {
92                eprintln!("Error serializing test results into JSON.");
93            }
94            return Ok(());
95        }
96    }
97    let skipped = if n_skipped > 0 { format!(", {} skipped", Paint::yellow(&n_skipped)) } else { String::new() };
98    // The share of the tests that ran, which leaves out the skipped ones
99    let share = (n_passed * 100).checked_div(n_passed + n_failed).map(|share| format!(" ({}%)", Paint::yellow(&share))).unwrap_or_default();
100    println!(
101        "➜ {} tests passed, {} tests failed{skipped}{share}. ⏱ : {}",
102        Paint::green(&n_passed),
103        Paint::red(&n_failed),
104        Paint::magenta(&format!("{:.4?}", start.elapsed()))
105    );
106
107    Ok(())
108}
109
110/// Print the call trace for a given test result.
111async fn print_call_trace(
112    mut arena: SparsedTraceArena,
113    address: Option<Address>,
114    functions: &[HuffFunction],
115    with_storage_changes: bool,
116) -> eyre::Result<()> {
117    // Create a signature identifier to decode unknown function selectors and events
118    let identifier = SignaturesIdentifier::new(false)?;
119
120    // Build a decoder with the signature identifier
121    let decoder = CallTraceDecoderBuilder::new().with_signature_identifier(identifier).build();
122
123    // Decode and render the trace
124    decode_trace_arena(&mut arena, &decoder).await;
125    if let Some(address) = address {
126        decode_huff_internal_calls(&mut arena, address, functions);
127    }
128
129    println!("├─ {}", Paint::cyan("TRACES"));
130    println!("{}", render_trace_arena_inner(&arena, false, with_storage_changes));
131
132    Ok(())
133}