Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: [EXPERIMENT] Sketching out a RusticIssueCollector #318

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// use std::error::Error as StdError;
// use std::fmt;

pub mod collector;

use std::{
error::Error,
ffi::OsString,
Expand Down
289 changes: 289 additions & 0 deletions crates/core/src/error/collector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
use derive_more::From;

use crate::{error::RusticErrorKind, RusticError};
use log::{error, info, warn};

/// A rustic issue result
///
/// rustic issue results are used to return a result along with possible issues.
#[derive(Debug)]
pub enum RusticIssueResult<T> {
/// For when we return a result along with possible issues
Ok(T, Option<Vec<RusticIssue>>),

/// For when we abort and return only the errors that occurred
Err(Vec<RusticError>),
}
simonsan marked this conversation as resolved.
Show resolved Hide resolved

/// A rustic issue
///
/// An issue is a message that can be logged to the user.
#[derive(Debug)]
pub enum RusticIssue {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not using something like error: RusticError and severity: RusticSeverity with the latter being an enum? I can imagine we can have the exactly identical error which is at some place classified as warning (or even info) and at another place just an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know, I would not classify an error at one place as a warning and at the other as an info. An error is an error, and should be clearly communicated as one. If we change the general meaning of an error, then it shouldn't be an error in the first place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I do know what you mean, something like: 'File not found'. At one place, it can be irrecoverable, e.g. a config file not found for a backend. At some other place, it maybe can be continued with default settings.

But in that case we should handle that error in-place and not return an error I feel.

Copy link
Contributor Author

@simonsan simonsan Oct 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, important to say here: RusticIssue, RusticWarning, and RusticIssueCollector should never go over the library boundary. So it's for internal use only, it should not be part of our public API, and we should actually not return RusticIssue from any public function - maybe even in general - every function. It should only be used for internal error handling, so we as library authors can judge situations better internally, and give our users errors only in cases that are worthwhile.

Hence, I reduced the visibility of all the types in this file to pub(crate)

/// An error issue, indicating that something went wrong irrecoverably
Error(RusticError),
Comment on lines +16 to +17
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might not have that within this Enum though? 🤔 as that should always be a hard error I think? Not sure though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I do think that we can have Errors on a deeper level which are shown as warnings on a higher level. Something like backup failed for creating the first snapshot, but we want to continue creating the second one...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, we should handle that at the point we get that error and start the backup for the second path for example. The failed backup for the first will be output to the user at the match of the result (e.g. in a loop) and we start the next one, but give user feedback.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think this is the same topic as above, we shouldn't give errors different meanings, I don't know if they are really contextual in that sense. There can be hard and soft errors, for sure. But I think we are going to replace the term soft error here, with an issue which can result in a warning or info. But then we need to reserve the term error for things that are irrecoverable.


/// A warning issue, indicating that something might be wrong
Warning(RusticWarning),

/// An info issue, indicating additional information
Info(RusticInfo),
}

impl RusticIssue {
pub fn new_error(error: RusticErrorKind) -> Self {

Check warning on line 34 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L34

Added line #L34 was not covered by tests
Self::Error(error.into())
}

pub fn new_warning(message: &str) -> Self {
Self::Warning(message.into())
}

pub fn new_info(message: &str) -> Self {
Self::Info(message.into())
}

pub fn log(&self) {
match self {
Self::Error(error) => error!("{}", error),
Self::Warning(warning) => warn!("{}", warning.0),
Self::Info(info) => info!("{}", info.0),
}
}
}

/// A rustic warning message
///
/// Warning messages are used to indicate that something might be wrong.
#[derive(Debug, Clone, From)]
pub struct RusticWarning(String);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like having String here. IMO we should just have RusticError

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, this here is a warning, RusticError should be something that we return to the user, in case we can't recover from something. This is pub here, but I'm not sure if we ever should make RusticWarning, RusticInfo part of our public API. Users of rustic_core, also rustic-rs should never have to deal with that. They will only get errors from us. I think 🤔 If it is recoverable, then we should recover from it, not the users.


impl RusticWarning {
pub fn new(message: &str) -> Self {
Self(message.to_owned())
}
}

impl From<&str> for RusticWarning {
fn from(message: &str) -> Self {

Check warning on line 68 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L68

Added line #L68 was not covered by tests
Self::new(message)
}
}

/// A rustic info message
///
/// Info messages are used to provide additional information to the user.
#[derive(Debug, Clone, From)]
pub struct RusticInfo(String);

impl RusticInfo {
pub fn new(message: &str) -> Self {
Self(message.to_owned())
}
}

impl From<&str> for RusticInfo {
fn from(message: &str) -> Self {

Check warning on line 86 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L86

Added line #L86 was not covered by tests
Self::new(message)
}
}

#[derive(Debug, Default)]
pub struct RusticIssueCollector {
/// The errors collected
errors: Option<Vec<RusticError>>,

/// The warnings collected
warnings: Option<Vec<RusticWarning>>,

/// The info collected
info: Option<Vec<RusticInfo>>,

/// Whether to log items directly during addition
log: bool,
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to adapt it, so we can easier use it from multiple threads, and other contexts, e.g. via Mutexes or Channels.


impl RusticIssueCollector {
pub fn new(log: bool) -> Self {
Self {
errors: None,
warnings: None,
info: None,
log,
}
}

pub fn add(&mut self, issue: RusticIssue) {
match issue {
RusticIssue::Error(error) => self.add_error(error.0),
RusticIssue::Warning(warning) => self.add_warning(&warning.0),
RusticIssue::Info(info) => self.add_info(&info.0),

Check warning on line 120 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L119-L120

Added lines #L119 - L120 were not covered by tests
}
}

pub fn add_error(&mut self, error: RusticErrorKind) {
if self.log {
error!("{error}");

Check warning on line 126 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L126

Added line #L126 was not covered by tests
}

if let Some(errors) = &mut self.errors {
errors.push(error.into());

Check warning on line 130 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L130

Added line #L130 was not covered by tests
} else {
self.errors = Some(vec![error.into()]);
}
}

pub fn add_warning(&mut self, message: &str) {
if self.log {
warn!("{message}");

Check warning on line 138 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L138

Added line #L138 was not covered by tests
}

if let Some(warnings) = &mut self.warnings {
warnings.push(message.to_owned().into());

Check warning on line 142 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L142

Added line #L142 was not covered by tests
} else {
self.warnings = Some(vec![message.to_owned().into()]);
}
}

pub fn add_info(&mut self, message: &str) {
if self.log {
warn!("{message}");

Check warning on line 150 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L150

Added line #L150 was not covered by tests
}

if let Some(info) = &mut self.info {
info.push(message.to_owned().into());

Check warning on line 154 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L154

Added line #L154 was not covered by tests
} else {
self.info = Some(vec![message.to_owned().into()]);
}
}

pub fn has_errors(&self) -> bool {

Check warning on line 160 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L160

Added line #L160 was not covered by tests
self.errors.is_some()
}

pub fn has_warnings(&self) -> bool {

Check warning on line 164 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L164

Added line #L164 was not covered by tests
self.warnings.is_some()
}

pub fn has_info(&self) -> bool {

Check warning on line 168 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L168

Added line #L168 was not covered by tests
self.info.is_some()
}

pub fn get_errors(&self) -> Option<Vec<&RusticError>> {
self.errors.as_ref().map(|errors| errors.iter().collect())

Check warning on line 173 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L172-L173

Added lines #L172 - L173 were not covered by tests
}

pub fn get_warnings(&self) -> Option<Vec<RusticWarning>> {

Check warning on line 176 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L176

Added line #L176 was not covered by tests
self.warnings.clone()
}

pub fn get_info(&self) -> Option<Vec<RusticInfo>> {

Check warning on line 180 in crates/core/src/error/collector.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/error/collector.rs#L180

Added line #L180 was not covered by tests
self.info.clone()
}

pub fn log_all(&self) {
self.log_all_errors();
self.log_all_warnings();
self.log_all_info();
}

pub fn log_all_errors(&self) {
if let Some(errors) = &self.errors {
for error in errors {
error!("{}", error);
}
}
}

pub fn log_all_warnings(&self) {
if let Some(warnings) = &self.warnings {
for warning in warnings {
warn!("{}", warning.0);
}
}
}

pub fn log_all_info(&self) {
if let Some(info) = &self.info {
for info in info {
info!("{}", info.0);
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::error::RusticErrorKind;

#[test]
fn test_add_issue() {
let mut collector = RusticIssueCollector::default();

let issue = RusticIssue::new_error(RusticErrorKind::StdIo(std::io::Error::new(
std::io::ErrorKind::NotFound,
"test",
)));

collector.add(issue);
assert!(collector.has_errors());
assert!(!collector.has_warnings());
assert!(!collector.has_info());
}

#[test]
fn test_add_error() {
let mut collector = RusticIssueCollector::default();
collector.add_error(RusticErrorKind::StdIo(std::io::Error::new(
std::io::ErrorKind::NotFound,
"test",
)));

assert!(collector.has_errors());
assert!(!collector.has_warnings());
assert!(!collector.has_info());
}

#[test]
fn test_add_warning() {
let mut collector = RusticIssueCollector::default();
collector.add_warning("test");
assert!(!collector.has_errors());
assert!(collector.has_warnings());
assert!(!collector.has_info());
}

#[test]
fn test_add_info() {
let mut collector = RusticIssueCollector::default();
collector.add_info("test");
assert!(!collector.has_errors());
assert!(!collector.has_warnings());
assert!(collector.has_info());
}

#[test]
fn test_get_errors() {
let mut collector = RusticIssueCollector::default();
collector.add_error(RusticErrorKind::StdIo(std::io::Error::new(
std::io::ErrorKind::NotFound,
"test",
)));
assert_eq!(collector.get_errors().unwrap().len(), 1);
}

#[test]
fn test_get_warnings() {
let mut collector = RusticIssueCollector::default();
collector.add_warning("test");
assert_eq!(collector.get_warnings().unwrap().len(), 1);
}

#[test]
fn test_get_info() {
let mut collector = RusticIssueCollector::default();
collector.add_info("test");
assert_eq!(collector.get_info().unwrap().len(), 1);
}
}