Skip to main content

cargo_coverage_gate/
error.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Error types for the `cargo-coverage-gate` library.
5//!
6//! Built on [`ohno`] for backtrace capture and error-chain support.
7//! The public surface is a single zero-field [`CoverageGateError`]
8//! umbrella that every fallible library function returns. Each
9//! distinct failure mode is a separate `pub(crate)` typed error that
10//! converts `.into()` the umbrella via `#[from]`, so the `?` operator
11//! propagates naturally.
12//!
13//! Per-call-site context (what we were trying to do when the failure
14//! surfaced) is attached with [`ohno::enrich_err`] at function level,
15//! which also stamps file and line into the error chain.
16
17use serde_json::Value;
18
19/// Top-level error returned from every fallible function in the
20/// `cargo-coverage-gate` library.
21///
22/// Carries no free-form fields — the specific cause is encoded in the
23/// chained source error (see the `From` impls). Callers surface the
24/// message verbatim through their own diagnostic surface; the
25/// [`Display`] rendering includes the chained source as `Caused by: …`
26/// automatically.
27///
28/// [`Display`]: std::fmt::Display
29#[ohno::error]
30#[from(
31    LoadMetadataError,
32    InvalidThresholdValueError,
33    ThresholdOutOfRangeError,
34    InvalidNoCoverableLinesValueError,
35    ConflictingCoverageMetadataError,
36    WorkspaceScopedNoCoverableLinesError,
37    ParseLcovError,
38    ReadLcovError,
39    UnknownPackageSelectorError
40)]
41pub struct CoverageGateError;
42
43/// Failed to invoke `cargo metadata` to enumerate workspace members.
44#[ohno::error]
45#[display("failed to load workspace metadata")]
46#[from(cargo_metadata::Error)]
47pub(crate) struct LoadMetadataError;
48
49/// The `coverage-gate.min-lines-percent` key was present in metadata
50/// but its value was not a JSON number.
51#[ohno::error]
52#[display("{source}: `coverage-gate.min-lines-percent` must be a number, got {min}")]
53pub(crate) struct InvalidThresholdValueError {
54    pub source: String,
55    pub min: Value,
56}
57
58/// The `coverage-gate.min-lines-percent` value was a number but fell
59/// outside the accepted `[0.0, 100.0]` range.
60#[ohno::error]
61#[display(
62    "invalid coverage-gate min-lines-percent value `{value}` for {source}: \
63     expected a value in {lower:.1}..={upper:.1}"
64)]
65pub(crate) struct ThresholdOutOfRangeError {
66    pub source: String,
67    pub value: f64,
68    pub lower: f64,
69    pub upper: f64,
70}
71
72/// The `coverage-gate.expect-no-coverable-lines` key was present in
73/// metadata but its value was not a JSON boolean.
74#[ohno::error]
75#[display("{source}: `coverage-gate.expect-no-coverable-lines` must be a boolean, got {value}")]
76pub(crate) struct InvalidNoCoverableLinesValueError {
77    pub source: String,
78    pub value: Value,
79}
80
81/// A package set both `coverage-gate.min-lines-percent` and
82/// `coverage-gate.expect-no-coverable-lines = true`. The two are
83/// mutually exclusive: a numeric floor describes code that should be
84/// covered, while the assertion declares there is no coverable code at
85/// all.
86#[ohno::error]
87#[display(
88    "{source}: `coverage-gate` cannot set both `min-lines-percent` and \
89     `expect-no-coverable-lines`; pick one"
90)]
91pub(crate) struct ConflictingCoverageMetadataError {
92    pub source: String,
93}
94
95/// `coverage-gate.expect-no-coverable-lines` was set in
96/// `[workspace.metadata.coverage-gate]`. The assertion is about a single
97/// package's contents, so it is only meaningful per-package.
98#[ohno::error]
99#[display(
100    "`coverage-gate.expect-no-coverable-lines` is a package-level assertion and \
101     cannot be set in `[workspace.metadata.coverage-gate]`"
102)]
103pub(crate) struct WorkspaceScopedNoCoverableLinesError;
104
105/// An lcov tracefile was syntactically malformed.
106#[ohno::error]
107#[display("lcov tracefile is not well-formed")]
108#[from(lcov::report::ParseError)]
109pub(crate) struct ParseLcovError;
110
111/// Failed to read an lcov tracefile from disk (the file itself was
112/// inaccessible or unreadable, distinct from a malformed payload).
113#[ohno::error]
114#[display("failed to read lcov tracefile `{path}`")]
115pub(crate) struct ReadLcovError {
116    pub path: String,
117}
118
119/// A `--package` selector did not match any workspace member.
120#[ohno::error]
121#[display("`--package` selector `{selector}` did not match any workspace member")]
122pub(crate) struct UnknownPackageSelectorError {
123    pub selector: String,
124}
125
126#[cfg(test)]
127#[cfg_attr(coverage_nightly, coverage(off))]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn umbrella_propagates_load_metadata_chain() {
133        let inner = LoadMetadataError::caused_by(std::io::Error::other("no manifest"));
134        let outer: CoverageGateError = inner.into();
135        let rendered = outer.to_string();
136        assert!(rendered.contains("failed to load workspace metadata"));
137        assert!(rendered.contains("no manifest"));
138    }
139
140    #[test]
141    fn umbrella_propagates_parse_lcov() {
142        let inner = ParseLcovError::new();
143        let outer: CoverageGateError = inner.into();
144        let rendered = outer.to_string();
145        assert!(rendered.contains("lcov tracefile"));
146    }
147
148    #[test]
149    fn unknown_package_selector_carries_pattern() {
150        let err = UnknownPackageSelectorError::new("nope-*".to_owned());
151        let rendered = err.to_string();
152        assert!(rendered.contains("nope-*"));
153        assert!(rendered.contains("did not match"));
154    }
155
156    #[test]
157    fn threshold_out_of_range_renders_value_and_bounds() {
158        let err = ThresholdOutOfRangeError::new("alpha".to_owned(), 150.0, 0.0, 100.0);
159        let rendered = err.to_string();
160        assert!(rendered.contains("150"));
161        assert!(rendered.contains("alpha"));
162        assert!(rendered.contains("0.0..=100.0"));
163    }
164
165    #[test]
166    fn invalid_no_coverable_lines_value_renders_source_and_value() {
167        let err = InvalidNoCoverableLinesValueError::new("alpha".to_owned(), Value::from("yes"));
168        let rendered = err.to_string();
169        assert!(rendered.contains("alpha"));
170        assert!(rendered.contains("expect-no-coverable-lines"));
171        assert!(rendered.contains("boolean"));
172        assert!(rendered.contains("yes"));
173    }
174
175    #[test]
176    fn conflicting_coverage_metadata_renders_source() {
177        let err = ConflictingCoverageMetadataError::new("alpha".to_owned());
178        let rendered = err.to_string();
179        assert!(rendered.contains("alpha"));
180        assert!(rendered.contains("min-lines-percent"));
181        assert!(rendered.contains("expect-no-coverable-lines"));
182    }
183
184    #[test]
185    fn workspace_scoped_no_coverable_lines_mentions_workspace() {
186        let err = WorkspaceScopedNoCoverableLinesError::new();
187        let rendered = err.to_string();
188        assert!(rendered.contains("expect-no-coverable-lines"));
189        assert!(rendered.contains("workspace.metadata.coverage-gate"));
190    }
191}