1use alloy_primitives::map::{HashMap, HashSet};
4use comfy_table::{modifiers::UTF8_ROUND_CORNERS, Attribute, Cell, Color, Row, Table};
5use evm_disassembler::disassemble_bytes;
6use foundry_common::fs;
7use semver::Version;
8use std::{
9 collections::hash_map,
10 io::Write,
11 path::{Path, PathBuf},
12};
13
14pub use foundry_evm::coverage::*;
15
16pub trait CoverageReporter {
18 fn needs_source_maps(&self) -> bool {
20 false
21 }
22
23 fn report(&mut self, report: &CoverageReport) -> eyre::Result<()>;
25}
26
27pub struct CoverageSummaryReporter {
29 table: Table,
31 total: CoverageSummary,
33}
34
35impl Default for CoverageSummaryReporter {
36 fn default() -> Self {
37 let mut table = Table::new();
38 table.apply_modifier(UTF8_ROUND_CORNERS);
39
40 table.set_header(vec![
41 Cell::new("File"),
42 Cell::new("% Lines"),
43 Cell::new("% Statements"),
44 Cell::new("% Branches"),
45 Cell::new("% Funcs"),
46 ]);
47
48 Self { table, total: CoverageSummary::default() }
49 }
50}
51
52impl CoverageSummaryReporter {
53 fn add_row(&mut self, name: impl Into<Cell>, summary: CoverageSummary) {
54 let mut row = Row::new();
55 row.add_cell(name.into())
56 .add_cell(format_cell(summary.line_hits, summary.line_count))
57 .add_cell(format_cell(summary.statement_hits, summary.statement_count))
58 .add_cell(format_cell(summary.branch_hits, summary.branch_count))
59 .add_cell(format_cell(summary.function_hits, summary.function_count));
60 self.table.add_row(row);
61 }
62}
63
64impl CoverageReporter for CoverageSummaryReporter {
65 fn report(&mut self, report: &CoverageReport) -> eyre::Result<()> {
66 for (path, summary) in report.summary_by_file() {
67 self.total.merge(&summary);
68 self.add_row(path.display(), summary);
69 }
70
71 self.add_row("Total", self.total.clone());
72 sh_println!("\n{}", self.table)?;
73 Ok(())
74 }
75}
76
77fn format_cell(hits: usize, total: usize) -> Cell {
78 let percentage = if total == 0 { 1. } else { hits as f64 / total as f64 };
79
80 let mut cell =
81 Cell::new(format!("{:.2}% ({hits}/{total})", percentage * 100.)).fg(match percentage {
82 _ if total == 0 => Color::Grey,
83 _ if percentage < 0.5 => Color::Red,
84 _ if percentage < 0.75 => Color::Yellow,
85 _ => Color::Green,
86 });
87
88 if total == 0 {
89 cell = cell.add_attribute(Attribute::Dim);
90 }
91 cell
92}
93
94pub struct LcovReporter {
99 path: PathBuf,
100 version: Version,
101}
102
103impl LcovReporter {
104 pub fn new(path: PathBuf, version: Version) -> Self {
106 Self { path, version }
107 }
108}
109
110impl CoverageReporter for LcovReporter {
111 fn report(&mut self, report: &CoverageReport) -> eyre::Result<()> {
112 let mut out = std::io::BufWriter::new(fs::create_file(&self.path)?);
113
114 let mut fn_index = 0usize;
115 for (path, items) in report.items_by_file() {
116 let summary = CoverageSummary::from_items(items.iter().copied());
117
118 writeln!(out, "TN:")?;
119 writeln!(out, "SF:{}", path.display())?;
120
121 let mut recorded_lines = HashSet::new();
122
123 for item in items {
124 let line = item.loc.lines.start;
125 let end_line = item.loc.lines.end - 1;
127 let hits = item.hits;
128 match item.kind {
129 CoverageItemKind::Function { ref name } => {
130 let name = format!("{}.{name}", item.loc.contract_name);
131 if self.version >= Version::new(2, 2, 0) {
132 writeln!(out, "FNL:{fn_index},{line},{end_line}")?;
134 writeln!(out, "FNA:{fn_index},{hits},{name}")?;
135 fn_index += 1;
136 } else if self.version >= Version::new(2, 0, 0) {
137 writeln!(out, "FN:{line},{end_line},{name}")?;
139 writeln!(out, "FNDA:{hits},{name}")?;
140 } else {
141 writeln!(out, "FN:{line},{name}")?;
142 writeln!(out, "FNDA:{hits},{name}")?;
143 }
144 }
145 CoverageItemKind::Line | CoverageItemKind::Statement => {
147 if recorded_lines.insert(line) {
148 writeln!(out, "DA:{line},{hits}")?;
149 }
150 }
151 CoverageItemKind::Branch { branch_id, path_id, .. } => {
152 writeln!(
153 out,
154 "BRDA:{line},{branch_id},{path_id},{}",
155 if hits == 0 { "-".to_string() } else { hits.to_string() }
156 )?;
157 }
158 }
159 }
160
161 writeln!(out, "FNF:{}", summary.function_count)?;
163 writeln!(out, "FNH:{}", summary.function_hits)?;
164
165 writeln!(out, "LF:{}", summary.line_count)?;
167 writeln!(out, "LH:{}", summary.line_hits)?;
168
169 writeln!(out, "BRF:{}", summary.branch_count)?;
171 writeln!(out, "BRH:{}", summary.branch_hits)?;
172
173 writeln!(out, "end_of_record")?;
174 }
175
176 out.flush()?;
177 sh_println!("Wrote LCOV report.")?;
178
179 Ok(())
180 }
181}
182
183pub struct DebugReporter;
185
186impl CoverageReporter for DebugReporter {
187 fn report(&mut self, report: &CoverageReport) -> eyre::Result<()> {
188 for (path, items) in report.items_by_file() {
189 sh_println!("Uncovered for {}:", path.display())?;
190 for item in items {
191 if item.hits == 0 {
192 sh_println!("- {item}")?;
193 }
194 }
195 sh_println!()?;
196 }
197
198 for (contract_id, anchors) in &report.anchors {
199 sh_println!("Anchors for {contract_id}:")?;
200 let anchors = anchors
201 .0
202 .iter()
203 .map(|anchor| (false, anchor))
204 .chain(anchors.1.iter().map(|anchor| (true, anchor)));
205 for (is_deployed, anchor) in anchors {
206 sh_println!("- {anchor}")?;
207 if is_deployed {
208 sh_println!("- Creation code")?;
209 } else {
210 sh_println!("- Runtime code")?;
211 }
212 sh_println!(
213 " - Refers to item: {}",
214 report
215 .analyses
216 .get(&contract_id.version)
217 .and_then(|items| items.get(anchor.item_id))
218 .map_or_else(|| "None".to_owned(), |item| item.to_string())
219 )?;
220 }
221 sh_println!()?;
222 }
223
224 Ok(())
225 }
226}
227
228pub struct BytecodeReporter {
229 root: PathBuf,
230 destdir: PathBuf,
231}
232
233impl BytecodeReporter {
234 pub fn new(root: PathBuf, destdir: PathBuf) -> Self {
235 Self { root, destdir }
236 }
237}
238
239impl CoverageReporter for BytecodeReporter {
240 fn needs_source_maps(&self) -> bool {
241 true
242 }
243
244 fn report(&mut self, report: &CoverageReport) -> eyre::Result<()> {
245 use std::fmt::Write;
246
247 fs::create_dir_all(&self.destdir)?;
248
249 let no_source_elements = Vec::new();
250 let mut line_number_cache = LineNumberCache::new(self.root.clone());
251
252 for (contract_id, hits) in &report.bytecode_hits {
253 let ops = disassemble_bytes(hits.bytecode().to_vec())?;
254 let mut formatted = String::new();
255
256 let source_elements =
257 report.source_maps.get(contract_id).map(|sm| &sm.1).unwrap_or(&no_source_elements);
258
259 for (code, source_element) in std::iter::zip(ops.iter(), source_elements) {
260 let hits = hits
261 .get(code.offset)
262 .map(|h| format!("[{h:03}]"))
263 .unwrap_or(" ".to_owned());
264 let source_id = source_element.index();
265 let source_path = source_id.and_then(|i| {
266 report.source_paths.get(&(contract_id.version.clone(), i as usize))
267 });
268
269 let code = format!("{code:?}");
270 let start = source_element.offset() as usize;
271 let end = (source_element.offset() + source_element.length()) as usize;
272
273 if let Some(source_path) = source_path {
274 let (sline, spos) = line_number_cache.get_position(source_path, start)?;
275 let (eline, epos) = line_number_cache.get_position(source_path, end)?;
276 writeln!(
277 formatted,
278 "{} {:40} // {}: {}:{}-{}:{} ({}-{})",
279 hits,
280 code,
281 source_path.display(),
282 sline,
283 spos,
284 eline,
285 epos,
286 start,
287 end
288 )?;
289 } else if let Some(source_id) = source_id {
290 writeln!(formatted, "{hits} {code:40} // SRCID{source_id}: ({start}-{end})")?;
291 } else {
292 writeln!(formatted, "{hits} {code:40}")?;
293 }
294 }
295 fs::write(
296 self.destdir.join(&*contract_id.contract_name).with_extension("asm"),
297 formatted,
298 )?;
299 }
300
301 Ok(())
302 }
303}
304
305struct LineNumberCache {
307 root: PathBuf,
308 line_offsets: HashMap<PathBuf, Vec<usize>>,
309}
310
311impl LineNumberCache {
312 pub fn new(root: PathBuf) -> Self {
313 Self { root, line_offsets: HashMap::default() }
314 }
315
316 pub fn get_position(&mut self, path: &Path, offset: usize) -> eyre::Result<(usize, usize)> {
317 let line_offsets = match self.line_offsets.entry(path.to_path_buf()) {
318 hash_map::Entry::Occupied(o) => o.into_mut(),
319 hash_map::Entry::Vacant(v) => {
320 let text = fs::read_to_string(self.root.join(path))?;
321 let mut line_offsets = vec![0];
322 for line in text.lines() {
323 let line_offset = line.as_ptr() as usize - text.as_ptr() as usize;
324 line_offsets.push(line_offset);
325 }
326 v.insert(line_offsets)
327 }
328 };
329 let lo = match line_offsets.binary_search(&offset) {
330 Ok(lo) => lo,
331 Err(lo) => lo - 1,
332 };
333 let pos = offset - line_offsets.get(lo).unwrap() + 1;
334 Ok((lo, pos))
335 }
336}