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}