1use clap::ValueEnum;
4use semver::{Version, VersionReq};
5use serde::{Deserialize, Deserializer, Serialize};
6use std::path::PathBuf;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
13#[serde(rename_all = "lowercase")]
14pub enum CoverageReportKind {
15 #[default]
16 Summary,
17 Lcov,
18 Debug,
19 Bytecode,
20 Attribution,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30pub struct CoverageConfig {
31 #[serde(default = "default_report_kinds")]
33 pub report: Vec<CoverageReportKind>,
34
35 #[serde(default = "default_lcov_version", deserialize_with = "deserialize_lcov_version")]
40 pub lcov_version: Version,
41
42 #[serde(default)]
46 pub ir_minimum: bool,
47
48 #[serde(default)]
51 pub report_file: Option<PathBuf>,
52
53 #[serde(default)]
55 pub include_libs: bool,
56
57 #[serde(default)]
59 pub exclude_tests: bool,
60
61 #[serde(default)]
73 pub skip_files: Vec<String>,
74}
75
76impl Default for CoverageConfig {
77 fn default() -> Self {
78 Self {
79 report: default_report_kinds(),
80 lcov_version: default_lcov_version(),
81 ir_minimum: false,
82 report_file: None,
83 include_libs: false,
84 exclude_tests: false,
85 skip_files: Vec::new(),
86 }
87 }
88}
89
90fn default_report_kinds() -> Vec<CoverageReportKind> {
91 vec![CoverageReportKind::Summary]
92}
93
94const fn default_lcov_version() -> Version {
95 Version::new(1, 0, 0)
96}
97
98fn deserialize_lcov_version<'de, D>(deserializer: D) -> Result<Version, D::Error>
99where
100 D: Deserializer<'de>,
101{
102 let version = String::deserialize(deserializer)?;
103 parse_lcov_version(&version).map_err(serde::de::Error::custom)
104}
105
106pub fn parse_lcov_version(s: &str) -> Result<Version, String> {
107 let vr = VersionReq::parse(&format!("={s}")).map_err(|e| e.to_string())?;
108 let [c] = &vr.comparators[..] else {
109 return Err("invalid version".to_string());
110 };
111 if c.op != semver::Op::Exact {
112 return Err("invalid version".to_string());
113 }
114 if !c.pre.is_empty() {
115 return Err("pre-releases are not supported".to_string());
116 }
117 Ok(Version::new(c.major, c.minor.unwrap_or(0), c.patch.unwrap_or(0)))
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn default_matches_cli_defaults() {
126 let cfg = CoverageConfig::default();
127 assert_eq!(cfg.report, vec![CoverageReportKind::Summary]);
128 assert_eq!(cfg.lcov_version, Version::new(1, 0, 0));
129 assert!(!cfg.ir_minimum);
130 assert!(cfg.report_file.is_none());
131 assert!(!cfg.include_libs);
132 assert!(!cfg.exclude_tests);
133 assert!(cfg.skip_files.is_empty());
134 }
135
136 #[test]
137 fn deserialize_from_toml() {
138 let toml = r#"
139 report = ["summary", "lcov"]
140 lcov_version = "2.2.0"
141 ir_minimum = true
142 report_file = "out/lcov.info"
143 include_libs = true
144 exclude_tests = true
145 skip_files = ["test/**", "src/mocks/**"]
146 "#;
147 let cfg: CoverageConfig = toml::from_str(toml).unwrap();
148 assert_eq!(cfg.report, vec![CoverageReportKind::Summary, CoverageReportKind::Lcov]);
149 assert_eq!(cfg.lcov_version, Version::new(2, 2, 0));
150 assert!(cfg.ir_minimum);
151 assert_eq!(cfg.report_file.as_deref(), Some(std::path::Path::new("out/lcov.info")));
152 assert!(cfg.include_libs);
153 assert!(cfg.exclude_tests);
154 assert_eq!(cfg.skip_files, vec!["test/**".to_string(), "src/mocks/**".to_string()]);
155 }
156
157 #[test]
158 fn deserialize_partial_uses_defaults() {
159 let toml = r#"skip_files = ["src/mocks/**"]"#;
160 let cfg: CoverageConfig = toml::from_str(toml).unwrap();
161 assert_eq!(cfg.report, vec![CoverageReportKind::Summary]);
163 assert_eq!(cfg.lcov_version, Version::new(1, 0, 0));
164 assert!(!cfg.ir_minimum);
165 assert_eq!(cfg.skip_files, vec!["src/mocks/**".to_string()]);
167 }
168
169 #[test]
170 fn deserialize_lcov_version_accepts_cli_formats() {
171 for (input, expected) in [
172 ("0", Version::new(0, 0, 0)),
173 ("1", Version::new(1, 0, 0)),
174 ("1.0", Version::new(1, 0, 0)),
175 ("1.1", Version::new(1, 1, 0)),
176 ("2", Version::new(2, 0, 0)),
177 ("2.2", Version::new(2, 2, 0)),
178 ] {
179 let cfg: CoverageConfig =
180 toml::from_str(&format!(r#"lcov_version = "{input}""#)).unwrap();
181 assert_eq!(cfg.lcov_version, expected);
182 }
183 }
184}