qlty_coverage/parser/
simplecov.rs

1use crate::Parser;
2use anyhow::{Context, Result};
3use qlty_types::tests::v1::FileCoverage;
4use semver::Version;
5use serde_json::Value;
6
7#[derive(Debug, Clone, PartialEq, Eq, Default)]
8pub struct Simplecov {}
9
10impl Simplecov {
11    pub fn new() -> Self {
12        Self {}
13    }
14}
15
16impl Parser for Simplecov {
17    fn parse_text(&self, text: &str) -> Result<Vec<FileCoverage>> {
18        let json_value: Value =
19            serde_json::from_str(text).with_context(|| "Failed to parse JSON text")?;
20
21        let mut file_coverages = vec![];
22
23        let coverage_data = if self.is_version_018_or_newer(&json_value) {
24            json_value.get("coverage").and_then(|c| c.as_object())
25        } else {
26            json_value
27                .as_object()
28                .and_then(|obj| obj.values().next())
29                .and_then(|group| group.get("coverage").and_then(|c| c.as_object()))
30        };
31
32        if let Some(coverage) = coverage_data {
33            for (file_path, data) in coverage {
34                let line_hits = self.parse_line_coverage(data);
35
36                let file_coverage = FileCoverage {
37                    path: file_path.to_string(),
38                    hits: line_hits,
39                    ..Default::default()
40                };
41
42                file_coverages.push(file_coverage);
43            }
44        }
45
46        Ok(file_coverages)
47    }
48}
49
50impl Simplecov {
51    fn parse_line_coverage(&self, data: &Value) -> Vec<i64> {
52        match data {
53            Value::Object(obj) => {
54                // Post-0.18.0 format with "lines" key
55                obj.get("lines")
56                    .and_then(|v| v.as_array())
57                    .map_or(vec![], |arr| {
58                        arr.iter().map(|x| self.parse_lines(x)).collect()
59                    })
60            }
61            Value::Array(arr) => {
62                // Pre-0.18.0 format, directly an array
63                arr.iter().map(|x| self.parse_lines(x)).collect()
64            }
65            _ => vec![],
66        }
67    }
68
69    fn parse_lines(&self, value: &Value) -> i64 {
70        match value {
71            Value::Number(n) => n.as_i64().unwrap_or(-1),
72            Value::String(s) if s == "ignored" => -2,
73            Value::Null => -1,
74            _ => -1,
75        }
76    }
77
78    fn is_version_018_or_newer(&self, json_value: &serde_json::Value) -> bool {
79        if let Some(meta) = json_value.get("meta") {
80            if let Some(version_str) = meta.get("simplecov_version").and_then(|v| v.as_str()) {
81                if let Ok(version) = Version::parse(version_str) {
82                    return version >= Version::parse("0.18.0").expect("Parsing version failed");
83                }
84            }
85        }
86        false
87    }
88}
89
90#[cfg(test)]
91mod test {
92    use super::*;
93
94    #[test]
95    fn simplecov_report() {
96        let input = r#"
97        {
98            "meta": {
99                "simplecov_version": "0.21.2"
100            },
101            "coverage": {
102                "sample.rb": {
103                    "lines": [null, 1, 1, 1, 1, null, null, 1, 1, null, null, 1, 1, 0, null, 1, null, null, null, "ignored", "ignored", "ignored", "ignored", "ignored", null]
104                }
105            },
106            "groups": {}
107        }
108        "#;
109        let results = Simplecov::new().parse_text(input).unwrap();
110        insta::assert_yaml_snapshot!(results, @r#"
111        - path: sample.rb
112          hits:
113            - "-1"
114            - "1"
115            - "1"
116            - "1"
117            - "1"
118            - "-1"
119            - "-1"
120            - "1"
121            - "1"
122            - "-1"
123            - "-1"
124            - "1"
125            - "1"
126            - "0"
127            - "-1"
128            - "1"
129            - "-1"
130            - "-1"
131            - "-1"
132            - "-2"
133            - "-2"
134            - "-2"
135            - "-2"
136            - "-2"
137            - "-1"
138        "#);
139    }
140
141    #[test]
142    fn simplecov_legacy_report() {
143        let input = r#"
144        {
145            "Unit Tests": {
146                "coverage": {
147                    "development/mygem/lib/mygem/errors.rb": [1, null, 1, 1, 0, null, null, null, 1, null, null, null, 1, null, null, null, 1, null, null, null, null]
148                },
149                "timestamp": 1488827968
150            }
151        }
152        "#;
153        let results = Simplecov::new().parse_text(input).unwrap();
154        insta::assert_yaml_snapshot!(results, @r#"
155        - path: development/mygem/lib/mygem/errors.rb
156          hits:
157            - "1"
158            - "-1"
159            - "1"
160            - "1"
161            - "0"
162            - "-1"
163            - "-1"
164            - "-1"
165            - "1"
166            - "-1"
167            - "-1"
168            - "-1"
169            - "1"
170            - "-1"
171            - "-1"
172            - "-1"
173            - "1"
174            - "-1"
175            - "-1"
176            - "-1"
177            - "-1"
178        "#);
179    }
180}