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