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