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