use std::path::PathBuf;
use smol_str::SmolStr;
use crate::output::OutputError;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to open the database file {0:?}: {1}")]
OpenDb(PathBuf, #[source] std::io::Error),
#[error("Failed to parse the database from file {0:?}: {1}")]
ParseDb(PathBuf, #[source] toml::de::Error),
#[error("Failed to execute command")]
Shell(#[from] xshell::Error),
#[error(transparent)]
ConfigFiles(#[from] crate::config_files::ConfigFilesError),
#[error(transparent)]
SafeRelativePath(#[from] zenops_safe_relative_path::error::Error),
#[error(
"Package {pkg} references undefined input {input}; mark the action optional or set [pkg.{pkg}.inputs].{input}"
)]
UnresolvedInput {
pkg: SmolStr,
input: SmolStr,
},
#[error("Package {pkg} has an unterminated `${{` in a template")]
TemplateUnterminated {
pkg: SmolStr,
},
#[error(
"apply requires a terminal for prompts; pass --yes to apply all changes non-interactively, or --dry-run to preview"
)]
ApplyNeedsYesOrTty,
#[error(
"zenops config repo at {0:?} has uncommitted changes. Commit them first, or re-run with --allow-dirty to apply anyway."
)]
DirtyRepoRequiresAllowDirty(PathBuf),
#[error("Failed to read confirmation from stdin: {0}")]
PromptRead(#[source] std::io::Error),
#[error("Interrupted")]
PromptInterrupted,
#[error(transparent)]
Output(#[from] OutputError),
#[error(transparent)]
Init(#[from] crate::init::InitError),
#[error(transparent)]
Ssh(#[from] crate::config::ssh::SshError),
#[error("Failed to emit schema: {0}")]
SchemaEmit(#[source] serde_json::Error),
#[error("Failed to write schema to stdout: {0}")]
SchemaWrite(#[source] std::io::Error),
#[error(transparent)]
PkgError(#[from] crate::config::pkg::Error),
#[error(transparent)]
Which(#[from] crate::utils::which::Error),
#[error("Could not determine the user's home directory")]
NoHomeDir,
#[error("Failed to probe for brew at {0:?}: {1}")]
BrewProbeFailed(PathBuf, #[source] std::io::Error),
}
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::OpenDb(l0, l1), Self::OpenDb(r0, r1)) => l0 == r0 && l1.kind() == r1.kind(),
(Self::ParseDb(l0, l1), Self::ParseDb(r0, r1)) => l0 == r0 && l1 == r1,
(Self::Shell(l0), Self::Shell(r0)) => l0.to_string() == r0.to_string(),
(Self::ConfigFiles(l0), Self::ConfigFiles(r0)) => l0 == r0,
(Self::SafeRelativePath(l0), Self::SafeRelativePath(r0)) => l0 == r0,
(
Self::UnresolvedInput {
pkg: l_pkg,
input: l_input,
},
Self::UnresolvedInput {
pkg: r_pkg,
input: r_input,
},
) => l_pkg == r_pkg && l_input == r_input,
(Self::TemplateUnterminated { pkg: l }, Self::TemplateUnterminated { pkg: r }) => {
l == r
}
(Self::ApplyNeedsYesOrTty, Self::ApplyNeedsYesOrTty) => true,
(Self::DirtyRepoRequiresAllowDirty(l0), Self::DirtyRepoRequiresAllowDirty(r0)) => {
l0 == r0
}
(Self::PromptRead(l0), Self::PromptRead(r0)) => l0.kind() == r0.kind(),
(Self::PromptInterrupted, Self::PromptInterrupted) => true,
(Self::Output(l0), Self::Output(r0)) => l0.to_string() == r0.to_string(),
(Self::Init(l0), Self::Init(r0)) => l0 == r0,
(Self::Ssh(l0), Self::Ssh(r0)) => l0 == r0,
(Self::SchemaEmit(l), Self::SchemaEmit(r)) => l.to_string() == r.to_string(),
(Self::SchemaWrite(l), Self::SchemaWrite(r)) => l.kind() == r.kind(),
(Self::NoHomeDir, Self::NoHomeDir) => true,
(Self::BrewProbeFailed(l0, l1), Self::BrewProbeFailed(r0, r1)) => {
l0 == r0 && l1.kind() == r1.kind()
}
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use similar_asserts::assert_eq;
use xshell::{Shell, cmd};
use zenops_safe_relative_path::srpath;
use crate::config_files::ConfigFilePath;
use crate::output::ResolvedConfigFilePath;
use super::*;
fn rcfp(rel: &'static str) -> ResolvedConfigFilePath {
let path = ConfigFilePath::Home(Arc::from(
zenops_safe_relative_path::SafeRelativePath::from_relative_path(rel).unwrap(),
));
let full = Arc::from(Path::new("/tmp").join(rel).as_path());
ResolvedConfigFilePath { path, full }
}
fn io(kind: io::ErrorKind) -> io::Error {
io::Error::from(kind)
}
fn xshell_err() -> xshell::Error {
let sh = Shell::new().unwrap();
cmd!(sh, "false").quiet().run().unwrap_err()
}
fn json_err() -> serde_json::Error {
serde_json::from_str::<serde_json::Value>("{").unwrap_err()
}
#[test]
fn open_db_eq_compares_path_and_io_kind() {
let a = Error::OpenDb(PathBuf::from("/x"), io(io::ErrorKind::NotFound));
let b = Error::OpenDb(PathBuf::from("/x"), io(io::ErrorKind::NotFound));
let c = Error::OpenDb(PathBuf::from("/y"), io(io::ErrorKind::NotFound));
let d = Error::OpenDb(PathBuf::from("/x"), io(io::ErrorKind::PermissionDenied));
assert_eq!(a, b);
assert_ne!(a, c);
assert_ne!(a, d);
}
#[test]
fn parse_db_eq_compares_path_and_inner_error() {
let toml_err = toml::from_str::<toml::Value>("not = valid = toml").unwrap_err();
let toml_err2 = toml::from_str::<toml::Value>("not = valid = toml").unwrap_err();
let a = Error::ParseDb(PathBuf::from("/x"), toml_err);
let b = Error::ParseDb(PathBuf::from("/x"), toml_err2);
assert_eq!(a, b);
}
#[test]
fn shell_eq_compares_display_string() {
let a = Error::Shell(xshell_err());
let b = Error::Shell(xshell_err());
assert_eq!(a, b);
}
#[test]
fn config_files_wrap_eq_delegates_to_inner() {
let a = Error::ConfigFiles(crate::config_files::ConfigFilesError::FailedToWriteConfig(
rcfp("a"),
io(io::ErrorKind::PermissionDenied),
));
let b = Error::ConfigFiles(crate::config_files::ConfigFilesError::FailedToWriteConfig(
rcfp("a"),
io(io::ErrorKind::PermissionDenied),
));
let c = Error::ConfigFiles(crate::config_files::ConfigFilesError::FailedToWriteConfig(
rcfp("b"),
io(io::ErrorKind::PermissionDenied),
));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn from_config_files_error_wraps_in_config_files_variant() {
let inner =
crate::config_files::ConfigFilesError::RefusingToOverwriteOtherWithSymlink(rcfp("a"));
let e: Error = inner.into();
assert!(matches!(e, Error::ConfigFiles(_)));
}
#[test]
fn safe_relative_path_eq_delegates_to_inner() {
let traversal_err =
zenops_safe_relative_path::SafeRelativePath::from_relative_path("..").unwrap_err();
let traversal_err2 =
zenops_safe_relative_path::SafeRelativePath::from_relative_path("..").unwrap_err();
let a = Error::SafeRelativePath(traversal_err);
let b = Error::SafeRelativePath(traversal_err2);
let c = Error::SafeRelativePath(
zenops_safe_relative_path::error::Error::NotASinglePathComponent("a/b".to_string()),
);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn unresolved_input_eq_and_ne() {
let a = Error::UnresolvedInput {
pkg: SmolStr::new_static("p"),
input: SmolStr::new_static("i"),
};
let b = Error::UnresolvedInput {
pkg: SmolStr::new_static("p"),
input: SmolStr::new_static("i"),
};
let c = Error::UnresolvedInput {
pkg: SmolStr::new_static("p"),
input: SmolStr::new_static("other"),
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn template_unterminated_eq_and_ne() {
let a = Error::TemplateUnterminated {
pkg: SmolStr::new_static("p"),
};
let b = Error::TemplateUnterminated {
pkg: SmolStr::new_static("p"),
};
let c = Error::TemplateUnterminated {
pkg: SmolStr::new_static("q"),
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn unit_variants_compare_equal_to_themselves() {
assert_eq!(Error::ApplyNeedsYesOrTty, Error::ApplyNeedsYesOrTty);
assert_eq!(Error::PromptInterrupted, Error::PromptInterrupted);
assert_eq!(Error::NoHomeDir, Error::NoHomeDir);
}
#[test]
fn brew_probe_failed_eq_and_ne() {
let a = Error::BrewProbeFailed(
PathBuf::from("/opt/homebrew/bin/brew"),
io(io::ErrorKind::PermissionDenied),
);
let b = Error::BrewProbeFailed(
PathBuf::from("/opt/homebrew/bin/brew"),
io(io::ErrorKind::PermissionDenied),
);
let c = Error::BrewProbeFailed(
PathBuf::from("/usr/local/bin/brew"),
io(io::ErrorKind::PermissionDenied),
);
let d = Error::BrewProbeFailed(
PathBuf::from("/opt/homebrew/bin/brew"),
io(io::ErrorKind::NotFound),
);
assert_eq!(a, b);
assert_ne!(a, c);
assert_ne!(a, d);
}
#[test]
fn dirty_repo_requires_allow_dirty_eq_and_ne() {
let a = Error::DirtyRepoRequiresAllowDirty(PathBuf::from("/x"));
let b = Error::DirtyRepoRequiresAllowDirty(PathBuf::from("/x"));
let c = Error::DirtyRepoRequiresAllowDirty(PathBuf::from("/y"));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn prompt_read_eq_compares_io_kind() {
let a = Error::PromptRead(io(io::ErrorKind::UnexpectedEof));
let b = Error::PromptRead(io(io::ErrorKind::UnexpectedEof));
let c = Error::PromptRead(io(io::ErrorKind::Other));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn output_eq_compares_display_string() {
let a = Error::Output(OutputError::Io(io(io::ErrorKind::BrokenPipe)));
let b = Error::Output(OutputError::Io(io(io::ErrorKind::BrokenPipe)));
assert_eq!(a, b);
}
#[test]
fn init_wrap_eq_delegates_to_inner() {
let a = Error::Init(crate::init::InitError::DirNotEmpty(PathBuf::from("/a")));
let b = Error::Init(crate::init::InitError::DirNotEmpty(PathBuf::from("/a")));
let c = Error::Init(crate::init::InitError::DirNotEmpty(PathBuf::from("/b")));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn from_init_error_wraps_in_init_variant() {
let inner = crate::init::InitError::NeedsTty;
let e: Error = inner.into();
assert!(matches!(e, Error::Init(_)));
}
#[test]
fn ssh_wrap_eq_delegates_to_inner() {
let a = Error::Ssh(crate::config::ssh::SshError::CurlNotFound);
let b = Error::Ssh(crate::config::ssh::SshError::CurlNotFound);
assert_eq!(a, b);
}
#[test]
fn from_ssh_error_wraps_in_ssh_variant() {
let inner = crate::config::ssh::SshError::CurlNotFound;
let e: Error = inner.into();
assert!(matches!(e, Error::Ssh(_)));
}
#[test]
fn schema_emit_eq_compares_display_string() {
let a = Error::SchemaEmit(json_err());
let b = Error::SchemaEmit(json_err());
assert_eq!(a, b);
}
#[test]
fn schema_write_eq_compares_io_kind() {
let a = Error::SchemaWrite(io(io::ErrorKind::BrokenPipe));
let b = Error::SchemaWrite(io(io::ErrorKind::BrokenPipe));
let c = Error::SchemaWrite(io(io::ErrorKind::Other));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn cross_variant_compare_returns_false() {
assert_ne!(Error::ApplyNeedsYesOrTty, Error::PromptInterrupted);
assert_ne!(
Error::NoHomeDir,
Error::OpenDb(PathBuf::from("/x"), io(io::ErrorKind::NotFound))
);
let _ = srpath!("dummy"); }
#[test]
fn from_xshell_error_wraps_in_shell_variant() {
let e: Error = xshell_err().into();
assert!(matches!(e, Error::Shell(_)));
}
#[test]
fn from_safe_relative_path_error_wraps() {
let inner = zenops_safe_relative_path::error::Error::NotASinglePathComponent("a/b".into());
let e: Error = inner.into();
assert!(matches!(e, Error::SafeRelativePath(_)));
}
}