phink_lib/cover/
report.rs1use crate::cli::ziggy::ZiggyConfig;
2use fs::read_to_string;
3
4use crate::{
5 cli::config::{
6 PFiles::CoverageTracePath,
7 PhinkFiles,
8 },
9 cover::trace::COV_IDENTIFIER,
10 EmptyResult,
11};
12use anyhow::{
13 bail,
14 Context,
15};
16use std::{
17 collections::HashMap,
18 fs::{
19 self,
20 File,
21 },
22 io,
23 io::Read,
24};
25use walkdir::WalkDir;
26
27pub struct CoverageTracker {
28 coverage: HashMap<String, Vec<bool>>,
31 hit_lines: Vec<usize>,
33}
34
35impl CoverageTracker {
36 pub fn new(coverage_string: &str) -> Self {
38 let hit_lines = coverage_string
39 .split("\n")
40 .filter_map(|s| s.parse().ok())
41 .collect();
42
43 CoverageTracker {
44 coverage: HashMap::new(),
45 hit_lines,
46 }
47 }
48
49 pub fn benchmark(&self, total_feedback: usize) {
51 let mut hit_lines = self.hit_lines.clone();
52 hit_lines.sort();
53 hit_lines.dedup();
54
55 let total_hit_lines = hit_lines.len();
56 let number_of_files = self.coverage.len(); let coverage_percentage = if total_feedback > 0 {
58 total_hit_lines * 100 / total_feedback
59 } else {
60 0 };
62
63 println!("📐 Phink Coverage Benchmark:");
64 println!(" - Total contract's files: {number_of_files}");
65 println!(" - Total of unique hit lines: {total_hit_lines}");
66 println!(" - Maximum theoretically reachable coverage: {total_feedback}"); println!(" - Coverage percentage: {coverage_percentage:.2}%");
68 }
69
70 pub fn process_file(&mut self, file_path: &str) -> io::Result<usize> {
74 let content = read_to_string(file_path)?;
75 let lines: Vec<&str> = content.lines().collect();
76 let mut spotted_debugprint = 0;
77 let mut file_coverage = vec![false; lines.len()];
78
79 for (i, line) in lines.iter().enumerate() {
80 let trimmed = line.trim();
81
82 if let Some(cov_num) = trimmed.strip_prefix("ink::env::debug_println!(\"COV={}\", ") {
83 if let Some(cov_num) = cov_num.strip_suffix(");") {
84 if let Ok(num) = cov_num.parse::<usize>() {
85 spotted_debugprint += 1;
87 if self.hit_lines.contains(&num) {
88 file_coverage[i + 1] = true;
91 }
92 }
93 }
94 }
95 }
96 self.coverage.insert(file_path.to_string(), file_coverage);
97 Ok(spotted_debugprint)
98 }
99
100 pub fn generate_report(&self, output_dir: &str) -> io::Result<()> {
101 fs::create_dir_all(output_dir)?;
102
103 let mut index_html = String::from(
104 "<!DOCTYPE html>
105 <html>
106 <head>
107 <title>Phink Coverage Report</title>
108 <style>
109 body {
110 font-family: Arial, sans-serif;
111 margin: 40px;
112 background-color: #f4f4f9;
113 }
114 h1 {
115 color: #333;
116 }
117 ul {
118 list-style-type: none;
119 padding: 0;
120 }
121 li {
122 margin: 10px 0;
123 }
124 a {
125 text-decoration: none;
126 color: #007bff;
127 }
128 a:hover {
129 text-decoration: underline;
130 }
131 </style>
132 </head>
133 <body>
134 <h1>Phink Coverage Report</h1>
135 <ul>",
136 );
137
138 for (file_path, coverage) in &self.coverage {
139 let sanitized_path = file_path.replace("/", "_").replace("\\", "_");
140 let report_path = format!("{output_dir}/{sanitized_path}.html");
141 self.generate_file_report(file_path, coverage, &report_path)?;
142
143 index_html.push_str(&format!(
144 "<li><a href='{sanitized_path}.html'>- {file_path}</a></li>",
145 ));
146 }
147
148 index_html.push_str("</ul></body></html>");
149 fs::write(format!("{output_dir}/index.html"), index_html)?;
150 println!("📊 Coverage report generated at: {output_dir}");
151
152 Ok(())
153 }
154
155 fn generate_file_report(
156 &self,
157 file_path: &str,
158 coverage: &[bool],
159 output_path: &str,
160 ) -> io::Result<()> {
161 let source_code = read_to_string(file_path)?;
162 let lines: Vec<&str> = source_code.lines().collect();
163
164 let mut html = String::from(
165 "<!DOCTYPE html><html><head><title>Phink File Coverage</title><style>
166 .covered { background-color: #90EE90; }
167 /*.uncovered { background-color: #FFB6C1; }*/
168 </style></head><body>",
169 );
170
171 html.push_str(&format!("<h1>Coverage for {file_path}</h1><pre>"));
172 html.push_str("<h3>This is a beta version of the code visualizer. \
173 <br>You can assume that if a line is green, it has been executed. <br>\
174 If the green line represents a block (e.g., green `if`), it means that the `if` condition was met, and we got inside the condition.<br>\
175 The report doesn't integrate the coverage of the crashing seeds.
176 <br></h3>");
177
178 for (i, line) in lines.iter().enumerate() {
179 let line_class = if coverage[i] { "covered" } else { "uncovered" };
180 html.push_str(&format!(
181 "<span class='{}'>{:4} | {}</span>\n",
182 line_class,
183 i + 1,
184 html_escape(line)
185 ));
186 }
187
188 html.push_str("</pre></body></html>");
189 Self::remove_debug_statement(&mut html);
190 fs::write(output_path, html)?;
191
192 Ok(())
193 }
194
195 pub fn generate(config: ZiggyConfig) -> EmptyResult {
196 let cov_trace_path =
197 PhinkFiles::new(config.to_owned().fuzz_output()).path(CoverageTracePath);
198
199 let mut coverage_trace = match File::open(cov_trace_path) {
200 Ok(file) => file,
201 Err(_) => {
202 bail!("Coverage file not found. Please execute the \"run\" command to create the coverage file.")
203 }
204 };
205
206 let mut contents = String::new();
207 coverage_trace.read_to_string(&mut contents)?;
208 let mut total_feedback = 0;
209 let mut tracker = CoverageTracker::new(&contents);
210 for entry in WalkDir::new(config.contract_path()?)
211 .into_iter()
212 .filter_map(|e| e.ok())
213 .filter(|e| e.path().extension().map_or(false, |ext| ext == "rs"))
214 .filter(|e| !e.path().components().any(|c| c.as_os_str() == "target"))
215 {
216 let entry = entry.path().as_os_str().to_str().unwrap();
217 let one_file_feedback = tracker
218 .process_file(entry)
219 .context(format!("Cannot process {entry:?} file"))?;
220
221 total_feedback += one_file_feedback;
222 }
223
224 tracker
225 .generate_report(
226 config
227 .config()
228 .report_path
229 .clone()
230 .unwrap()
231 .to_str()
232 .unwrap(),
233 )
234 .context("Cannot generate coverage report")?;
235
236 tracker.benchmark(total_feedback);
237
238 Ok(())
239 }
240
241 pub fn remove_debug_statement(html: &mut String) {
242 let lines: Vec<&str> = html.lines().collect();
243
244 let filtered_lines: Vec<&str> = lines
245 .into_iter()
246 .filter(|line| {
247 !(line.contains("ink::env::debug_println!") && line.contains(COV_IDENTIFIER))
248 })
249 .collect();
250
251 *html = filtered_lines.join("\n");
252 }
253}
254
255fn html_escape(s: &str) -> String {
256 s.replace('&', "&")
257 .replace('<', "<")
258 .replace('>', ">")
259 .replace('"', """)
260 .replace('\'', "'")
261}