cargo_tarpaulin/report/
cobertura.rs

1#![allow(dead_code)]
2/// The XML structure for a cobatura report is roughly as follows:
3/// ```xml
4/// <coverage lines-valid="5" lines-covered="0" line-rate="0.0" branches-valid="0"
5/// branches-covered="0" branch-rate="0.0" version="1.9" timestamp="...">
6///   <sources>
7///     <source>PATH</source>
8///     ...
9///   </sources>
10///
11///   <packages>
12///     <package name=".."  line-rate="0.0" branch-rate="0.0" complexity="0.0">
13///       <classes>
14///         <class name="Main" filename="main.rs" line-rate="0.0" branch-rate="0.0" complexity="0.0">
15///           <methods>
16///             <method name="main" signature="()" line-rate="0.0" branch-rate="0.0">
17///               <lines>
18///                 <line number="1" hits="5" branch="false"/>
19///                 <line number="3" hits="2" branch="true">
20///                   <conditions>
21///                     <condition number="0" type="jump" coverage="50%"/>
22///                     ...
23///                   </conditions>
24///                 </line>
25///               </lines>
26///             </method>
27///             ...
28///           </methods>
29///
30///           <lines>
31///             <line number="10" hits="4" branch="false"/>
32///           </lines>
33///         </class>
34///         ...
35///       </classes>
36///     </package>
37///     ...
38///   </packages>
39/// </coverage>
40/// ```
41use std::collections::HashSet;
42use std::error;
43use std::fmt;
44use std::fs::File;
45use std::io::{Cursor, Write};
46use std::path::{Path, PathBuf};
47use std::time::{SystemTime, UNIX_EPOCH};
48
49use quick_xml::{
50    events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event},
51    Writer,
52};
53
54use chrono::offset::Utc;
55
56use crate::config::Config;
57use crate::traces::{CoverageStat, Trace, TraceMap};
58
59pub fn report(traces: &TraceMap, config: &Config) -> Result<(), Error> {
60    let result = Report::render(config, traces)?;
61    result.export(config)
62}
63
64#[derive(Debug)]
65pub enum Error {
66    Unknown,
67    ExportError(std::io::Error),
68}
69
70impl error::Error for Error {}
71
72impl fmt::Display for Error {
73    #[inline]
74    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75        match self {
76            Error::ExportError(ref e) => write!(f, "Export Error {e}"),
77            Error::Unknown => write!(f, "Unknown Error"),
78        }
79    }
80}
81
82#[derive(Debug)]
83pub struct Report {
84    timestamp: i64,
85    lines_covered: usize,
86    lines_valid: usize,
87    line_rate: f64,
88    branches_covered: usize,
89    branches_valid: usize,
90    branch_rate: f64,
91    sources: Vec<PathBuf>,
92    packages: Vec<Package>,
93}
94
95impl Report {
96    pub fn render(config: &Config, traces: &TraceMap) -> Result<Self, Error> {
97        let timestamp = Utc::now().timestamp();
98        let sources = render_sources(config);
99        let packages = render_packages(config, traces);
100        let mut line_rate = 0.0;
101        let mut branch_rate = 0.0;
102
103        if !packages.is_empty() {
104            line_rate = traces.coverage_percentage();
105            branch_rate = 0.0;
106        }
107
108        Ok(Report {
109            timestamp,
110            lines_covered: traces.total_covered(),
111            lines_valid: traces.total_coverable(),
112            line_rate,
113            branches_covered: 0,
114            branches_valid: 0,
115            branch_rate,
116            sources,
117            packages,
118        })
119    }
120
121    pub fn export(&self, config: &Config) -> Result<(), Error> {
122        let file_path = config.output_dir().join("cobertura.xml");
123        let mut file = File::create(file_path).map_err(|e| Error::ExportError(e))?;
124
125        let mut writer = Writer::new(Cursor::new(vec![]));
126        writer
127            .write_event(Event::Decl(BytesDecl::new("1.0", None, None)))
128            .map_err(Error::ExportError)?;
129
130        let cov_tag = "coverage";
131        let mut cov = BytesStart::new(cov_tag);
132        cov.push_attribute(("lines-covered", self.lines_covered.to_string().as_ref()));
133        cov.push_attribute(("lines-valid", self.lines_valid.to_string().as_ref()));
134        cov.push_attribute(("line-rate", self.line_rate.to_string().as_ref()));
135        cov.push_attribute((
136            "branches-covered",
137            self.branches_covered.to_string().as_ref(),
138        ));
139        cov.push_attribute(("branches-valid", self.branches_valid.to_string().as_ref()));
140        cov.push_attribute(("branch-rate", self.branch_rate.to_string().as_ref()));
141        cov.push_attribute(("complexity", "0"));
142        cov.push_attribute(("version", "1.9"));
143
144        let secs = match SystemTime::now().duration_since(UNIX_EPOCH) {
145            Ok(s) => s.as_secs().to_string(),
146            Err(_) => String::from("0"),
147        };
148        cov.push_attribute(("timestamp", secs.as_ref()));
149
150        writer
151            .write_event(Event::Start(cov))
152            .map_err(Error::ExportError)?;
153
154        self.export_header(&mut writer)
155            .map_err(Error::ExportError)?;
156
157        self.export_packages(&mut writer)
158            .map_err(Error::ExportError)?;
159
160        writer
161            .write_event(Event::End(BytesEnd::new(cov_tag)))
162            .map_err(Error::ExportError)?;
163
164        let result = writer.into_inner().into_inner();
165        file.write_all(&result).map_err(|e| Error::ExportError(e))
166    }
167
168    fn export_header<T: Write>(&self, writer: &mut Writer<T>) -> Result<(), std::io::Error> {
169        let sources_tag = "sources";
170        let source_tag = "source";
171        writer.write_event(Event::Start(BytesStart::new(sources_tag)))?;
172        for source in &self.sources {
173            if let Some(path) = source.to_str() {
174                writer.write_event(Event::Start(BytesStart::new(source_tag)))?;
175                writer.write_event(Event::Text(BytesText::new(path)))?;
176                writer.write_event(Event::End(BytesEnd::new(source_tag)))?;
177            }
178        }
179        writer
180            .write_event(Event::End(BytesEnd::new(sources_tag)))
181            .map(|_| ())
182    }
183
184    fn export_packages<T: Write>(&self, writer: &mut Writer<T>) -> Result<(), std::io::Error> {
185        let packages_tag = "packages";
186        let pack_tag = "package";
187
188        writer.write_event(Event::Start(BytesStart::new(packages_tag)))?;
189        // Export the package
190        for package in &self.packages {
191            let mut pack = BytesStart::new(pack_tag);
192            pack.push_attribute(("name", package.name.as_str()));
193            pack.push_attribute(("line-rate", package.line_rate.to_string().as_ref()));
194            pack.push_attribute(("branch-rate", package.branch_rate.to_string().as_ref()));
195            pack.push_attribute(("complexity", package.complexity.to_string().as_ref()));
196
197            writer.write_event(Event::Start(pack))?;
198            self.export_classes(&package.classes, writer)?;
199            writer.write_event(Event::End(BytesEnd::new(pack_tag)))?;
200        }
201
202        writer
203            .write_event(Event::End(BytesEnd::new(packages_tag)))
204            .map(|_| ())
205    }
206
207    fn export_classes<T: Write>(
208        &self,
209        classes: &[Class],
210        writer: &mut Writer<T>,
211    ) -> Result<(), std::io::Error> {
212        let classes_tag = "classes";
213        let class_tag = "class";
214        let methods_tag = "methods";
215
216        writer.write_event(Event::Start(BytesStart::new(classes_tag)))?;
217        for class in classes {
218            let mut c = BytesStart::new(class_tag);
219            c.push_attribute(("name", class.name.as_ref()));
220            c.push_attribute(("filename", class.file_name.as_ref()));
221            c.push_attribute(("line-rate", class.line_rate.to_string().as_ref()));
222            c.push_attribute(("branch-rate", class.branch_rate.to_string().as_ref()));
223            c.push_attribute(("complexity", class.complexity.to_string().as_ref()));
224
225            writer.write_event(Event::Start(c))?;
226            writer.write_event(Event::Empty(BytesStart::new(methods_tag)))?;
227            self.export_lines(&class.lines, writer)?;
228            writer.write_event(Event::End(BytesEnd::new(class_tag)))?;
229        }
230        writer
231            .write_event(Event::End(BytesEnd::new(classes_tag)))
232            .map(|_| ())
233    }
234
235    fn export_lines<T: Write>(
236        &self,
237        lines: &[Line],
238        writer: &mut Writer<T>,
239    ) -> Result<(), std::io::Error> {
240        let lines_tag = "lines";
241        let line_tag = "line";
242
243        writer.write_event(Event::Start(BytesStart::new(lines_tag)))?;
244        for line in lines {
245            let mut l = BytesStart::new(line_tag);
246            match line {
247                Line::Plain {
248                    ref number,
249                    ref hits,
250                } => {
251                    l.push_attribute(("number", number.to_string().as_ref()));
252                    l.push_attribute(("hits", hits.to_string().as_ref()));
253                }
254                Line::Branch { .. } => {}
255            }
256            writer.write_event(Event::Empty(l))?;
257        }
258        writer
259            .write_event(Event::End(BytesEnd::new(lines_tag)))
260            .map(|_| ())
261    }
262}
263
264fn render_sources(config: &Config) -> Vec<PathBuf> {
265    vec![config.get_base_dir()]
266}
267
268#[derive(Debug)]
269struct Package {
270    name: String,
271    line_rate: f64,
272    branch_rate: f64,
273    complexity: f64,
274    classes: Vec<Class>,
275}
276
277fn render_packages(config: &Config, traces: &TraceMap) -> Vec<Package> {
278    let dirs: HashSet<&Path> = traces
279        .files()
280        .into_iter()
281        .filter_map(|x| x.parent())
282        .collect();
283
284    dirs.into_iter()
285        .map(|x| render_package(config, traces, x))
286        .collect()
287}
288
289fn render_package(config: &Config, traces: &TraceMap, pkg: &Path) -> Package {
290    let name = config.strip_base_dir(pkg).to_str().unwrap().to_string();
291
292    let line_cover = traces.covered_in_path(pkg) as f64;
293    let coverable = traces.coverable_in_path(pkg);
294    let line_rate = if coverable > 0 {
295        line_cover / (coverable as f64)
296    } else {
297        0.0
298    };
299
300    Package {
301        name,
302        line_rate,
303        branch_rate: 0.0,
304        complexity: 0.0,
305        classes: render_classes(config, traces, pkg),
306    }
307}
308
309#[derive(Debug)]
310struct Class {
311    name: String,
312    file_name: String,
313    line_rate: f64,
314    branch_rate: f64,
315    complexity: f64,
316    lines: Vec<Line>,
317    methods: Vec<Method>,
318}
319
320fn render_classes(config: &Config, traces: &TraceMap, pkg: &Path) -> Vec<Class> {
321    traces
322        .files()
323        .iter()
324        .filter(|x| x.parent() == Some(pkg))
325        .filter_map(|x| render_class(config, traces, x))
326        .collect()
327}
328
329// TODO: Cobertura distinguishes between lines outside methods, and methods
330// (which also contain lines). As there is currently no way to get traces from
331// a particular function only, all traces are put into lines, and the vector
332// of methods is empty.
333//
334// Until this is fixed, the render_method function will panic if called, as it
335// cannot be properly implemented.
336//
337fn render_class(config: &Config, traces: &TraceMap, file: &Path) -> Option<Class> {
338    let name = file
339        .file_stem()
340        .map(|x| x.to_str().unwrap())
341        .unwrap_or_default()
342        .to_string();
343
344    let file_name = config.strip_base_dir(file).to_str().unwrap().to_string();
345    let coverable = traces.coverable_in_path(file);
346    if coverable == 0 {
347        None
348    } else {
349        let covered = traces.covered_in_path(file) as f64;
350        let line_rate = covered / coverable as f64;
351        let lines = traces.get_child_traces(file).map(render_line).collect();
352
353        Some(Class {
354            name,
355            file_name,
356            line_rate,
357            branch_rate: 0.0,
358            complexity: 0.0,
359            lines,
360            methods: vec![],
361        })
362    }
363}
364
365/// So I don't currently export out methods for cobertura. This may change in future easily enough
366/// though so leaving the type here as a potential stub.
367#[derive(Debug)]
368struct Method {
369    name: String,
370    signature: String,
371    line_rate: f64,
372    branch_rate: f64,
373    lines: Vec<Line>,
374}
375
376#[derive(Debug)]
377enum Line {
378    Plain {
379        number: usize,
380        hits: usize,
381    },
382
383    Branch {
384        number: usize,
385        hits: usize,
386        conditions: Vec<Condition>,
387    },
388}
389
390fn render_line(trace: &Trace) -> Line {
391    match &trace.stats {
392        CoverageStat::Line(hits) => Line::Plain {
393            number: trace.line as usize,
394            hits: *hits as usize,
395        },
396
397        // TODO: Branches in cobertura are given a fresh number as a label,
398        // which would require having some form of context when rendering.
399        //
400        _ => panic!("Not currently supported"),
401    }
402}
403
404#[derive(Debug)]
405struct Condition {
406    number: usize,
407    cond_type: ConditionType,
408    coverage: f64,
409}
410
411// Condition types
412#[derive(Debug)]
413enum ConditionType {
414    Jump,
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420    use crate::traces::*;
421    use std::collections::HashSet;
422    use std::path::PathBuf;
423
424    #[test]
425    fn package_coverage() {
426        let mut config = Config::default();
427        config.set_manifest(PathBuf::from("fake/Cargo.toml"));
428        let mut map = TraceMap::new();
429
430        map.add_file(&PathBuf::from("fake/examples/foo.rs"));
431
432        let empty_trace = Trace::new_stub(2);
433        let mut address = HashSet::new();
434        address.insert(2);
435        let hit_trace = Trace::new(3, address, 1);
436
437        let source_file = PathBuf::from("fake/src/lib.rs");
438
439        map.add_trace(&source_file, empty_trace);
440        map.add_trace(&source_file, hit_trace);
441
442        let report = Report::render(&config, &map).unwrap();
443        assert_eq!(report.lines_covered, 0);
444        assert_eq!(report.lines_valid, 2);
445        assert_eq!(report.line_rate, 0.0);
446        assert_eq!(report.packages.len(), 2);
447        assert_eq!(report.sources.len(), 1);
448
449        map.increment_hit(2);
450
451        let report = Report::render(&config, &map).unwrap();
452        assert_eq!(report.lines_covered, 1);
453        assert_eq!(report.lines_valid, 2);
454        assert_eq!(report.line_rate, 0.5);
455        assert_eq!(report.packages.len(), 2);
456        assert_eq!(report.sources.len(), 1);
457    }
458}