use thiserror::Error;
use tracing_subscriber::EnvFilter;
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitCategory {
Success = 0,
Runtime = 1,
Usage = 2,
AuthRequired = 3,
}
#[derive(Debug, Clone, Error)]
pub enum Diagnostic {
#[error("usage error: {0}")]
Usage(String),
#[error("authentication required: {0}")]
AuthRequired(String),
#[error("not found: {0}")]
NotFound(String),
#[error("server error: {0}")]
ServerError(String),
#[error("network error: {0}")]
Network(String),
#[error("conflict: {0}")]
Conflict(String),
#[error("io error: {0}")]
Io(String),
#[error("internal error: {0}")]
Internal(String),
#[error("not implemented: {0}")]
NotImplemented(String),
}
impl From<std::io::Error> for Diagnostic {
fn from(e: std::io::Error) -> Self {
Self::Internal(format!("io error: {e}"))
}
}
impl Diagnostic {
pub fn not_implemented(msg: impl Into<String>) -> Self {
Self::NotImplemented(msg.into())
}
pub fn exit_category(&self) -> ExitCategory {
match self {
Self::Usage(_) => ExitCategory::Usage,
Self::AuthRequired(_) => ExitCategory::AuthRequired,
Self::NotFound(_)
| Self::ServerError(_)
| Self::Network(_)
| Self::Conflict(_)
| Self::Io(_)
| Self::Internal(_)
| Self::NotImplemented(_) => ExitCategory::Runtime,
}
}
}
pub fn init_tracing(verbose: u8) {
let filter = if let Ok(env) = std::env::var("RUST_LOG") {
EnvFilter::new(env)
} else {
let level = match verbose {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
EnvFilter::new(level)
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.with_target(false)
.compact()
.try_init()
.ok();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn usage_maps_to_exit_code_2() {
let d = Diagnostic::Usage("missing --server".into());
assert_eq!(d.exit_category(), ExitCategory::Usage);
}
#[test]
fn auth_required_maps_to_exit_code_3() {
let d = Diagnostic::AuthRequired("login first".into());
assert_eq!(d.exit_category(), ExitCategory::AuthRequired);
}
#[test]
fn runtime_errors_map_to_exit_code_1() {
for d in [
Diagnostic::NotFound("x".into()),
Diagnostic::ServerError("x".into()),
Diagnostic::Network("x".into()),
Diagnostic::Internal("x".into()),
Diagnostic::Conflict("x".into()),
Diagnostic::Io("x".into()),
Diagnostic::NotImplemented("x".into()),
] {
assert_eq!(
d.exit_category(),
ExitCategory::Runtime,
"{d:?} must map to Runtime"
);
}
}
#[test]
fn not_implemented_maps_to_exit_code_1() {
let d = Diagnostic::not_implemented("vre project list");
assert_eq!(d.exit_category(), ExitCategory::Runtime);
}
#[test]
fn diagnostic_is_clone() {
let original = Diagnostic::AuthRequired("test".into());
let cloned = original.clone();
assert_eq!(format!("{original}"), format!("{cloned}"));
}
#[test]
fn conflict_maps_to_runtime() {
let d = Diagnostic::Conflict("dump already in progress".into());
assert_eq!(d.exit_category(), ExitCategory::Runtime);
}
#[test]
fn io_maps_to_runtime() {
let d = Diagnostic::Io("failed to write /tmp/0001.zip: permission denied".into());
assert_eq!(d.exit_category(), ExitCategory::Runtime);
}
#[test]
fn from_io_error_yields_internal() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let diag = Diagnostic::from(io_err);
assert!(matches!(diag, Diagnostic::Internal(_)));
assert!(diag.to_string().contains("io error"));
}
}