Skip to main content

foundry_config/
coverage.rs

1//! Configuration for `forge coverage`.
2
3use clap::ValueEnum;
4use semver::{Version, VersionReq};
5use serde::{Deserialize, Deserializer, Serialize};
6use std::path::PathBuf;
7
8/// Coverage report kinds that can be generated by `forge coverage`.
9///
10/// Used both as a CLI value (`--report <kind>`) and as a TOML configuration
11/// value under `[profile.<name>.coverage] report = ["..."]`.
12#[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/// Configuration for `forge coverage`, exposed under `[coverage]` and
23/// `[profile.<name>.coverage]` in `foundry.toml`.
24///
25/// Fields here mirror the CLI flags accepted by `forge coverage`. CLI flags
26/// take precedence over configuration values when both are provided.
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
28pub struct CoverageConfig {
29    /// The report kinds to generate. Defaults to `["summary"]`.
30    #[serde(default = "default_report_kinds")]
31    pub report: Vec<CoverageReportKind>,
32
33    /// The version of the LCOV "tracefile" format to use.
34    ///
35    /// Defaults to `1` (parsed as `1.0.0`). See `forge coverage --help` for the
36    /// supported variants and their differences.
37    #[serde(default = "default_lcov_version", deserialize_with = "deserialize_lcov_version")]
38    pub lcov_version: Version,
39
40    /// Whether to enable `viaIR` with minimum optimization. Useful as a
41    /// workaround for "stack too deep" errors at the cost of source map
42    /// accuracy.
43    #[serde(default)]
44    pub ir_minimum: bool,
45
46    /// Path to write the coverage report to (relative to the project root). If
47    /// unset, the report is written to the project root.
48    #[serde(default)]
49    pub report_file: Option<PathBuf>,
50
51    /// Whether to include library dependencies in the coverage report.
52    #[serde(default)]
53    pub include_libs: bool,
54
55    /// Whether to exclude tests from the coverage report.
56    #[serde(default)]
57    pub exclude_tests: bool,
58
59    /// Glob patterns of source files to exclude from the coverage report.
60    ///
61    /// Patterns are matched against project-root-relative paths using the
62    /// `globset` crate's `Glob` semantics (`**`, `*`, `?`, character classes).
63    /// A file is skipped if any pattern matches.
64    ///
65    /// Example:
66    /// ```toml
67    /// [profile.default.coverage]
68    /// skip_files = ["test/**", "script/**", "src/mocks/**"]
69    /// ```
70    #[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        // Defaulted fields keep their default values.
160        assert_eq!(cfg.report, vec![CoverageReportKind::Summary]);
161        assert_eq!(cfg.lcov_version, Version::new(1, 0, 0));
162        assert!(!cfg.ir_minimum);
163        // Set field came through.
164        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}