use colored::Colorize;
use std::fmt;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CcpmError {
#[error("Git operation failed: {operation}")]
GitCommandError {
operation: String,
stderr: String,
},
#[error("Git is not installed or not found in PATH")]
GitNotFound,
#[error("Not a valid git repository: {path}")]
GitRepoInvalid {
path: String,
},
#[error("Git authentication failed for repository: {url}")]
GitAuthenticationFailed {
url: String,
},
#[error("Failed to clone repository: {url}")]
GitCloneFailed {
url: String,
reason: String,
},
#[error("Failed to checkout reference '{reference}' in repository")]
GitCheckoutFailed {
reference: String,
reason: String,
},
#[error("Configuration error: {message}")]
ConfigError {
message: String,
},
#[error("Manifest file ccpm.toml not found in current directory or any parent directory")]
ManifestNotFound,
#[error("Invalid manifest file syntax in {file}")]
ManifestParseError {
file: String,
reason: String,
},
#[error("Manifest validation failed: {reason}")]
ManifestValidationError {
reason: String,
},
#[error("Invalid lockfile syntax in {file}")]
LockfileParseError {
file: String,
reason: String,
},
#[error("Resource '{name}' not found")]
ResourceNotFound {
name: String,
},
#[error("Resource file '{path}' not found in source '{source_name}'")]
ResourceFileNotFound {
path: String,
source_name: String,
},
#[error("Source repository '{name}' not defined in manifest")]
SourceNotFound {
name: String,
},
#[error("Cannot reach source repository '{name}' at {url}")]
SourceUnreachable {
name: String,
url: String,
},
#[error("Invalid version constraint: {constraint}")]
InvalidVersionConstraint {
constraint: String,
},
#[error("Version '{version}' not found for resource '{resource}'")]
VersionNotFound {
resource: String,
version: String,
},
#[error("Resource '{name}' is already installed")]
AlreadyInstalled {
name: String,
},
#[error("Invalid resource type: {resource_type}")]
InvalidResourceType {
resource_type: String,
},
#[error("Invalid resource structure in '{file}': {reason}")]
InvalidResourceStructure {
file: String,
reason: String,
},
#[error("Circular dependency detected: {chain}")]
CircularDependency {
chain: String,
},
#[error("Cannot resolve dependencies: {reason}")]
DependencyResolutionFailed {
reason: String,
},
#[error("Network error: {operation}")]
NetworkError {
operation: String,
reason: String,
},
#[error("File system error: {operation}")]
FileSystemError {
operation: String,
path: String,
},
#[error("Permission denied: {operation}")]
PermissionDenied {
operation: String,
path: String,
},
#[error("Directory is not empty: {path}")]
DirectoryNotEmpty {
path: String,
},
#[error("Invalid dependency specification for '{name}': {reason}")]
InvalidDependency {
name: String,
reason: String,
},
#[error("Invalid resource content in '{name}': {reason}")]
InvalidResource {
name: String,
reason: String,
},
#[error("Dependency '{name}' requires version {required}, but {found} was found")]
DependencyNotMet {
name: String,
required: String,
found: String,
},
#[error("Configuration file not found: {path}")]
ConfigNotFound {
path: String,
},
#[error("Checksum mismatch for resource '{name}': expected {expected}, got {actual}")]
ChecksumMismatch {
name: String,
expected: String,
actual: String,
},
#[error("Operation not supported on this platform: {operation}")]
PlatformNotSupported {
operation: String,
},
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("TOML parsing error: {0}")]
TomlError(#[from] toml::de::Error),
#[error("TOML serialization error: {0}")]
TomlSerError(#[from] toml::ser::Error),
#[error("Semver parsing error: {0}")]
SemverError(#[from] semver::Error),
#[error("{message}")]
Other {
message: String,
},
}
impl Clone for CcpmError {
fn clone(&self) -> Self {
match self {
CcpmError::GitCommandError { operation, stderr } => CcpmError::GitCommandError {
operation: operation.clone(),
stderr: stderr.clone(),
},
CcpmError::GitNotFound => CcpmError::GitNotFound,
CcpmError::GitRepoInvalid { path } => CcpmError::GitRepoInvalid { path: path.clone() },
CcpmError::GitAuthenticationFailed { url } => {
CcpmError::GitAuthenticationFailed { url: url.clone() }
}
CcpmError::GitCloneFailed { url, reason } => CcpmError::GitCloneFailed {
url: url.clone(),
reason: reason.clone(),
},
CcpmError::GitCheckoutFailed { reference, reason } => CcpmError::GitCheckoutFailed {
reference: reference.clone(),
reason: reason.clone(),
},
CcpmError::ConfigError { message } => CcpmError::ConfigError {
message: message.clone(),
},
CcpmError::ManifestNotFound => CcpmError::ManifestNotFound,
CcpmError::ManifestParseError { file, reason } => CcpmError::ManifestParseError {
file: file.clone(),
reason: reason.clone(),
},
CcpmError::ManifestValidationError { reason } => CcpmError::ManifestValidationError {
reason: reason.clone(),
},
CcpmError::LockfileParseError { file, reason } => CcpmError::LockfileParseError {
file: file.clone(),
reason: reason.clone(),
},
CcpmError::ResourceNotFound { name } => {
CcpmError::ResourceNotFound { name: name.clone() }
}
CcpmError::ResourceFileNotFound { path, source_name } => {
CcpmError::ResourceFileNotFound {
path: path.clone(),
source_name: source_name.clone(),
}
}
CcpmError::SourceNotFound { name } => CcpmError::SourceNotFound { name: name.clone() },
CcpmError::SourceUnreachable { name, url } => CcpmError::SourceUnreachable {
name: name.clone(),
url: url.clone(),
},
CcpmError::InvalidVersionConstraint { constraint } => {
CcpmError::InvalidVersionConstraint {
constraint: constraint.clone(),
}
}
CcpmError::VersionNotFound { resource, version } => CcpmError::VersionNotFound {
resource: resource.clone(),
version: version.clone(),
},
CcpmError::AlreadyInstalled { name } => {
CcpmError::AlreadyInstalled { name: name.clone() }
}
CcpmError::InvalidResourceType { resource_type } => CcpmError::InvalidResourceType {
resource_type: resource_type.clone(),
},
CcpmError::InvalidResourceStructure { file, reason } => {
CcpmError::InvalidResourceStructure {
file: file.clone(),
reason: reason.clone(),
}
}
CcpmError::CircularDependency { chain } => CcpmError::CircularDependency {
chain: chain.clone(),
},
CcpmError::DependencyResolutionFailed { reason } => {
CcpmError::DependencyResolutionFailed {
reason: reason.clone(),
}
}
CcpmError::NetworkError { operation, reason } => CcpmError::NetworkError {
operation: operation.clone(),
reason: reason.clone(),
},
CcpmError::FileSystemError { operation, path } => CcpmError::FileSystemError {
operation: operation.clone(),
path: path.clone(),
},
CcpmError::PermissionDenied { operation, path } => CcpmError::PermissionDenied {
operation: operation.clone(),
path: path.clone(),
},
CcpmError::DirectoryNotEmpty { path } => {
CcpmError::DirectoryNotEmpty { path: path.clone() }
}
CcpmError::InvalidDependency { name, reason } => CcpmError::InvalidDependency {
name: name.clone(),
reason: reason.clone(),
},
CcpmError::InvalidResource { name, reason } => CcpmError::InvalidResource {
name: name.clone(),
reason: reason.clone(),
},
CcpmError::DependencyNotMet {
name,
required,
found,
} => CcpmError::DependencyNotMet {
name: name.clone(),
required: required.clone(),
found: found.clone(),
},
CcpmError::ConfigNotFound { path } => CcpmError::ConfigNotFound { path: path.clone() },
CcpmError::ChecksumMismatch {
name,
expected,
actual,
} => CcpmError::ChecksumMismatch {
name: name.clone(),
expected: expected.clone(),
actual: actual.clone(),
},
CcpmError::PlatformNotSupported { operation } => CcpmError::PlatformNotSupported {
operation: operation.clone(),
},
CcpmError::IoError(e) => CcpmError::Other {
message: format!("IO error: {e}"),
},
CcpmError::TomlError(e) => CcpmError::Other {
message: format!("TOML parsing error: {e}"),
},
CcpmError::TomlSerError(e) => CcpmError::Other {
message: format!("TOML serialization error: {e}"),
},
CcpmError::SemverError(e) => CcpmError::Other {
message: format!("Semver parsing error: {e}"),
},
CcpmError::Other { message } => CcpmError::Other {
message: message.clone(),
},
}
}
}
#[derive(Debug)]
pub struct ErrorContext {
pub error: CcpmError,
pub suggestion: Option<String>,
pub details: Option<String>,
}
impl ErrorContext {
#[must_use]
pub fn new(error: CcpmError) -> Self {
Self {
error,
suggestion: None,
details: None,
}
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn with_details(mut self, details: impl Into<String>) -> Self {
self.details = Some(details.into());
self
}
pub fn display(&self) {
eprintln!("{}: {}", "error".red().bold(), self.error);
if let Some(details) = &self.details {
eprintln!("{}: {}", "details".yellow(), details);
}
if let Some(suggestion) = &self.suggestion {
eprintln!("{}: {}", "suggestion".green(), suggestion);
}
}
}
impl fmt::Display for ErrorContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)?;
if let Some(details) = &self.details {
write!(f, "\nDetails: {details}")?;
}
if let Some(suggestion) = &self.suggestion {
write!(f, "\nSuggestion: {suggestion}")?;
}
Ok(())
}
}
impl std::error::Error for ErrorContext {}
pub trait IntoAnyhowWithContext {
fn into_anyhow_with_context(self, context: ErrorContext) -> anyhow::Error;
}
impl IntoAnyhowWithContext for CcpmError {
fn into_anyhow_with_context(self, context: ErrorContext) -> anyhow::Error {
anyhow::Error::new(ErrorContext {
error: self,
suggestion: context.suggestion,
details: context.details,
})
}
}
impl ErrorContext {
pub fn suggestion(suggestion: impl Into<String>) -> Self {
Self {
error: CcpmError::Other {
message: String::new(),
},
suggestion: Some(suggestion.into()),
details: None,
}
}
}
#[must_use]
pub fn user_friendly_error(error: anyhow::Error) -> ErrorContext {
if let Some(ccmp_error) = error.downcast_ref::<CcpmError>() {
return create_error_context(ccmp_error.clone());
}
if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
match io_error.kind() {
std::io::ErrorKind::PermissionDenied => {
return ErrorContext::new(CcpmError::PermissionDenied {
operation: "file access".to_string(),
path: "unknown".to_string(),
})
.with_suggestion(
"Try running with elevated permissions (sudo/Administrator) or check file ownership",
)
.with_details("This error occurs when CCPM doesn't have permission to read or write files");
}
std::io::ErrorKind::NotFound => {
return ErrorContext::new(CcpmError::FileSystemError {
operation: "file access".to_string(),
path: "unknown".to_string(),
})
.with_suggestion("Check that the file or directory exists and the path is correct")
.with_details(
"This error occurs when a required file or directory cannot be found",
);
}
std::io::ErrorKind::AlreadyExists => {
return ErrorContext::new(CcpmError::FileSystemError {
operation: "file creation".to_string(),
path: "unknown".to_string(),
})
.with_suggestion("Remove the existing file or use --force to overwrite")
.with_details("The target file or directory already exists");
}
std::io::ErrorKind::InvalidData => {
return ErrorContext::new(CcpmError::InvalidResource {
name: "unknown".to_string(),
reason: "invalid file format".to_string(),
})
.with_suggestion("Check the file format and ensure it's a valid resource file")
.with_details("The file contains invalid or corrupted data");
}
_ => {}
}
}
if let Some(toml_error) = error.downcast_ref::<toml::de::Error>() {
return ErrorContext::new(CcpmError::ManifestParseError {
file: "ccpm.toml".to_string(),
reason: toml_error.to_string(),
})
.with_suggestion("Check the TOML syntax in your ccpm.toml file. Verify quotes, brackets, and indentation")
.with_details("TOML parsing errors are usually caused by syntax issues like missing quotes or mismatched brackets");
}
ErrorContext::new(CcpmError::Other {
message: error.to_string(),
})
}
fn create_error_context(error: CcpmError) -> ErrorContext {
match &error {
CcpmError::GitNotFound => ErrorContext::new(CcpmError::GitNotFound)
.with_suggestion("Install git from https://git-scm.com/ or your package manager (e.g., 'brew install git', 'apt install git')")
.with_details("CCPM requires git to be installed and available in your PATH to manage repositories"),
CcpmError::GitCommandError { operation, stderr } => {
ErrorContext::new(CcpmError::GitCommandError {
operation: operation.clone(),
stderr: stderr.clone(),
})
.with_suggestion(match operation.as_str() {
op if op.contains("clone") => "Check the repository URL and your internet connection. Verify you have access to the repository",
op if op.contains("fetch") => "Check your internet connection and repository access. Try 'git fetch' manually in the repository directory",
op if op.contains("checkout") => "Verify the branch, tag, or commit exists. Use 'git tag -l' or 'git branch -r' to list available references",
op if op.contains("worktree") => {
if stderr.contains("invalid reference")
|| stderr.contains("not a valid object name")
|| stderr.contains("pathspec")
|| stderr.contains("did not match")
|| stderr.contains("unknown revision") {
"Invalid version: The specified version/tag/branch does not exist in the repository. Check available versions with 'git tag -l' or 'git branch -r'"
} else {
"Failed to create worktree. Check that the reference exists and the repository is valid"
}
},
_ => "Check your git configuration and repository access. Try running the git command manually for more details",
})
.with_details(if operation.contains("worktree") && (stderr.contains("invalid reference") || stderr.contains("not a valid object name") || stderr.contains("pathspec") || stderr.contains("did not match") || stderr.contains("unknown revision")) {
"Invalid version specification: Failed to checkout reference - the specified version/tag/branch does not exist"
} else {
"Git operations failed. This is often due to network issues, authentication problems, or invalid references"
})
}
CcpmError::GitAuthenticationFailed { url } => ErrorContext::new(CcpmError::GitAuthenticationFailed {
url: url.clone(),
})
.with_suggestion("Configure git authentication: use 'git config --global user.name' and 'git config --global user.email', or set up SSH keys")
.with_details("Authentication is required for private repositories. You may need to log in with 'git credential-manager-core' or similar"),
CcpmError::GitCloneFailed { url, reason } => ErrorContext::new(CcpmError::GitCloneFailed {
url: url.clone(),
reason: reason.clone(),
})
.with_suggestion(format!(
"Verify the repository URL is correct: {url}. Check your internet connection and repository access"
))
.with_details("Clone operations can fail due to invalid URLs, network issues, or access restrictions"),
CcpmError::ManifestNotFound => ErrorContext::new(CcpmError::ManifestNotFound)
.with_suggestion("Create a ccpm.toml file in your project directory. See documentation for the manifest format")
.with_details("CCPM looks for ccpm.toml in the current directory and parent directories up to the filesystem root"),
CcpmError::ManifestParseError { file, reason } => ErrorContext::new(CcpmError::ManifestParseError {
file: file.clone(),
reason: reason.clone(),
})
.with_suggestion(format!(
"Check the TOML syntax in {file}. Common issues: missing quotes, unmatched brackets, invalid characters"
))
.with_details("Use a TOML validator or check the ccpm documentation for correct manifest format"),
CcpmError::SourceNotFound { name } => ErrorContext::new(CcpmError::SourceNotFound {
name: name.clone(),
})
.with_suggestion(format!(
"Add source '{name}' to the [sources] section in ccpm.toml with the repository URL"
))
.with_details("All dependencies must reference a source defined in the [sources] section"),
CcpmError::ResourceFileNotFound { path, source_name } => ErrorContext::new(CcpmError::ResourceFileNotFound {
path: path.clone(),
source_name: source_name.clone(),
})
.with_suggestion(format!(
"Verify the file '{path}' exists in the '{source_name}' repository at the specified version/commit"
))
.with_details("The resource file may have been moved, renamed, or deleted in the repository"),
CcpmError::VersionNotFound { resource, version } => ErrorContext::new(CcpmError::VersionNotFound {
resource: resource.clone(),
version: version.clone(),
})
.with_suggestion(format!(
"Check available versions for '{resource}' using 'git tag -l' in the repository, or use 'main' or 'master' branch"
))
.with_details(format!(
"The version '{version}' doesn't exist as a git tag, branch, or commit in the repository"
)),
CcpmError::CircularDependency { chain } => ErrorContext::new(CcpmError::CircularDependency {
chain: chain.clone(),
})
.with_suggestion("Review your dependency graph and remove circular references")
.with_details(format!(
"Circular dependency chain detected: {chain}. Dependencies cannot depend on themselves directly or indirectly"
)),
CcpmError::PermissionDenied { operation, path } => ErrorContext::new(CcpmError::PermissionDenied {
operation: operation.clone(),
path: path.clone(),
})
.with_suggestion(match cfg!(windows) {
true => "Run as Administrator or check file permissions in File Explorer",
false => "Use 'sudo' or check file permissions with 'ls -la'",
})
.with_details(format!(
"Cannot {operation} due to insufficient permissions on {path}"
)),
CcpmError::ChecksumMismatch { name, expected, actual } => ErrorContext::new(CcpmError::ChecksumMismatch {
name: name.clone(),
expected: expected.clone(),
actual: actual.clone(),
})
.with_suggestion("The file may have been corrupted or modified. Try reinstalling with --force")
.with_details(format!(
"Resource '{name}' has checksum {actual} but expected {expected}. This indicates file corruption or tampering"
)),
_ => ErrorContext::new(error.clone()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let error = CcpmError::GitNotFound;
assert_eq!(
error.to_string(),
"Git is not installed or not found in PATH"
);
let error = CcpmError::ResourceNotFound {
name: "test".to_string(),
};
assert_eq!(error.to_string(), "Resource 'test' not found");
let error = CcpmError::InvalidVersionConstraint {
constraint: "bad-version".to_string(),
};
assert_eq!(error.to_string(), "Invalid version constraint: bad-version");
let error = CcpmError::GitCommandError {
operation: "clone".to_string(),
stderr: "repository not found".to_string(),
};
assert_eq!(error.to_string(), "Git operation failed: clone");
}
#[test]
fn test_error_context() {
let ctx = ErrorContext::new(CcpmError::GitNotFound)
.with_suggestion("Install git using your package manager")
.with_details("Git is required for CCPM to function");
assert_eq!(
ctx.suggestion,
Some("Install git using your package manager".to_string())
);
assert_eq!(
ctx.details,
Some("Git is required for CCPM to function".to_string())
);
}
#[test]
fn test_error_context_display() {
let ctx = ErrorContext::new(CcpmError::GitNotFound).with_suggestion("Install git");
let display = format!("{ctx}");
assert!(display.contains("Git is not installed or not found in PATH"));
assert!(display.contains("Install git"));
}
#[test]
fn test_user_friendly_error_permission_denied() {
use std::io::{Error, ErrorKind};
let io_error = Error::new(ErrorKind::PermissionDenied, "access denied");
let anyhow_error = anyhow::Error::from(io_error);
let ctx = user_friendly_error(anyhow_error);
match ctx.error {
CcpmError::PermissionDenied { .. } => {}
_ => panic!("Expected PermissionDenied error"),
}
assert!(ctx.suggestion.is_some());
assert!(ctx.details.is_some());
}
#[test]
fn test_user_friendly_error_not_found() {
use std::io::{Error, ErrorKind};
let io_error = Error::new(ErrorKind::NotFound, "file not found");
let anyhow_error = anyhow::Error::from(io_error);
let ctx = user_friendly_error(anyhow_error);
match ctx.error {
CcpmError::FileSystemError { .. } => {}
_ => panic!("Expected FileSystemError"),
}
assert!(ctx.suggestion.is_some());
assert!(ctx.details.is_some());
}
#[test]
fn test_from_io_error() {
use std::io::Error;
let io_error = Error::other("test error");
let ccpm_error = CcpmError::from(io_error);
match ccpm_error {
CcpmError::IoError(_) => {}
_ => panic!("Expected IoError"),
}
}
#[test]
fn test_from_toml_error() {
let toml_str = "invalid = toml {";
let result: Result<toml::Value, _> = toml::from_str(toml_str);
if let Err(e) = result {
let ccpm_error = CcpmError::from(e);
match ccpm_error {
CcpmError::TomlError(_) => {}
_ => panic!("Expected TomlError"),
}
}
}
#[test]
fn test_create_error_context_git_not_found() {
let ctx = create_error_context(CcpmError::GitNotFound);
assert!(ctx.suggestion.is_some());
assert!(ctx.suggestion.unwrap().contains("Install git"));
assert!(ctx.details.is_some());
}
#[test]
fn test_create_error_context_git_command_error() {
let ctx = create_error_context(CcpmError::GitCommandError {
operation: "clone".to_string(),
stderr: "error".to_string(),
});
assert!(ctx.suggestion.is_some());
assert!(ctx.suggestion.unwrap().contains("repository URL"));
assert!(ctx.details.is_some());
}
#[test]
fn test_create_error_context_git_auth_failed() {
let ctx = create_error_context(CcpmError::GitAuthenticationFailed {
url: "https://github.com/test/repo".to_string(),
});
assert!(ctx.suggestion.is_some());
assert!(
ctx.suggestion
.unwrap()
.contains("Configure git authentication")
);
assert!(ctx.details.is_some());
}
#[test]
fn test_create_error_context_manifest_not_found() {
let ctx = create_error_context(CcpmError::ManifestNotFound);
assert!(ctx.suggestion.is_some());
assert!(ctx.suggestion.unwrap().contains("Create a ccpm.toml"));
assert!(ctx.details.is_some());
}
#[test]
fn test_create_error_context_source_not_found() {
let ctx = create_error_context(CcpmError::SourceNotFound {
name: "test-source".to_string(),
});
assert!(ctx.suggestion.is_some());
assert!(ctx.suggestion.unwrap().contains("test-source"));
assert!(ctx.details.is_some());
}
#[test]
fn test_create_error_context_version_not_found() {
let ctx = create_error_context(CcpmError::VersionNotFound {
resource: "test-resource".to_string(),
version: "v1.0.0".to_string(),
});
assert!(ctx.suggestion.is_some());
assert!(ctx.suggestion.unwrap().contains("test-resource"));
assert!(ctx.details.is_some());
assert!(ctx.details.unwrap().contains("v1.0.0"));
}
#[test]
fn test_create_error_context_circular_dependency() {
let ctx = create_error_context(CcpmError::CircularDependency {
chain: "a -> b -> c -> a".to_string(),
});
assert!(ctx.suggestion.is_some());
assert!(ctx.suggestion.unwrap().contains("remove circular"));
assert!(ctx.details.is_some());
assert!(ctx.details.unwrap().contains("a -> b -> c -> a"));
}
#[test]
fn test_create_error_context_permission_denied() {
let ctx = create_error_context(CcpmError::PermissionDenied {
operation: "write".to_string(),
path: "/test/path".to_string(),
});
assert!(ctx.suggestion.is_some());
assert!(ctx.details.is_some());
assert!(ctx.details.unwrap().contains("/test/path"));
}
#[test]
fn test_create_error_context_checksum_mismatch() {
let ctx = create_error_context(CcpmError::ChecksumMismatch {
name: "test-resource".to_string(),
expected: "abc123".to_string(),
actual: "def456".to_string(),
});
assert!(ctx.suggestion.is_some());
assert!(ctx.suggestion.unwrap().contains("reinstalling"));
assert!(ctx.details.is_some());
assert!(ctx.details.unwrap().contains("abc123"));
}
#[test]
fn test_error_clone() {
let error1 = CcpmError::GitNotFound;
let error2 = error1.clone();
assert_eq!(error1.to_string(), error2.to_string());
let error1 = CcpmError::ResourceNotFound {
name: "test".to_string(),
};
let error2 = error1.clone();
assert_eq!(error1.to_string(), error2.to_string());
}
#[test]
fn test_error_context_suggestion() {
let ctx = ErrorContext::suggestion("Test suggestion");
assert_eq!(ctx.suggestion, Some("Test suggestion".to_string()));
assert!(ctx.details.is_none());
}
#[test]
fn test_into_anyhow_with_context() {
let error = CcpmError::GitNotFound;
let context = ErrorContext::new(CcpmError::Other {
message: "dummy".to_string(),
})
.with_suggestion("Test suggestion")
.with_details("Test details");
let anyhow_error = error.into_anyhow_with_context(context);
let display = format!("{anyhow_error}");
assert!(display.contains("Git is not installed"));
}
#[test]
fn test_user_friendly_error_already_exists() {
use std::io::{Error, ErrorKind};
let io_error = Error::new(ErrorKind::AlreadyExists, "file exists");
let anyhow_error = anyhow::Error::from(io_error);
let ctx = user_friendly_error(anyhow_error);
match ctx.error {
CcpmError::FileSystemError { .. } => {}
_ => panic!("Expected FileSystemError"),
}
assert!(ctx.suggestion.is_some());
assert!(ctx.suggestion.unwrap().contains("overwrite"));
}
#[test]
fn test_user_friendly_error_invalid_data() {
use std::io::{Error, ErrorKind};
let io_error = Error::new(ErrorKind::InvalidData, "corrupt data");
let anyhow_error = anyhow::Error::from(io_error);
let ctx = user_friendly_error(anyhow_error);
match ctx.error {
CcpmError::InvalidResource { .. } => {}
_ => panic!("Expected InvalidResource"),
}
assert!(ctx.suggestion.is_some());
assert!(ctx.details.is_some());
}
#[test]
fn test_user_friendly_error_ccpm_error() {
let error = CcpmError::GitNotFound;
let anyhow_error = anyhow::Error::from(error);
let ctx = user_friendly_error(anyhow_error);
match ctx.error {
CcpmError::GitNotFound => {}
_ => panic!("Expected GitNotFound"),
}
assert!(ctx.suggestion.is_some());
}
#[test]
fn test_user_friendly_error_toml_parse() {
let toml_str = "invalid = toml {";
let result: Result<toml::Value, _> = toml::from_str(toml_str);
if let Err(e) = result {
let anyhow_error = anyhow::Error::from(e);
let ctx = user_friendly_error(anyhow_error);
match ctx.error {
CcpmError::ManifestParseError { .. } => {}
_ => panic!("Expected ManifestParseError"),
}
assert!(ctx.suggestion.is_some());
assert!(ctx.suggestion.unwrap().contains("TOML syntax"));
}
}
#[test]
fn test_user_friendly_error_generic() {
let error = anyhow::anyhow!("Generic error");
let ctx = user_friendly_error(error);
match ctx.error {
CcpmError::Other { message } => {
assert_eq!(message, "Generic error");
}
_ => panic!("Expected Other error"),
}
}
#[test]
fn test_from_semver_error() {
let result = semver::Version::parse("invalid-version");
if let Err(e) = result {
let ccpm_error = CcpmError::from(e);
match ccpm_error {
CcpmError::SemverError(_) => {}
_ => panic!("Expected SemverError"),
}
}
}
#[test]
fn test_error_display_all_variants() {
let errors = vec![
CcpmError::GitRepoInvalid {
path: "/test/path".to_string(),
},
CcpmError::GitCheckoutFailed {
reference: "main".to_string(),
reason: "not found".to_string(),
},
CcpmError::ConfigError {
message: "config issue".to_string(),
},
CcpmError::ManifestValidationError {
reason: "invalid format".to_string(),
},
CcpmError::LockfileParseError {
file: "ccpm.lock".to_string(),
reason: "syntax error".to_string(),
},
CcpmError::ResourceFileNotFound {
path: "test.md".to_string(),
source_name: "source".to_string(),
},
CcpmError::DirectoryNotEmpty {
path: "/some/dir".to_string(),
},
CcpmError::InvalidDependency {
name: "dep".to_string(),
reason: "bad format".to_string(),
},
CcpmError::DependencyNotMet {
name: "dep".to_string(),
required: "v1.0".to_string(),
found: "v2.0".to_string(),
},
CcpmError::ConfigNotFound {
path: "/config/path".to_string(),
},
CcpmError::PlatformNotSupported {
operation: "test op".to_string(),
},
];
for error in errors {
let display = format!("{error}");
assert!(!display.is_empty());
}
}
#[test]
fn test_create_error_context_git_operations() {
let operations = vec![
("fetch", "internet connection"),
("checkout", "branch, tag"),
("pull", "git configuration"),
];
for (op, expected_text) in operations {
let ctx = create_error_context(CcpmError::GitCommandError {
operation: op.to_string(),
stderr: "error".to_string(),
});
assert!(ctx.suggestion.is_some());
assert!(
ctx.suggestion
.unwrap()
.to_lowercase()
.contains(expected_text)
);
}
}
#[test]
fn test_create_error_context_resource_file_not_found() {
let ctx = create_error_context(CcpmError::ResourceFileNotFound {
path: "agents/test.md".to_string(),
source_name: "official".to_string(),
});
assert!(ctx.suggestion.is_some());
let suggestion = ctx.suggestion.unwrap();
assert!(suggestion.contains("agents/test.md"));
assert!(suggestion.contains("official"));
assert!(ctx.details.is_some());
}
#[test]
fn test_create_error_context_manifest_parse_error() {
let ctx = create_error_context(CcpmError::ManifestParseError {
file: "custom.toml".to_string(),
reason: "invalid syntax".to_string(),
});
assert!(ctx.suggestion.is_some());
let suggestion = ctx.suggestion.unwrap();
assert!(suggestion.contains("custom.toml"));
assert!(ctx.details.is_some());
}
#[test]
fn test_create_error_context_git_clone_failed() {
let ctx = create_error_context(CcpmError::GitCloneFailed {
url: "https://example.com/repo.git".to_string(),
reason: "network error".to_string(),
});
assert!(ctx.suggestion.is_some());
let suggestion = ctx.suggestion.unwrap();
assert!(suggestion.contains("https://example.com/repo.git"));
assert!(ctx.details.is_some());
}
}