1#![allow(dead_code)]
2use 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 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
329fn 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#[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 _ => 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#[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}