lcov_summary/
lcov_file.rs

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    /// Parse an LCOV file.
31    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    /// Return a reference to the parsed files.
110    pub fn files(&self) -> &[LcovFile] {
111        &self.files
112    }
113
114    /// Return a mutable reference to the parsed files.
115    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    /// Print a summary of the diff of two files to stdout.
124    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    /// Return the summary of a an LCOV file.
239    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    /// Print the summary of an LCOV file to stdout.
260    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    /// Print the LCOV file to stdout.
294    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}