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    ParseLcovError,
35    ReadLcovError,
36    UnknownPackageSelectorError
37)]
38pub struct CoverageGateError;
39
40/// Failed to invoke `cargo metadata` to enumerate workspace members.
41#[ohno::error]
42#[display("failed to load workspace metadata")]
43#[from(cargo_metadata::Error)]
44pub(crate) struct LoadMetadataError;
45
46/// The `coverage-gate.min-lines-percent` key was present in metadata
47/// but its value was not a JSON number.
48#[ohno::error]
49#[display("{source}: `coverage-gate.min-lines-percent` must be a number, got {min}")]
50pub(crate) struct InvalidThresholdValueError {
51    pub source: String,
52    pub min: Value,
53}
54
55/// The `coverage-gate.min-lines-percent` value was a number but fell
56/// outside the accepted `[0.0, 100.0]` range.
57#[ohno::error]
58#[display(
59    "invalid coverage-gate min-lines-percent value `{value}` for {source}: \
60     expected a value in {lower:.1}..={upper:.1}"
61)]
62pub(crate) struct ThresholdOutOfRangeError {
63    pub source: String,
64    pub value: f64,
65    pub lower: f64,
66    pub upper: f64,
67}
68
69/// An lcov tracefile was syntactically malformed.
70#[ohno::error]
71#[display("lcov tracefile is not well-formed")]
72#[from(lcov::report::ParseError)]
73pub(crate) struct ParseLcovError;
74
75/// Failed to read an lcov tracefile from disk (the file itself was
76/// inaccessible or unreadable, distinct from a malformed payload).
77#[ohno::error]
78#[display("failed to read lcov tracefile `{path}`")]
79pub(crate) struct ReadLcovError {
80    pub path: String,
81}
82
83/// A `--package` selector did not match any workspace member.
84#[ohno::error]
85#[display("`--package` selector `{selector}` did not match any workspace member")]
86pub(crate) struct UnknownPackageSelectorError {
87    pub selector: String,
88}
89
90#[cfg(test)]
91#[cfg_attr(coverage_nightly, coverage(off))]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn umbrella_propagates_load_metadata_chain() {
97        let inner = LoadMetadataError::caused_by(std::io::Error::other("no manifest"));
98        let outer: CoverageGateError = inner.into();
99        let rendered = outer.to_string();
100        assert!(rendered.contains("failed to load workspace metadata"));
101        assert!(rendered.contains("no manifest"));
102    }
103
104    #[test]
105    fn umbrella_propagates_parse_lcov() {
106        let inner = ParseLcovError::new();
107        let outer: CoverageGateError = inner.into();
108        let rendered = outer.to_string();
109        assert!(rendered.contains("lcov tracefile"));
110    }
111
112    #[test]
113    fn unknown_package_selector_carries_pattern() {
114        let err = UnknownPackageSelectorError::new("nope-*".to_owned());
115        let rendered = err.to_string();
116        assert!(rendered.contains("nope-*"));
117        assert!(rendered.contains("did not match"));
118    }
119
120    #[test]
121    fn threshold_out_of_range_renders_value_and_bounds() {
122        let err = ThresholdOutOfRangeError::new("alpha".to_owned(), 150.0, 0.0, 100.0);
123        let rendered = err.to_string();
124        assert!(rendered.contains("150"));
125        assert!(rendered.contains("alpha"));
126        assert!(rendered.contains("0.0..=100.0"));
127    }
128}