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    WorkspaceTargetPolicyError,
37    InvalidTargetTableError,
38    InvalidTargetPolicyShapeError,
39    MissingTargetPolicyBehaviorError,
40    InvalidTargetSelectorError,
41    UnsupportedTargetSelectorError,
42    AmbiguousTargetPolicyError,
43    WorkspaceScopedNoCoverableLinesError,
44    ResolveTargetError,
45    ParseLcovError,
46    ReadLcovError,
47    UnknownPackageSelectorError
48)]
49pub struct CoverageGateError;
50
51/// Failed to invoke `cargo metadata` to enumerate workspace members.
52#[ohno::error]
53#[display("failed to load workspace metadata")]
54#[from(cargo_metadata::Error)]
55pub(crate) struct LoadMetadataError;
56
57/// The `coverage-gate.min-lines-percent` key was present in metadata
58/// but its value was not a JSON number.
59#[ohno::error]
60#[display("{source}: `coverage-gate.min-lines-percent` must be a number, got {min}")]
61pub(crate) struct InvalidThresholdValueError {
62    pub source: String,
63    pub min: Value,
64}
65
66/// The `coverage-gate.min-lines-percent` value was a number but fell
67/// outside the accepted `[0.0, 100.0]` range.
68#[ohno::error]
69#[display(
70    "invalid coverage-gate min-lines-percent value `{value}` for {source}: \
71     expected a value in {lower:.1}..={upper:.1}"
72)]
73pub(crate) struct ThresholdOutOfRangeError {
74    pub source: String,
75    pub value: f64,
76    pub lower: f64,
77    pub upper: f64,
78}
79
80/// The `coverage-gate.expect-no-coverable-lines` key was present in
81/// metadata but its value was not a JSON boolean.
82#[ohno::error]
83#[display("{source}: `coverage-gate.expect-no-coverable-lines` must be a boolean, got {value}")]
84pub(crate) struct InvalidNoCoverableLinesValueError {
85    pub source: String,
86    pub value: Value,
87}
88
89/// A package set both `coverage-gate.min-lines-percent` and
90/// `coverage-gate.expect-no-coverable-lines = true`. The two are
91/// mutually exclusive: a numeric floor describes code that should be
92/// covered, while the assertion declares there is no coverable code at
93/// all.
94#[ohno::error]
95#[display(
96    "{source}: `coverage-gate` cannot set both `min-lines-percent` and \
97     `expect-no-coverable-lines`; pick one"
98)]
99pub(crate) struct ConflictingCoverageMetadataError {
100    pub source: String,
101}
102
103/// `coverage-gate.expect-no-coverable-lines` was set in
104/// `[workspace.metadata.coverage-gate]`. The assertion is about a single
105/// package's contents, so it is only meaningful per-package.
106#[ohno::error]
107#[display(
108    "`coverage-gate.expect-no-coverable-lines` is a package-level assertion and \
109     cannot be set in `[workspace.metadata.coverage-gate]`"
110)]
111pub(crate) struct WorkspaceScopedNoCoverableLinesError;
112
113/// Target policies were declared in workspace metadata.
114#[ohno::error]
115#[display("coverage-gate target policies are package-scoped and cannot be set in workspace metadata")]
116pub(crate) struct WorkspaceTargetPolicyError;
117
118/// A target-policy container was not a table.
119#[ohno::error]
120#[display("{source}: coverage-gate `target` must be a table keyed by target triple or cfg expression")]
121pub(crate) struct InvalidTargetTableError {
122    pub(crate) source: String,
123}
124
125/// A selected target policy was not a table.
126#[ohno::error]
127#[display("{source}: coverage-gate target policy must be a table")]
128pub(crate) struct InvalidTargetPolicyShapeError {
129    pub(crate) source: String,
130}
131
132/// A target policy did not select an effective behavior.
133#[ohno::error]
134#[display("{source}: target policy must set `min-lines-percent` or `expect-no-coverable-lines = true`")]
135pub(crate) struct MissingTargetPolicyBehaviorError {
136    pub(crate) source: String,
137}
138
139/// A target-policy selector was syntactically invalid.
140#[ohno::error]
141#[display("{source}: invalid coverage-gate target selector `{selector}`")]
142#[from(cargo_platform::ParseError)]
143pub(crate) struct InvalidTargetSelectorError {
144    pub(crate) source: String,
145    pub(crate) selector: String,
146}
147
148/// A target selector depends on Cargo build-unit context.
149#[ohno::error]
150#[display("{source}: coverage-gate target selector `{selector}` uses unsupported build-context configuration options: {options}")]
151pub(crate) struct UnsupportedTargetSelectorError {
152    pub(crate) source: String,
153    pub(crate) selector: String,
154    pub(crate) options: String,
155}
156
157/// More than one `cfg(...)` target policy matched the selected Rust target.
158#[ohno::error]
159#[display(
160    "{source}: multiple coverage-gate target policies match `{target}`: {selectors}; \
161     use disjoint cfg expressions or an exact Rust target-triple override"
162)]
163pub(crate) struct AmbiguousTargetPolicyError {
164    pub(crate) source: String,
165    pub(crate) target: String,
166    pub(crate) selectors: String,
167}
168
169/// The Rust target or its cfg values could not be obtained.
170#[ohno::error]
171#[display("failed to resolve Rust target")]
172#[from(ExecuteRustcError, RustcCommandFailedError, MissingRustcHostTargetError, InvalidRustcCfgError)]
173pub(crate) struct ResolveTargetError;
174
175/// A rustc target-information command could not be launched.
176#[ohno::error]
177#[display("could not execute `{command}`")]
178#[from(std::io::Error)]
179pub(crate) struct ExecuteRustcError {
180    pub(crate) command: String,
181}
182
183/// A rustc target-information command exited unsuccessfully.
184#[ohno::error]
185#[display("`{command}` exited with {status}: {stderr}")]
186pub(crate) struct RustcCommandFailedError {
187    pub(crate) command: String,
188    pub(crate) status: String,
189    pub(crate) stderr: String,
190}
191
192/// The rustc version output did not identify its host target.
193#[ohno::error]
194#[display("`{command}` did not report a host target")]
195pub(crate) struct MissingRustcHostTargetError {
196    pub(crate) command: String,
197}
198
199/// A cfg value emitted by rustc could not be parsed.
200#[ohno::error]
201#[display("rustc reported invalid cfg `{value}` for Rust target `{target}`")]
202#[from(cargo_platform::ParseError)]
203pub(crate) struct InvalidRustcCfgError {
204    pub(crate) value: String,
205    pub(crate) target: String,
206}
207
208/// An lcov tracefile was syntactically malformed.
209#[ohno::error]
210#[display("lcov tracefile is not well-formed")]
211#[from(lcov::report::ParseError)]
212pub(crate) struct ParseLcovError;
213
214/// Failed to read an lcov tracefile from disk (the file itself was
215/// inaccessible or unreadable, distinct from a malformed payload).
216#[ohno::error]
217#[display("failed to read lcov tracefile `{path}`")]
218pub(crate) struct ReadLcovError {
219    pub path: String,
220}
221
222/// A `--package` selector did not match any workspace member.
223#[ohno::error]
224#[display("`--package` selector `{selector}` did not match any workspace member")]
225pub(crate) struct UnknownPackageSelectorError {
226    pub selector: String,
227}
228
229#[cfg(test)]
230#[cfg_attr(coverage_nightly, coverage(off))]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn umbrella_propagates_load_metadata_chain() {
236        let inner = LoadMetadataError::caused_by(std::io::Error::other("no manifest"));
237        let outer: CoverageGateError = inner.into();
238        let rendered = outer.to_string();
239        assert!(rendered.contains("failed to load workspace metadata"));
240        assert!(rendered.contains("no manifest"));
241    }
242
243    #[test]
244    fn umbrella_propagates_parse_lcov() {
245        let inner = ParseLcovError::new();
246        let outer: CoverageGateError = inner.into();
247        let rendered = outer.to_string();
248        assert!(rendered.contains("lcov tracefile"));
249    }
250
251    #[test]
252    fn execute_rustc_error_preserves_io_source() {
253        let error = ExecuteRustcError::caused_by("rustc -vV".to_owned(), std::io::Error::other("launch failed"));
254        let source = std::error::Error::source(&error).expect("execute error must retain its IO source");
255        assert_eq!(source.to_string(), "launch failed");
256    }
257
258    #[test]
259    fn unknown_package_selector_carries_pattern() {
260        let err = UnknownPackageSelectorError::new("nope-*".to_owned());
261        let rendered = err.to_string();
262        assert!(rendered.contains("nope-*"));
263        assert!(rendered.contains("did not match"));
264    }
265
266    #[test]
267    fn threshold_out_of_range_renders_value_and_bounds() {
268        let err = ThresholdOutOfRangeError::new("alpha".to_owned(), 150.0, 0.0, 100.0);
269        let rendered = err.to_string();
270        assert!(rendered.contains("150"));
271        assert!(rendered.contains("alpha"));
272        assert!(rendered.contains("0.0..=100.0"));
273    }
274
275    #[test]
276    fn invalid_no_coverable_lines_value_renders_source_and_value() {
277        let err = InvalidNoCoverableLinesValueError::new("alpha".to_owned(), Value::from("yes"));
278        let rendered = err.to_string();
279        assert!(rendered.contains("alpha"));
280        assert!(rendered.contains("expect-no-coverable-lines"));
281        assert!(rendered.contains("boolean"));
282        assert!(rendered.contains("yes"));
283    }
284
285    #[test]
286    fn conflicting_coverage_metadata_renders_source() {
287        let err = ConflictingCoverageMetadataError::new("alpha".to_owned());
288        let rendered = err.to_string();
289        assert!(rendered.contains("alpha"));
290        assert!(rendered.contains("min-lines-percent"));
291        assert!(rendered.contains("expect-no-coverable-lines"));
292    }
293
294    #[test]
295    fn workspace_scoped_no_coverable_lines_mentions_workspace() {
296        let err = WorkspaceScopedNoCoverableLinesError::new();
297        let rendered = err.to_string();
298        assert!(rendered.contains("expect-no-coverable-lines"));
299        assert!(rendered.contains("workspace.metadata.coverage-gate"));
300    }
301
302    #[test]
303    fn ambiguous_target_policy_names_target_and_selectors() {
304        let err = AmbiguousTargetPolicyError::new(
305            "alpha".to_owned(),
306            "x86_64-pc-windows-msvc".to_owned(),
307            "cfg(windows), cfg(target_os = \"windows\")".to_owned(),
308        );
309        let rendered = err.to_string();
310        assert!(rendered.contains("alpha"));
311        assert!(rendered.contains("x86_64-pc-windows-msvc"));
312        assert!(rendered.contains("cfg(windows)"));
313    }
314}