1use std::collections::HashMap;
2
3use anyhow::Result;
4
5use crate::parsers::*;
6
7pub struct Lcov {
8 name: std::path::PathBuf,
9 files: Vec<LcovFile>,
10}
11
12pub struct LcovSummary {
13 total_lines: usize,
14 total_lines_hit: usize,
15 total_functions: usize,
16 total_functions_hit: usize,
17}
18
19impl LcovSummary {
20 fn lines_percentage(&self) -> f64 {
21 self.total_lines_hit as f64 / self.total_lines as f64 * 100.
22 }
23
24 fn functions_percentage(&self) -> f64 {
25 self.total_functions_hit as f64 / self.total_functions as f64 * 100.
26 }
27}
28
29impl Lcov {
30 pub fn parse(name: std::path::PathBuf) -> Result<Self> {
32 let source = std::fs::read_to_string(&name)?;
33 let mut files = vec![];
34 for line in source.lines() {
35 if line.starts_with("SF:") {
36 let (_, source) = source_file_path(line).unwrap();
37 files.push(LcovFile::new(&source));
38 continue;
39 }
40
41 if line.starts_with("FN:") {
42 let (_, (_, name)) = function_name(line).unwrap();
43 if let Some(file) = files.last_mut() {
44 file.function_hits.insert(name.to_string(), 0);
45 }
46 continue;
47 }
48
49 if line.starts_with("FNDA:") {
50 let (_, (hits, name)) = function_hit_count(line).unwrap();
51 if let Some(file) = files.last_mut() {
52 *file.function_hits.get_mut(name).unwrap() = hits;
53 }
54 continue;
55 }
56
57 if line.starts_with("FNF:") {
58 let (_, found) = functions_found(line).unwrap();
59 if let Some(file) = files.last_mut() {
60 file.functions_found = found;
61 }
62 continue;
63 }
64
65 if line.starts_with("FNH:") {
66 let (_, hit) = functions_hit(line).unwrap();
67 if let Some(file) = files.last_mut() {
68 file.functions_hit = hit;
69 }
70 continue;
71 }
72
73 if line.starts_with("LF:") {
74 let (_, found) = lines_found(line).unwrap();
75 if let Some(file) = files.last_mut() {
76 file.lines_found = found;
77 }
78 continue;
79 }
80
81 if line.starts_with("LH:") {
82 let (_, hit) = lines_hit(line).unwrap();
83 if let Some(file) = files.last_mut() {
84 file.lines_hit = hit;
85 }
86 continue;
87 }
88
89 if line.starts_with("BF:") {
90 let (_, found) = branches_found(line).unwrap();
91 if let Some(file) = files.last_mut() {
92 file.branches_found = found;
93 }
94 continue;
95 }
96
97 if line.starts_with("BH:") {
98 let (_, hit) = branches_hit(line).unwrap();
99 if let Some(file) = files.last_mut() {
100 file.branches_hit = hit;
101 }
102 continue;
103 }
104 }
105
106 Ok(Self { name, files })
107 }
108
109 pub fn files(&self) -> &[LcovFile] {
111 &self.files
112 }
113
114 pub fn files_mut(&mut self) -> &mut [LcovFile] {
116 &mut self.files
117 }
118
119 pub fn diffstd(&self, other: &Self) {
120 todo!();
121 }
122
123 pub fn diffsummarystd(&self, other: &Self) {
125 use prettytable::{format::consts::FORMAT_CLEAN, format::Alignment, Cell, Row, Table};
126
127 let summary = self.summary();
128 let summary_other = other.summary();
129
130 let lines_diff = summary_other.lines_percentage() - summary.lines_percentage();
131 let functions_diff = summary_other.functions_percentage() - summary.functions_percentage();
132
133 let mut table = Table::new();
134 table.set_format(*FORMAT_CLEAN);
135 table.set_titles(Self::title_row());
136 table.add_row(Self::sub_title_row());
137
138 table.add_row(Row::new(vec![
139 Cell::new_align(&self.name.to_string_lossy(), Alignment::RIGHT),
140 Cell::new("│"),
141 Cell::new_align(&summary.total_lines_hit.to_string(), Alignment::RIGHT),
142 Cell::new_align(&summary.total_lines.to_string(), Alignment::RIGHT),
143 Cell::new_align(
144 &Self::color_percentage(summary.lines_percentage(), 70., 80.),
145 Alignment::RIGHT,
146 ),
147 Cell::new("│"),
148 Cell::new_align(&summary.total_functions_hit.to_string(), Alignment::RIGHT),
149 Cell::new_align(&summary.total_functions.to_string(), Alignment::RIGHT),
150 Cell::new_align(
151 &Self::color_percentage(summary.functions_percentage(), 70., 80.),
152 Alignment::RIGHT,
153 ),
154 ]));
155
156 table.add_row(Row::new(vec![
157 Cell::new_align(&other.name.to_string_lossy(), Alignment::RIGHT),
158 Cell::new("│"),
159 Cell::new_align(&summary_other.total_lines_hit.to_string(), Alignment::RIGHT),
160 Cell::new_align(&summary_other.total_lines.to_string(), Alignment::RIGHT),
161 Cell::new_align(
162 &Self::color_percentage(summary_other.lines_percentage(), 70., 80.),
163 Alignment::RIGHT,
164 ),
165 Cell::new("│"),
166 Cell::new_align(
167 &summary_other.total_functions_hit.to_string(),
168 Alignment::RIGHT,
169 ),
170 Cell::new_align(&summary_other.total_functions.to_string(), Alignment::RIGHT),
171 Cell::new_align(
172 &Self::color_percentage(summary_other.functions_percentage(), 70., 80.),
173 Alignment::RIGHT,
174 ),
175 ]));
176
177 table.add_row(Row::new(vec![
178 Cell::new_align("diff", Alignment::RIGHT),
179 Cell::new("│"),
180 Cell::new_align(
181 {
182 let diff =
183 summary_other.total_lines_hit as isize - summary.total_lines_hit as isize;
184 &match diff {
185 diff if diff > 0 => format!("+ {diff}"),
186 diff if diff < 0 => format!("- {}", diff.abs()),
187 _ => String::new(),
188 }
189 },
190 Alignment::RIGHT,
191 ),
192 Cell::new_align(
193 {
194 let diff = summary_other.total_lines as isize - summary.total_lines as isize;
195 &match diff {
196 diff if diff > 0 => format!("+ {diff}"),
197 diff if diff < 0 => format!("- {}", diff.abs()),
198 _ => String::new(),
199 }
200 },
201 Alignment::RIGHT,
202 ),
203 Cell::new_align(&Self::color_percentage_diff(lines_diff), Alignment::RIGHT),
204 Cell::new("│"),
205 Cell::new_align(
206 {
207 let diff = summary_other.total_functions_hit as isize
208 - summary.total_functions_hit as isize;
209 &match diff {
210 diff if diff > 0 => format!("+ {diff}"),
211 diff if diff < 0 => format!("- {}", diff.abs()),
212 _ => String::new(),
213 }
214 },
215 Alignment::RIGHT,
216 ),
217 Cell::new_align(
218 {
219 let diff =
220 summary_other.total_functions as isize - summary.total_functions as isize;
221 &match diff {
222 diff if diff > 0 => format!("+ {diff}"),
223 diff if diff < 0 => format!("- {}", diff.abs()),
224 _ => String::new(),
225 }
226 },
227 Alignment::RIGHT,
228 ),
229 Cell::new_align(
230 &Self::color_percentage_diff(functions_diff),
231 Alignment::RIGHT,
232 ),
233 ]));
234
235 table.printstd();
236 }
237
238 pub fn summary(&self) -> LcovSummary {
240 let mut total_lines = 0;
241 let mut total_lines_hit = 0;
242
243 let mut total_functions = 0;
244 let mut total_functions_hit = 0;
245 for file in &self.files {
246 total_lines += file.lines_found;
247 total_lines_hit += file.lines_hit;
248 total_functions += file.functions_found;
249 total_functions_hit += file.functions_hit;
250 }
251 LcovSummary {
252 total_lines,
253 total_lines_hit,
254 total_functions,
255 total_functions_hit,
256 }
257 }
258
259 pub fn summarystd(&self) {
261 use prettytable::{format::consts::FORMAT_CLEAN, format::Alignment, Cell, Row, Table};
262
263 let summary = self.summary();
264 let total_lines_p = summary.lines_percentage();
265 let total_functions_p = summary.functions_percentage();
266
267 let mut table = Table::new();
268 table.set_format(*FORMAT_CLEAN);
269 table.set_titles(Self::title_row());
270 table.add_row(Self::sub_title_row());
271
272 table.add_row(Row::new(vec![
273 Cell::new_align(&self.name.to_string_lossy(), Alignment::RIGHT),
274 Cell::new("│"),
275 Cell::new_align(&summary.total_lines_hit.to_string(), Alignment::RIGHT),
276 Cell::new_align(&summary.total_lines.to_string(), Alignment::RIGHT),
277 Cell::new_align(
278 &Self::color_percentage(total_lines_p, 70., 80.),
279 Alignment::RIGHT,
280 ),
281 Cell::new("│"),
282 Cell::new_align(&summary.total_functions_hit.to_string(), Alignment::RIGHT),
283 Cell::new_align(&summary.total_functions.to_string(), Alignment::RIGHT),
284 Cell::new_align(
285 &Self::color_percentage(total_functions_p, 70., 80.),
286 Alignment::RIGHT,
287 ),
288 ]));
289
290 table.printstd();
291 }
292
293 pub fn printstd(&self) {
295 use prettytable::{format::consts::FORMAT_CLEAN, format::Alignment, Cell, Row, Table};
296
297 let mut table = Table::new();
298 table.set_format(*FORMAT_CLEAN);
299
300 table.set_titles(Self::title_row());
301 table.add_row(Self::sub_title_row());
302
303 let summary = self.summary();
304 let total_lines_p = summary.lines_percentage();
305 let total_functions_p = summary.functions_percentage();
306
307 for file in &self.files {
308 let lines_percentage = (file.lines_hit as f64 / file.lines_found as f64) * 100.;
309
310 let functions_percentage =
311 (file.functions_hit as f64 / file.functions_found as f64) * 100.;
312
313 let file_name = if let Some(i) = file.name.find("/src") {
314 file.name.split_at(i + 1).1
315 } else {
316 &file.name
317 };
318
319 table.add_row(Row::new(vec![
320 Cell::new(file_name),
321 Cell::new("│"),
322 Cell::new_align(&file.lines_hit.to_string(), Alignment::RIGHT),
323 Cell::new_align(&file.lines_found.to_string(), Alignment::RIGHT),
324 Cell::new_align(
325 &Self::color_percentage(lines_percentage, 70., 80.),
326 Alignment::RIGHT,
327 ),
328 Cell::new("│"),
329 Cell::new_align(&file.functions_hit.to_string(), Alignment::RIGHT),
330 Cell::new_align(&file.functions_found.to_string(), Alignment::RIGHT),
331 Cell::new_align(
332 &Self::color_percentage(functions_percentage, 70., 80.),
333 Alignment::RIGHT,
334 ),
335 ]));
336 }
337
338 table.add_row(Row::new(vec![
339 Cell::new_align(&self.name.to_string_lossy(), Alignment::RIGHT),
340 Cell::new("│"),
341 Cell::new_align(&summary.total_lines_hit.to_string(), Alignment::RIGHT),
342 Cell::new_align(&summary.total_lines.to_string(), Alignment::RIGHT),
343 Cell::new_align(
344 &Self::color_percentage(total_lines_p, 70., 80.),
345 Alignment::RIGHT,
346 ),
347 Cell::new("│"),
348 Cell::new_align(&summary.total_functions_hit.to_string(), Alignment::RIGHT),
349 Cell::new_align(&summary.total_functions.to_string(), Alignment::RIGHT),
350 Cell::new_align(
351 &Self::color_percentage(total_functions_p, 70., 80.),
352 Alignment::RIGHT,
353 ),
354 ]));
355
356 table.printstd();
357 }
358
359 fn color_percentage_diff(value: f64) -> String {
360 use colored::*;
361
362 if value == 0. {
363 format!("= {value:.2}%").yellow().to_string()
364 } else if value > 0. {
365 format!("+ {value:.2}%").green().to_string()
366 } else {
367 let value = value.abs();
368 format!("- {value:.2}%").red().to_string()
369 }
370 }
371
372 fn color_percentage(value: f64, low: f64, mid: f64) -> String {
373 use colored::*;
374
375 let p = format!("{value:.2}%");
376 format!(
377 "{}",
378 if value < low {
379 p.red()
380 } else if value < mid {
381 p.yellow()
382 } else {
383 p.green()
384 }
385 )
386 }
387
388 fn title_row() -> prettytable::Row {
389 use prettytable::{format::Alignment, Cell, Row};
390
391 Row::new(vec![
392 Cell::new(""),
393 Cell::new(""),
394 {
395 let mut line = Cell::new_align("Lines", Alignment::CENTER);
396 line.set_hspan(3);
397 line
398 },
399 Cell::new(""),
400 {
401 let mut fs = Cell::new_align("Functions", Alignment::CENTER);
402 fs.set_hspan(3);
403 fs
404 },
405 ])
406 }
407 fn sub_title_row() -> prettytable::Row {
408 use prettytable::{Cell, Row};
409
410 Row::new(vec![
411 Cell::new(""),
412 Cell::new("│"),
413 Cell::new("Hit"),
414 Cell::new("Total"),
415 Cell::new("H/T"),
416 Cell::new("│"),
417 Cell::new("Hit"),
418 Cell::new("Total"),
419 Cell::new("H/T"),
420 ])
421 }
422}
423
424#[derive(Debug)]
425pub struct LcovFile {
426 name: String,
427 function_hits: HashMap<String, usize>,
428 functions_found: usize,
429 functions_hit: usize,
430 lines_found: usize,
431 lines_hit: usize,
432 branches_found: usize,
433 branches_hit: usize,
434}
435
436impl LcovFile {
437 pub fn new(source: &impl AsRef<str>) -> Self {
438 let source = source.as_ref();
439 Self {
440 name: source.to_string(),
441 function_hits: Default::default(),
442 functions_found: 0,
443 functions_hit: 0,
444 lines_found: 0,
445 lines_hit: 0,
446 branches_found: 0,
447 branches_hit: 0,
448 }
449 }
450}