fe_test_runner/
lib.rs

1use colored::Colorize;
2use ethabi::{Event, Hash, RawLog};
3use indexmap::IndexMap;
4use revm::primitives::{
5    AccountInfo, Address, Bytecode, Bytes, Env, ExecutionResult, TransactTo, B256, U256,
6};
7use std::{fmt::Display, str::FromStr};
8
9pub use ethabi;
10
11#[derive(Debug)]
12pub struct TestSink {
13    success_count: usize,
14    failure_details: Vec<String>,
15    logs_details: Vec<String>,
16    collect_logs: bool,
17}
18
19impl TestSink {
20    pub fn new(collect_logs: bool) -> Self {
21        Self {
22            success_count: 0,
23            failure_details: vec![],
24            logs_details: vec![],
25            collect_logs,
26        }
27    }
28
29    pub fn test_count(&self) -> usize {
30        self.failure_count() + self.success_count()
31    }
32
33    pub fn failure_count(&self) -> usize {
34        self.failure_details.len()
35    }
36
37    pub fn logs_count(&self) -> usize {
38        self.logs_details.len()
39    }
40
41    pub fn success_count(&self) -> usize {
42        self.success_count
43    }
44
45    pub fn insert_failure(&mut self, name: &str, reason: &str) {
46        self.failure_details
47            .push(format!("{}\n{}", name, reason.red()))
48    }
49
50    pub fn insert_logs(&mut self, name: &str, logs: &str) {
51        if self.collect_logs {
52            self.logs_details.push(format!(
53                "{} produced the following logs:\n{}\n",
54                name,
55                logs.bright_yellow()
56            ))
57        }
58    }
59
60    pub fn inc_success_count(&mut self) {
61        self.success_count += 1
62    }
63
64    pub fn failure_details(&self) -> String {
65        self.failure_details.join("\n")
66    }
67
68    pub fn logs_details(&self) -> String {
69        self.logs_details.join("\n")
70    }
71}
72
73impl Display for TestSink {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        if self.logs_count() != 0 {
76            writeln!(f, "{}", self.logs_details())?;
77            writeln!(f)?;
78        }
79
80        if self.failure_count() != 0 {
81            writeln!(f, "{}", self.failure_details())?;
82            writeln!(f)?;
83            if self.collect_logs {
84                writeln!(f, "note: failed tests do not produce logs")?;
85                writeln!(f)?;
86            }
87        }
88
89        let test_description = |n: usize, status: &dyn Display| -> String {
90            if n == 1 {
91                format!("1 test {status}")
92            } else {
93                format!("{n} tests {status}")
94            }
95        };
96
97        write!(
98            f,
99            "{}; ",
100            test_description(self.success_count(), &"passed".green())
101        )?;
102        write!(
103            f,
104            "{}; ",
105            test_description(self.failure_count(), &"failed".red())
106        )?;
107        write!(f, "{}", test_description(self.test_count(), &"executed"))
108    }
109}
110
111pub fn execute(name: &str, events: &[Event], bytecode: &str, sink: &mut TestSink) -> bool {
112    let events: IndexMap<_, _> = events
113        .iter()
114        .map(|event| (event.signature(), event))
115        .collect();
116    let bytecode = Bytecode::new_raw(Bytes::copy_from_slice(&hex::decode(bytecode).unwrap()));
117
118    let mut database = revm::InMemoryDB::default();
119    let test_address = Address::from_str("0000000000000000000000000000000000000042").unwrap();
120    let test_info = AccountInfo::new(U256::ZERO, 0, B256::default(), bytecode);
121    database.insert_account_info(test_address, test_info);
122
123    let mut env = Env::default();
124    env.tx.transact_to = TransactTo::Call(test_address);
125
126    let builder = revm::EvmBuilder::default()
127        .with_db(database)
128        .with_env(Box::new(env));
129    let mut evm = builder.build();
130    let result = evm.transact_commit().expect("evm failure");
131
132    if let ExecutionResult::Success { logs, .. } = result {
133        let logs: Vec<_> = logs
134            .iter()
135            .map(|log| {
136                if let Some(Some(event)) = log
137                    .topics()
138                    .first()
139                    .map(|sig| events.get(&Hash::from_slice(sig.as_slice())))
140                {
141                    let topics = log
142                        .topics()
143                        .iter()
144                        .map(|topic| Hash::from_slice(topic.as_slice()))
145                        .collect();
146                    let data = log.data.data.clone().to_vec();
147                    let raw_log = RawLog { topics, data };
148                    if let Ok(parsed_event) = event.parse_log(raw_log) {
149                        format!(
150                            "  {} emitted by {} with the following parameters [{}]",
151                            event.name,
152                            log.address,
153                            parsed_event
154                                .params
155                                .iter()
156                                .map(|param| format!("{}: {}", param.name, param.value))
157                                .collect::<Vec<String>>()
158                                .join(", "),
159                        )
160                    } else {
161                        format!("  {:?}", log)
162                    }
163                } else {
164                    format!("  {:?}", log)
165                }
166            })
167            .collect();
168
169        if !logs.is_empty() {
170            sink.insert_logs(name, &logs.join("\n"))
171        }
172
173        sink.inc_success_count();
174        true
175    } else if let ExecutionResult::Revert { output, .. } = result {
176        sink.insert_failure(
177            name,
178            &if output.is_empty() {
179                "  reverted".to_string()
180            } else {
181                format!(
182                    "  reverted with the following output: {}",
183                    hex::encode(output)
184                )
185            },
186        );
187        false
188    } else {
189        panic!("test halted")
190    }
191}