qlty_coverage/parser/
cobertura.rs

1use crate::Parser;
2use anyhow::{Context, Result};
3use qlty_types::tests::v1::FileCoverage;
4use serde::Deserialize;
5use std::collections::BTreeMap;
6
7#[derive(Debug, Deserialize)]
8#[serde(rename = "coverage")]
9struct CoberturaSource {
10    packages: Packages,
11}
12
13#[derive(Debug, Deserialize)]
14struct Packages {
15    package: Vec<Package>,
16}
17
18#[derive(Debug, Deserialize)]
19struct Package {
20    classes: Classes,
21}
22
23#[derive(Debug, Deserialize)]
24struct Classes {
25    class: Vec<Class>,
26}
27
28#[derive(Debug, Deserialize, Clone)]
29struct Class {
30    filename: String,
31    lines: Lines,
32}
33
34#[derive(Debug, Deserialize, Clone)]
35struct Lines {
36    #[serde(default)]
37    line: Option<Vec<Line>>,
38}
39
40#[derive(Debug, Deserialize, Clone)]
41struct Line {
42    number: String,
43    hits: String,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Default)]
47pub struct Cobertura {}
48
49impl Cobertura {
50    pub fn new() -> Self {
51        Self {}
52    }
53}
54
55impl Parser for Cobertura {
56    fn parse_text(&self, text: &str) -> Result<Vec<FileCoverage>> {
57        let source: CoberturaSource =
58            serde_xml_rs::from_str(text).with_context(|| "Failed to parse XML text")?;
59
60        // BTreeMap allows us to index by filename while sorting at the same time
61        let mut lines_by_filename: BTreeMap<String, Vec<Line>> = BTreeMap::new();
62        let mut file_coverages = vec![];
63
64        for package in source.packages.package.iter() {
65            for class in package.classes.class.iter() {
66                if let Some(ref lines) = class.lines.line {
67                    for line in lines {
68                        lines_by_filename
69                            .entry(class.filename.clone())
70                            .or_insert_with(Vec::new)
71                            .push(line.clone());
72                    }
73                } else {
74                    lines_by_filename
75                        .entry(class.filename.clone())
76                        .or_insert_with(Vec::new);
77                }
78            }
79        }
80
81        for (filename, lines) in lines_by_filename {
82            let mut line_hits = Vec::new();
83            let mut sorted_lines = lines.clone();
84            sorted_lines.sort_by_key(|line| line.number.parse::<i32>().unwrap_or_default());
85
86            if let Some(last_line) = sorted_lines.last() {
87                line_hits = vec![-1; last_line.number.parse::<usize>().unwrap_or(0)];
88
89                for line in sorted_lines.iter() {
90                    let line_number = line.number.parse::<usize>().ok().unwrap_or(0);
91
92                    if line_number > 0 {
93                        let hits = line.hits.parse::<i64>().ok().unwrap_or(-1);
94                        if line_hits[line_number - 1] == -1 {
95                            line_hits[line_number - 1] = hits;
96                        } else {
97                            line_hits[line_number - 1] += hits;
98                        }
99                    }
100                }
101            }
102
103            let file_coverage = FileCoverage {
104                path: filename,
105                hits: line_hits,
106                ..Default::default()
107            };
108
109            file_coverages.push(file_coverage);
110        }
111
112        Ok(file_coverages)
113    }
114}