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