use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum LifecycleError {
#[error("Failed to load configuration from {path}: {source}")]
ConfigLoad {
path: PathBuf,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("Failed to parse TOML configuration at {path}: {source}")]
ConfigParse {
path: PathBuf,
#[source]
source: toml::de::Error,
},
#[error("Phase '{phase}' not found in configuration")]
PhaseNotFound { phase: String },
#[error("Phase '{phase}' has no commands defined")]
NoCommands { phase: String },
#[error("Command failed in phase '{phase}': {command}\n Exit code: {exit_code}\n Stderr: {stderr}")]
CommandFailed {
phase: String,
command: String,
exit_code: i32,
stderr: String,
},
#[error("Failed to spawn command in phase '{phase}': {command}")]
CommandSpawn {
phase: String,
command: String,
#[source]
source: std::io::Error,
},
#[error("Hook recursion detected: phase '{phase}' called recursively through chain: {}", chain.join(" -> "))]
HookRecursion { phase: String, chain: Vec<String> },
#[error("Hook failed for phase '{phase}': {hook_phase}")]
HookFailed {
phase: String,
hook_phase: String,
#[source]
source: Box<LifecycleError>,
},
#[error("Failed to load state from {path}: {source}")]
StateLoad {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to parse state JSON from {path}: {source}")]
StateParse {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("Failed to save state to {path}: {source}")]
StateSave {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Invalid cache path: {path}")]
InvalidCachePath { path: PathBuf },
#[error("Failed to create cache directory for phase '{phase}': {source}")]
CacheCreate {
phase: String,
#[source]
source: std::io::Error,
},
#[error("File I/O error at {path}: {source}")]
FileIo {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to create directory {path}: {source}")]
DirectoryCreate {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to load make.toml from {path}: {source}")]
MakeTomlLoad {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to parse make.toml at {path}: {source}")]
MakeTomlParse {
path: PathBuf,
#[source]
source: toml::de::Error,
},
#[error("Workspace '{workspace}' not found in configuration")]
WorkspaceNotFound { workspace: String },
#[error("Invalid workspace path '{workspace}': {path}")]
WorkspacePath { workspace: String, path: PathBuf },
#[error("Parallel execution failed for workspace '{workspace}': {source}")]
ParallelExecution {
workspace: String,
#[source]
source: Box<LifecycleError>,
},
#[error("Invalid environment variable: {key}={value}")]
InvalidEnv { key: String, value: String },
#[error("Internal error: mutex poisoned in phase '{phase}'")]
MutexPoisoned { phase: String },
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Circular dependency detected in phase dependencies: {phases}")]
DependencyCycle { phases: String },
#[error("{0}")]
Other(String),
}
impl LifecycleError {
pub fn config_load(
path: impl Into<PathBuf>, source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::ConfigLoad {
path: path.into(),
source: Box::new(source),
}
}
pub fn config_parse(path: impl Into<PathBuf>, source: toml::de::Error) -> Self {
Self::ConfigParse {
path: path.into(),
source,
}
}
pub fn phase_not_found(phase: impl Into<String>) -> Self {
Self::PhaseNotFound {
phase: phase.into(),
}
}
pub fn command_failed(
phase: impl Into<String>, command: impl Into<String>, exit_code: i32,
stderr: impl Into<String>,
) -> Self {
Self::CommandFailed {
phase: phase.into(),
command: command.into(),
exit_code,
stderr: stderr.into(),
}
}
pub fn command_spawn(
phase: impl Into<String>, command: impl Into<String>, source: std::io::Error,
) -> Self {
Self::CommandSpawn {
phase: phase.into(),
command: command.into(),
source,
}
}
pub fn hook_recursion_with_chain(phase: impl Into<String>, chain: Vec<String>) -> Self {
Self::HookRecursion {
phase: phase.into(),
chain,
}
}
pub fn hook_recursion(phase: impl Into<String>) -> Self {
let phase_str = phase.into();
Self::HookRecursion {
phase: phase_str.clone(),
chain: vec![phase_str],
}
}
pub fn state_load(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
Self::StateLoad {
path: path.into(),
source,
}
}
pub fn state_parse(path: impl Into<PathBuf>, source: serde_json::Error) -> Self {
Self::StateParse {
path: path.into(),
source,
}
}
pub fn state_save(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
Self::StateSave {
path: path.into(),
source,
}
}
pub fn invalid_cache_path(path: impl Into<PathBuf>) -> Self {
Self::InvalidCachePath { path: path.into() }
}
pub fn cache_create(phase: impl Into<String>, source: std::io::Error) -> Self {
Self::CacheCreate {
phase: phase.into(),
source,
}
}
pub fn file_io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
Self::FileIo {
path: path.into(),
source,
}
}
pub fn directory_create(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
Self::DirectoryCreate {
path: path.into(),
source,
}
}
pub fn make_toml_load(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
Self::MakeTomlLoad {
path: path.into(),
source,
}
}
pub fn make_toml_parse(path: impl Into<PathBuf>, source: toml::de::Error) -> Self {
Self::MakeTomlParse {
path: path.into(),
source,
}
}
pub fn parallel_execution(workspace: impl Into<String>, source: LifecycleError) -> Self {
Self::ParallelExecution {
workspace: workspace.into(),
source: Box::new(source),
}
}
pub fn dependency_cycle(phases: impl Into<String>) -> Self {
Self::DependencyCycle {
phases: phases.into(),
}
}
}
pub type Result<T> = std::result::Result<T, LifecycleError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_phase_not_found_error() {
let err = LifecycleError::phase_not_found("build");
assert_eq!(err.to_string(), "Phase 'build' not found in configuration");
}
#[test]
fn test_command_failed_error() {
let err = LifecycleError::command_failed("test", "cargo test", 101, "test failed");
let msg = err.to_string();
assert!(msg.contains("Command failed"));
assert!(msg.contains("Exit code: 101"));
assert!(msg.contains("test failed"));
}
#[test]
fn test_hook_recursion_error() {
let err = LifecycleError::hook_recursion("build");
let msg = err.to_string();
assert!(msg.contains("Hook recursion detected"));
assert!(msg.contains("build"));
}
#[test]
fn test_hook_recursion_with_chain() {
let chain = vec![
"init".to_string(),
"setup".to_string(),
"build".to_string(),
"init".to_string(),
];
let err = LifecycleError::hook_recursion_with_chain("init", chain);
let msg = err.to_string();
assert!(msg.contains("init -> setup -> build -> init"));
}
#[test]
fn test_state_errors() {
let path = PathBuf::from("/tmp/state.json");
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let err = LifecycleError::state_load(&path, io_err);
assert!(err.to_string().contains("Failed to load state"));
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
let err = LifecycleError::state_save(&path, io_err);
assert!(err.to_string().contains("Failed to save state"));
}
#[test]
fn test_make_toml_errors() {
let path = PathBuf::from("/tmp/make.toml");
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let err = LifecycleError::make_toml_load(&path, io_err);
assert!(err.to_string().contains("Failed to load make.toml"));
assert!(err.to_string().contains("/tmp/make.toml"));
}
#[test]
fn test_cache_errors() {
let path = PathBuf::from("/tmp/cache");
let err = LifecycleError::invalid_cache_path(&path);
assert!(err.to_string().contains("Invalid cache path"));
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
let err = LifecycleError::cache_create("build", io_err);
assert!(err.to_string().contains("Failed to create cache directory"));
assert!(err.to_string().contains("build"));
}
#[test]
fn test_error_downcasting() {
let err = LifecycleError::phase_not_found("build");
let err_ref: &dyn std::error::Error = &err;
assert!(err_ref.source().is_none());
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let err = LifecycleError::state_load("/tmp/state.json", io_err);
let err_ref: &dyn std::error::Error = &err;
assert!(err_ref.source().is_some());
}
#[test]
fn test_error_display() {
let lifecycle_err = LifecycleError::phase_not_found("test");
let err_str = lifecycle_err.to_string();
assert!(err_str.contains("Phase 'test' not found"));
}
}