omni_dev/coverage/
cobertura.rs1use anyhow::{Context, Result};
10use quick_xml::events::{BytesStart, Event};
11use quick_xml::reader::Reader;
12
13use super::model::{CoverageReport, FileCoverage};
14
15pub fn parse(content: &str) -> Result<CoverageReport> {
17 let mut reader = Reader::from_str(content);
18 reader.config_mut().check_end_names = false;
21 let mut report = CoverageReport::new();
22 let mut current: Option<FileCoverage> = None;
23
24 loop {
25 match reader.read_event().context("malformed cobertura XML")? {
26 Event::Eof => break,
27 Event::Start(e) | Event::Empty(e) => {
28 handle_start(&e, &mut current, &mut report)?;
29 }
30 Event::End(e) if e.name().as_ref() == b"class" => {
31 if let Some(file) = current.take() {
32 report.insert(file);
33 }
34 }
35 _ => {}
36 }
37 }
38
39 if let Some(file) = current.take() {
41 report.insert(file);
42 }
43
44 Ok(report)
45}
46
47fn handle_start(
49 e: &BytesStart,
50 current: &mut Option<FileCoverage>,
51 report: &mut CoverageReport,
52) -> Result<()> {
53 match e.name().as_ref() {
54 b"class" => {
55 if let Some(file) = current.take() {
57 report.insert(file);
58 }
59 if let Some(filename) = attr(e, b"filename")? {
60 *current = Some(FileCoverage::new(filename));
61 }
62 }
63 b"line" => {
64 if let Some(file) = current.as_mut() {
65 let number = attr(e, b"number")?
66 .and_then(|s| s.parse::<u32>().ok())
67 .context("cobertura <line> missing/invalid number attribute")?;
68 let hits = attr(e, b"hits")?
69 .and_then(|s| s.parse::<u64>().ok())
70 .unwrap_or(0);
71 file.record(number, hits);
72 }
73 }
74 _ => {}
75 }
76 Ok(())
77}
78
79fn attr(e: &BytesStart, key: &[u8]) -> Result<Option<String>> {
81 for attr in e.attributes() {
82 let attr = attr.context("malformed cobertura attribute")?;
83 if attr.key.as_ref() == key {
84 let value = attr
85 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
86 .context("invalid cobertura attribute value")?;
87 return Ok(Some(value.into_owned()));
88 }
89 }
90 Ok(None)
91}
92
93#[cfg(test)]
94#[allow(clippy::unwrap_used, clippy::expect_used)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn parses_classes_and_lines() {
100 let xml = r#"<?xml version="1.0" ?>
101<coverage>
102 <packages>
103 <package name="omni-dev">
104 <classes>
105 <class name="a" filename="src/a.rs">
106 <lines>
107 <line number="1" hits="5"/>
108 <line number="2" hits="0"/>
109 </lines>
110 </class>
111 <class name="b" filename="src/b.rs">
112 <lines>
113 <line number="1" hits="1"/>
114 </lines>
115 </class>
116 </classes>
117 </package>
118 </packages>
119</coverage>"#;
120 let report = parse(xml).unwrap();
121 assert_eq!(report.files.len(), 2);
122 assert_eq!(report.hits("src/a.rs", 1), Some(5));
123 assert_eq!(report.hits("src/a.rs", 2), Some(0));
124 assert_eq!(report.hits("src/b.rs", 1), Some(1));
125 }
126
127 #[test]
128 fn merges_split_file_classes() {
129 let xml = r#"<coverage><packages><package><classes>
130 <class filename="src/a.rs"><lines><line number="1" hits="0"/></lines></class>
131 <class filename="src/a.rs"><lines><line number="2" hits="3"/></lines></class>
132 </classes></package></packages></coverage>"#;
133 let report = parse(xml).unwrap();
134 assert_eq!(report.files.len(), 1);
135 assert_eq!(report.hits("src/a.rs", 1), Some(0));
136 assert_eq!(report.hits("src/a.rs", 2), Some(3));
137 }
138
139 #[test]
140 fn ignores_branch_conditions() {
141 let xml = r#"<coverage><packages><package><classes>
142 <class filename="src/a.rs"><lines>
143 <line number="1" hits="2" branch="true" condition-coverage="50% (1/2)">
144 <conditions><condition number="0" type="jump" coverage="50%"/></conditions>
145 </line>
146 </lines></class>
147 </classes></package></packages></coverage>"#;
148 let report = parse(xml).unwrap();
149 let f = &report.files["src/a.rs"];
150 assert_eq!(f.lines.len(), 1);
151 assert_eq!(f.lines.get(&1), Some(&2));
152 }
153
154 #[test]
155 fn tolerates_unclosed_class() {
156 let xml = r#"<coverage><class filename="a.rs"><lines><line number="1" hits="1"/></lines>"#;
158 let report = parse(xml).unwrap();
159 assert_eq!(report.hits("a.rs", 1), Some(1));
160 }
161
162 #[test]
163 fn new_class_closes_previous_unclosed_class() {
164 let xml = r#"<coverage>
165 <class filename="a.rs"><lines><line number="1" hits="1"/></lines>
166 <class filename="b.rs"><lines><line number="2" hits="0"/></lines></class>
167 </coverage>"#;
168 let report = parse(xml).unwrap();
169 assert_eq!(report.hits("a.rs", 1), Some(1));
170 assert_eq!(report.hits("b.rs", 2), Some(0));
171 }
172
173 #[test]
174 fn lines_outside_a_class_are_ignored() {
175 let xml = r#"<coverage><lines><line number="1" hits="1"/></lines></coverage>"#;
176 let report = parse(xml).unwrap();
177 assert!(report.files.is_empty());
178 }
179
180 #[test]
181 fn malformed_xml_errors() {
182 assert!(parse("<coverage><class").is_err());
183 }
184}