Skip to main content

cuenv_ci/flake/
error.rs

1//! Error types for flake.lock parsing and purity analysis
2
3use miette::Diagnostic;
4use std::path::PathBuf;
5use thiserror::Error;
6
7/// Errors related to flake.lock parsing and purity analysis
8#[derive(Debug, Error, Diagnostic)]
9pub enum FlakeLockError {
10    /// Failed to parse flake.lock JSON
11    #[error("Failed to parse flake.lock: {0}")]
12    #[diagnostic(
13        code(cuenv::ci::flake::parse),
14        help("Ensure flake.lock is valid JSON and follows Nix flake.lock schema v7")
15    )]
16    ParseError(String),
17
18    /// Failed to read flake.lock file
19    #[error("Failed to read flake.lock at {path}: {message}")]
20    #[diagnostic(
21        code(cuenv::ci::flake::io),
22        help("Check that flake.lock exists and is readable")
23    )]
24    IoError {
25        /// Path to the flake.lock file
26        path: PathBuf,
27        /// Error message
28        message: String,
29    },
30
31    /// Flake purity check failed in strict mode
32    #[error("Flake purity check failed with {count} unlocked input(s): {}", inputs.join(", "))]
33    #[diagnostic(
34        code(cuenv::ci::flake::impure_strict),
35        help(
36            "In strict mode, all flake inputs must be locked. Run 'nix flake lock' to fix, or use purity_mode: warning/override"
37        )
38    )]
39    StrictModeViolation {
40        /// Number of unlocked inputs
41        count: usize,
42        /// Names of unlocked inputs
43        inputs: Vec<String>,
44    },
45
46    /// Missing flake.lock file
47    #[error("No flake.lock file found at {path}")]
48    #[diagnostic(
49        code(cuenv::ci::flake::missing),
50        help("Run 'nix flake lock' to generate a flake.lock file")
51    )]
52    MissingLockFile {
53        /// Expected path to the flake.lock file
54        path: PathBuf,
55    },
56}
57
58impl FlakeLockError {
59    /// Create a parse error
60    #[must_use]
61    pub fn parse(message: impl Into<String>) -> Self {
62        Self::ParseError(message.into())
63    }
64
65    /// Create an IO error
66    #[must_use]
67    pub fn io(path: impl Into<PathBuf>, message: impl Into<String>) -> Self {
68        Self::IoError {
69            path: path.into(),
70            message: message.into(),
71        }
72    }
73
74    /// Create a strict mode violation error
75    #[must_use]
76    pub const fn strict_violation(inputs: Vec<String>) -> Self {
77        Self::StrictModeViolation {
78            count: inputs.len(),
79            inputs,
80        }
81    }
82
83    /// Create a missing lock file error
84    #[must_use]
85    pub fn missing(path: impl Into<PathBuf>) -> Self {
86        Self::MissingLockFile { path: path.into() }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_parse_error_constructor() {
96        let err = FlakeLockError::parse("invalid JSON");
97        assert!(matches!(err, FlakeLockError::ParseError(_)));
98        let display = format!("{err}");
99        assert!(display.contains("Failed to parse flake.lock"));
100        assert!(display.contains("invalid JSON"));
101    }
102
103    #[test]
104    fn test_io_error_constructor() {
105        let err = FlakeLockError::io("/path/to/flake.lock", "permission denied");
106        if let FlakeLockError::IoError { path, message } = err {
107            assert_eq!(path, PathBuf::from("/path/to/flake.lock"));
108            assert_eq!(message, "permission denied");
109        } else {
110            panic!("Expected IoError");
111        }
112    }
113
114    #[test]
115    fn test_io_error_display() {
116        let err = FlakeLockError::io("/project/flake.lock", "file not found");
117        let display = format!("{err}");
118        assert!(display.contains("/project/flake.lock"));
119        assert!(display.contains("file not found"));
120    }
121
122    #[test]
123    fn test_strict_violation_constructor() {
124        let inputs = vec!["nixpkgs".to_string(), "home-manager".to_string()];
125        let err = FlakeLockError::strict_violation(inputs);
126
127        if let FlakeLockError::StrictModeViolation { count, inputs } = err {
128            assert_eq!(count, 2);
129            assert!(inputs.contains(&"nixpkgs".to_string()));
130            assert!(inputs.contains(&"home-manager".to_string()));
131        } else {
132            panic!("Expected StrictModeViolation");
133        }
134    }
135
136    #[test]
137    fn test_strict_violation_display() {
138        let inputs = vec!["input1".to_string(), "input2".to_string()];
139        let err = FlakeLockError::strict_violation(inputs);
140        let display = format!("{err}");
141        assert!(display.contains("2 unlocked input(s)"));
142        assert!(display.contains("input1"));
143        assert!(display.contains("input2"));
144    }
145
146    #[test]
147    fn test_missing_lock_file_constructor() {
148        let err = FlakeLockError::missing("/project/flake.lock");
149        if let FlakeLockError::MissingLockFile { path } = err {
150            assert_eq!(path, PathBuf::from("/project/flake.lock"));
151        } else {
152            panic!("Expected MissingLockFile");
153        }
154    }
155
156    #[test]
157    fn test_missing_lock_file_display() {
158        let err = FlakeLockError::missing("/my/project/flake.lock");
159        let display = format!("{err}");
160        assert!(display.contains("No flake.lock file found"));
161        assert!(display.contains("/my/project/flake.lock"));
162    }
163
164    #[test]
165    fn test_error_debug() {
166        let err = FlakeLockError::parse("test error");
167        let debug_str = format!("{err:?}");
168        assert!(debug_str.contains("ParseError"));
169    }
170
171    #[test]
172    fn test_strict_violation_empty_inputs() {
173        let err = FlakeLockError::strict_violation(vec![]);
174        if let FlakeLockError::StrictModeViolation { count, inputs } = err {
175            assert_eq!(count, 0);
176            assert!(inputs.is_empty());
177        } else {
178            panic!("Expected StrictModeViolation");
179        }
180    }
181}