cargo_tarpaulin/report/
lcov.rs

1use crate::config::Config;
2use crate::errors::RunError;
3use crate::traces::{CoverageStat, TraceMap};
4use std::collections::BTreeMap;
5use std::fs::File;
6use std::io::Write;
7
8pub fn export(coverage_data: &TraceMap, config: &Config) -> Result<(), RunError> {
9    let file_path = config.output_dir().join("lcov.info");
10    let file = match File::create(file_path) {
11        Ok(k) => k,
12        Err(e) => return Err(RunError::Lcov(format!("File is not writeable: {e}"))),
13    };
14
15    write_lcov(file, coverage_data)
16}
17
18fn write_lcov(mut file: impl Write, coverage_data: &TraceMap) -> Result<(), RunError> {
19    for (path, traces) in coverage_data.iter() {
20        if traces.is_empty() {
21            continue;
22        }
23        writeln!(file, "TN:")?;
24        writeln!(file, "SF:{}", path.to_str().unwrap())?;
25
26        let mut fns: Vec<String> = vec![];
27        let mut fnda: Vec<String> = vec![];
28        let mut da: Vec<(u64, u64)> = vec![];
29
30        let mut fn_locs = coverage_data
31            .get_functions(&path)
32            .map(|x| ((x.start, x.end), &x.name))
33            .collect::<BTreeMap<_, _>>();
34
35        let mut first_fn = fn_locs.pop_first();
36
37        for trace in traces {
38            match &first_fn {
39                Some(((start, end), name)) if (*start..=*end).contains(&trace.line) => {
40                    let fn_hits = match trace.stats {
41                        CoverageStat::Line(hits) => hits,
42                        _ => {
43                            return Err(RunError::Lcov(
44                                "Function doesn't have hits number".to_string(),
45                            ))
46                        }
47                    };
48
49                    fns.push(format!("FN:{},{}", trace.line, name));
50                    fnda.push(format!("FNDA:{fn_hits},{name}"));
51
52                    first_fn = fn_locs.pop_first();
53                }
54                _ => {}
55            }
56
57            if let CoverageStat::Line(hits) = trace.stats {
58                da.push((trace.line, hits));
59            }
60        }
61
62        for fn_line in &fns {
63            writeln!(file, "{fn_line}",)?;
64        }
65
66        writeln!(file, "FNF:{}", fns.len())?;
67
68        for fnda_line in fnda {
69            writeln!(file, "{fnda_line}")?;
70        }
71
72        for (line, hits) in &da {
73            writeln!(file, "DA:{line},{hits}")?;
74        }
75
76        writeln!(file, "LF:{}", da.len())?;
77        writeln!(
78            file,
79            "LH:{}",
80            da.iter().filter(|(_, hits)| *hits != 0).count()
81        )?;
82
83        // TODO: add support for branching
84        // BRDA (BRDA:<line number>,<block number>,<branch number>,<hits>)
85        // BRF (branches found)
86        // BRH (branches hit)
87        // More at http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
88
89        writeln!(file, "end_of_record")?;
90    }
91    Ok(())
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::source_analysis::Function;
98    use crate::traces::*;
99    use lcov::{record::Record, Reader};
100    use std::collections::HashMap;
101    use std::io::Cursor;
102    use std::path::{Path, PathBuf};
103
104    #[test]
105    fn generate_valid_lcov() {
106        let mut traces = TraceMap::new();
107        traces.add_trace(
108            Path::new("foo.rs"),
109            Trace {
110                line: 4,
111                stats: CoverageStat::Line(1),
112                address: Default::default(),
113                length: 0,
114            },
115        );
116        traces.add_trace(
117            Path::new("foo.rs"),
118            Trace {
119                line: 5,
120                stats: CoverageStat::Line(0),
121                address: Default::default(),
122                length: 0,
123            },
124        );
125
126        traces.add_trace(
127            Path::new("bar.rs"),
128            Trace {
129                line: 14,
130                stats: CoverageStat::Line(9),
131                address: Default::default(),
132                length: 0,
133            },
134        );
135
136        let mut functions = HashMap::new();
137        functions.insert(
138            PathBuf::from("bar.rs"),
139            vec![Function {
140                name: "baz".to_string(),
141                start: 14,
142                end: 20,
143            }],
144        );
145        traces.set_functions(functions);
146
147        let mut data = vec![];
148        let cursor = Cursor::new(&mut data);
149
150        write_lcov(cursor, &traces).unwrap();
151
152        let reader = Reader::new(data.as_slice());
153        let mut items = 0;
154        let mut files_seen = 0;
155
156        let mut current_source = PathBuf::new();
157        for item in reader {
158            let record = item.unwrap();
159
160            match record {
161                Record::SourceFile { path } => {
162                    current_source = path.clone();
163                    // We know files are presented sorted
164                    if files_seen == 0 {
165                        assert_eq!(path, Path::new("bar.rs"));
166                    } else if files_seen == 1 {
167                        assert_eq!(path, Path::new("foo.rs"));
168                    } else {
169                        panic!("Too many files");
170                    }
171
172                    files_seen += 1;
173                }
174                Record::EndOfRecord => {
175                    current_source = PathBuf::new();
176                }
177                Record::FunctionName { name, start_line } => {
178                    assert_eq!(name, "baz");
179                    assert_eq!(start_line, 14);
180                }
181                Record::LineData {
182                    line,
183                    count,
184                    checksum: _,
185                } => {
186                    if current_source == Path::new("bar.rs") {
187                        assert_eq!(line, 14);
188                        assert_eq!(count, 9);
189                    } else if current_source == Path::new("foo.rs") {
190                        assert!((line == 4 && count == 1) || (line == 5 && count == 0));
191                    } else {
192                        panic!("Line data not attached to file");
193                    }
194                }
195                Record::LinesFound { found } => {
196                    if current_source == Path::new("bar.rs") {
197                        assert_eq!(found, 1);
198                    } else if current_source == Path::new("foo.rs") {
199                        assert_eq!(found, 2);
200                    } else {
201                        panic!("Lines found not attached to file");
202                    }
203                }
204                Record::LinesHit { hit } => {
205                    if current_source == Path::new("bar.rs") {
206                        assert_eq!(hit, 1);
207                    } else if current_source == Path::new("foo.rs") {
208                        assert_eq!(hit, 1);
209                    } else {
210                        panic!("Lines found not attached to file");
211                    }
212                }
213                _ => {}
214            }
215
216            items += 1;
217        }
218        assert!(items > 0);
219    }
220}