cargo_tarpaulin/report/
lcov.rs1use 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 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 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}