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