omni_dev/coverage/
format.rs1use std::fmt;
4
5use anyhow::{Context, Result};
6
7use super::model::CoverageReport;
8use super::{cobertura, lcov, llvm_json};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Format {
13 Lcov,
15 LlvmCovJson,
17 Cobertura,
19}
20
21impl fmt::Display for Format {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 let name = match self {
24 Self::Lcov => "lcov",
25 Self::LlvmCovJson => "llvm-cov-json",
26 Self::Cobertura => "cobertura",
27 };
28 f.write_str(name)
29 }
30}
31
32impl Format {
33 pub fn detect(content: &str) -> Result<Self> {
38 let trimmed = content.trim_start();
39 let first = trimmed
40 .chars()
41 .next()
42 .context("coverage report is empty; cannot detect format")?;
43 match first {
44 '<' => Ok(Self::Cobertura),
45 '{' | '[' => Ok(Self::LlvmCovJson),
46 _ if trimmed.starts_with("TN:")
47 || trimmed.starts_with("SF:")
48 || trimmed.starts_with("DA:") =>
49 {
50 Ok(Self::Lcov)
51 }
52 _ => anyhow::bail!(
53 "could not auto-detect coverage report format; pass an explicit --report-format \
54 (lcov, llvm-cov-json, or cobertura)"
55 ),
56 }
57 }
58
59 pub fn parse(self, content: &str) -> Result<CoverageReport> {
61 match self {
62 Self::Lcov => lcov::parse(content),
63 Self::LlvmCovJson => llvm_json::parse(content),
64 Self::Cobertura => cobertura::parse(content),
65 }
66 }
67}
68
69pub fn parse(content: &str, format: Option<Format>) -> Result<CoverageReport> {
71 let format = match format {
72 Some(f) => f,
73 None => Format::detect(content).context("coverage report format auto-detection failed")?,
74 };
75 format
76 .parse(content)
77 .with_context(|| format!("failed to parse {format} coverage report"))
78}
79
80#[cfg(test)]
81#[allow(clippy::unwrap_used, clippy::expect_used)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn detects_lcov() {
87 assert_eq!(
88 Format::detect("SF:src/a.rs\nDA:1,1\n").unwrap(),
89 Format::Lcov
90 );
91 assert_eq!(Format::detect("TN:\nSF:x\n").unwrap(), Format::Lcov);
92 }
93
94 #[test]
95 fn detects_json() {
96 assert_eq!(
97 Format::detect(" {\"data\":[]}").unwrap(),
98 Format::LlvmCovJson
99 );
100 }
101
102 #[test]
103 fn detects_cobertura() {
104 assert_eq!(
105 Format::detect("<?xml version=\"1.0\"?><coverage/>").unwrap(),
106 Format::Cobertura
107 );
108 }
109
110 #[test]
111 fn unknown_format_errors() {
112 assert!(Format::detect("hello world").is_err());
113 assert!(Format::detect("").is_err());
114 }
115
116 #[test]
117 fn parse_dispatches_by_detection() {
118 let report = parse("SF:src/a.rs\nDA:1,2\nend_of_record\n", None).unwrap();
119 assert_eq!(report.hits("src/a.rs", 1), Some(2));
120 }
121
122 #[test]
123 fn display_names() {
124 assert_eq!(Format::Lcov.to_string(), "lcov");
125 assert_eq!(Format::LlvmCovJson.to_string(), "llvm-cov-json");
126 assert_eq!(Format::Cobertura.to_string(), "cobertura");
127 }
128
129 #[test]
130 fn parse_with_explicit_format_dispatches_each_parser() {
131 let lcov = parse("SF:a.rs\nDA:1,1\nend_of_record\n", Some(Format::Lcov)).unwrap();
132 assert_eq!(lcov.hits("a.rs", 1), Some(1));
133
134 let json = parse(
135 r#"{"data":[{"files":[{"filename":"a.rs","segments":[[1,1,3,true,true,false],[2,1,0,false,false,false]]}]}]}"#,
136 Some(Format::LlvmCovJson),
137 )
138 .unwrap();
139 assert_eq!(json.hits("a.rs", 1), Some(3));
140
141 let xml = parse(
142 r#"<coverage><packages><package><classes><class filename="a.rs"><lines><line number="1" hits="2"/></lines></class></classes></package></packages></coverage>"#,
143 Some(Format::Cobertura),
144 )
145 .unwrap();
146 assert_eq!(xml.hits("a.rs", 1), Some(2));
147 }
148
149 #[test]
150 fn parse_propagates_parser_errors() {
151 assert!(parse("{ not json", None).is_err());
153 }
154}