From 3be2def9afe35b1fbaec6f3c8ae5bc71d1487745 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:58:59 +0100 Subject: [PATCH 01/18] feat: add summary module for aggregating runtime information and internal metrics Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error.rs | 2 + ...c_core__error__summary__tests__Issues.snap | 11 + ..._core__error__summary__tests__Metrics.snap | 8 + crates/core/src/error/summary.rs | 390 ++++++++++++++++++ 4 files changed, 411 insertions(+) create mode 100644 crates/core/src/error/snapshots/rustic_core__error__summary__tests__Issues.snap create mode 100644 crates/core/src/error/snapshots/rustic_core__error__summary__tests__Metrics.snap create mode 100644 crates/core/src/error/summary.rs diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 9d6b3933..4a005bc6 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -48,6 +48,8 @@ //! All types that we want to attach to an error should implement `Display` and `Debug` to provide a good error message and a nice way //! to display the error. +pub(crate) mod summary; + use derive_more::derive::Display; use ecow::{EcoString, EcoVec}; use std::{ diff --git a/crates/core/src/error/snapshots/rustic_core__error__summary__tests__Issues.snap b/crates/core/src/error/snapshots/rustic_core__error__summary__tests__Issues.snap new file mode 100644 index 00000000..e85da02d --- /dev/null +++ b/crates/core/src/error/snapshots/rustic_core__error__summary__tests__Issues.snap @@ -0,0 +1,11 @@ +--- +source: crates/core/src/error/summary.rs +expression: display_output +--- +Context: Check + +Issues Encountered: + Scope: Internal + Pack not found - Occurrences: 1 (Root Cause: Inconsistent state on disk) + Scope: UserInput + Invalid input - Occurrences: 2 (Root Cause: Missing field) diff --git a/crates/core/src/error/snapshots/rustic_core__error__summary__tests__Metrics.snap b/crates/core/src/error/snapshots/rustic_core__error__summary__tests__Metrics.snap new file mode 100644 index 00000000..23caac14 --- /dev/null +++ b/crates/core/src/error/snapshots/rustic_core__error__summary__tests__Metrics.snap @@ -0,0 +1,8 @@ +--- +source: crates/core/src/error/summary.rs +expression: display_output +--- +Context: Check + +Metrics: + execution_time: 5s diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs new file mode 100644 index 00000000..0ff287d1 --- /dev/null +++ b/crates/core/src/error/summary.rs @@ -0,0 +1,390 @@ +//! An informative summary system for aggregating and condensing data collected +//! from runtime checks, including warnings, issues, and operational metrics. +//! +//! This system should provide end-users with a clear, concise summary of command +//! execution results without conflicting with existing error-handling standards. +//! In scenarios where execution cannot proceed due to a critical error, a +//! `RusticError` will be raised instead, and no summary will be provided. +//! +//! # Separation of Concerns +//! +//! Critical runtime errors that prevent further execution are handled through the +//! existing `RusticError` system. The `Summary` will only collect information for +//! non-fatal events. +//! +//! # Compatibility with Existing Error Handling +//! +//! Summaries must coexist with error propagation rules. They will not replace +//! the core behavior of error propagation but act as a complementary mechanism +//! for presenting non-fatal feedback. +//! +//! # User-Friendly Reporting +//! +//! Summaries should aggregate detailed runtime information—such as warnings, +//! issues, and metrics — in a clear and condensed format for the end-user. +//! +//! # Aggregation & Condensation +//! +//! Similar or repeated errors should be aggregated to avoid redundant information, +//! presenting users with a high-level overview. + +use std::{ + collections::{BTreeMap, HashSet}, + fmt::{self, Display}, + string::ToString, + time::Instant, +}; + +pub type IssueIdentifier = String; + +pub type Issues = BTreeMap>; +pub type Metrics = BTreeMap; + +#[derive( + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + derive_more::Display, + serde::Serialize, + serde::Deserialize, +)] +pub enum IssueScope { + #[default] + Internal, + Unknown, + UserInput, +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct CondensedIssue { + /// High-level description of the problem + message: String, + + /// Number of occurrences + count: usize, + + /// Optional diagnostic information, e.g. an error message + root_cause: Option, +} + +#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq, derive_more::Display)] +pub enum DisplayOptionKind { + #[default] + Issues, + Timing, + Metrics, + All, +} + +#[derive(Debug, Clone)] +pub struct Summary { + /// Name of the active context, e.g. a command or operation + context: String, + + /// Start time of the collection + // Instant cannot be (de-)serialized, for an implementation see: + // https://github.com/serde-rs/serde/issues/1375#issuecomment-419688068 + start_time: Instant, + + /// End time, when the collection is completed + // Serialization: See note above + end_time: Option, + + /// Collection of non-critical warnings + issues: Issues, + + /// Optional custom metrics collected during execution + metrics: Metrics, + + /// Display this data + display: HashSet, +} + +impl Summary { + /// Constructor to create an initial empty Summary + pub fn new(context: &str) -> Self { + Self { + context: context.to_string(), + start_time: Instant::now(), + end_time: None, + issues: Issues::default(), + metrics: BTreeMap::default(), + display: HashSet::from([DisplayOptionKind::default()]), + } + } + + /// Marks the summary as completed, capturing the end time. + pub fn complete(&mut self) { + self.end_time = Some(Instant::now()); + } + + /// Adds a new issue to the summary, condensing similar issues + pub fn add_issue(&mut self, scope: IssueScope, message: &str, root_cause: Option<&str>) { + _ = self + .issues + .entry(scope) + .or_default() + .entry(message.to_string()) + .and_modify(|val| { + val.count += 1; + if val.root_cause.is_none() { + val.root_cause = root_cause.map(ToString::to_string); + } + }) + .or_insert(CondensedIssue { + message: message.to_string(), + count: 1, + root_cause: root_cause.map(ToString::to_string), + }); + } + + /// Adds a custom metric + pub fn add_metric(&mut self, key: &str, value: &str) { + _ = self + .metrics + .entry(key.to_string()) + .and_modify(|val| *val = value.to_string()) + .or_insert(value.to_string()); + } + + pub fn export_issues(&mut self) -> bool { + self.display.insert(DisplayOptionKind::Issues) + } + + pub fn export_timing(&mut self) -> bool { + self.display.insert(DisplayOptionKind::Timing) + } + + pub fn export_metrics(&mut self) -> bool { + self.display.insert(DisplayOptionKind::Metrics) + } + + pub fn export_all(&mut self) -> bool { + self.display.insert(DisplayOptionKind::All) + } + + pub fn export_none(&mut self) { + self.display.clear() + } + + pub fn set_export(&mut self, option: DisplayOptionKind) -> bool { + self.display.clear(); + self.display.insert(option) + } +} + +impl Display for Summary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // General context information + writeln!(f, "Context: {}", self.context)?; + // Display Duration + if !self.display.is_disjoint(&HashSet::from([ + DisplayOptionKind::Timing, + DisplayOptionKind::All, + ])) { + writeln!(f)?; + if let Some(end_time) = self.end_time { + let duration = end_time.duration_since(self.start_time); + let human_duration = humantime::format_duration(duration); + + writeln!(f, "Execution Time: {human_duration}")?; + } + } + + // Display Issues + if !self.issues.is_empty() + && !self.display.is_disjoint(&HashSet::from([ + DisplayOptionKind::Issues, + DisplayOptionKind::All, + ])) + { + writeln!(f)?; + writeln!(f, "Issues Encountered:")?; + for (scope, scoped_issues) in &self.issues { + writeln!(f, " Scope: {scope}")?; + for (message, issue) in scoped_issues { + let root_cause_info = issue + .root_cause + .as_ref() + .map_or_else(String::new, |root| format!(" (Root Cause: {root})")); + + writeln!( + f, + " {} - Occurrences: {}{}", + message, issue.count, root_cause_info + )?; + } + } + } + + // Additional metrics + if !self.metrics.is_empty() + && !self.display.is_disjoint(&HashSet::from([ + DisplayOptionKind::Metrics, + DisplayOptionKind::All, + ])) + { + writeln!(f)?; + writeln!(f, "Metrics:")?; + for (key, value) in &self.metrics { + writeln!(f, " {key}: {value}")?; + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::{thread::sleep, time::Duration}; + + use insta::assert_snapshot; + use rstest::rstest; + + use super::*; + + #[test] + fn test_summary_completion_and_display_passes() { + let mut summary = Summary::new("test_command"); + + sleep(Duration::from_millis(250)); + + summary.complete(); + + assert!(summary.end_time.is_some()); + } + + #[test] + fn test_add_issue_passes() { + let mut summary = Summary::new("test_command"); + + summary.add_issue( + IssueScope::UserInput, + "Invalid input", + Some("Missing field"), + ); + + assert_eq!(summary.issues.len(), 1); + + let user_input_issues = summary.issues.get(&IssueScope::UserInput).unwrap(); + + let issue = user_input_issues.get("Invalid input").unwrap(); + + assert_eq!(issue.count, 1); + + assert_eq!(issue.root_cause.as_deref(), Some("Missing field")); + } + + #[test] + fn test_add_issue_aggregation() { + let mut summary = Summary::new("test_command"); + + summary.add_issue( + IssueScope::UserInput, + "Invalid input", + Some("Missing field"), + ); + + summary.add_issue( + IssueScope::UserInput, + "Invalid input", + Some("Missing field"), + ); + + assert_eq!(summary.issues.len(), 1); + + let user_input_issues = summary.issues.get(&IssueScope::UserInput).unwrap(); + + let issue = user_input_issues.get("Invalid input").unwrap(); + + assert_eq!(issue.count, 2); + } + + #[test] + fn test_add_metric() { + let mut summary = Summary::new("test_command"); + + summary.add_metric("execution_time", "5s"); + + assert_eq!(summary.metrics.len(), 1); + + assert_eq!(summary.metrics.get("execution_time").unwrap(), "5s"); + } + + #[rstest] + #[case(DisplayOptionKind::Issues)] + #[case(DisplayOptionKind::Timing)] + #[case(DisplayOptionKind::Metrics)] + #[case(DisplayOptionKind::All)] + fn test_summary_display(#[case] display: DisplayOptionKind) { + let mut summary = Summary::new("Check"); + _ = summary.set_export(display); + + summary.add_issue( + IssueScope::UserInput, + "Invalid input", + Some("Missing field"), + ); + + summary.add_issue( + IssueScope::UserInput, + "Invalid input", + Some("Missing field"), + ); + + summary.add_issue( + IssueScope::Internal, + "Pack not found", + Some("Inconsistent state on disk"), + ); + + summary.add_metric("execution_time", "5s"); + + summary.complete(); + + let display_output = format!("{summary}"); + + assert!(display_output.contains("Context: Check")); + + match display { + DisplayOptionKind::Issues => { + assert!(display_output.contains("Issues Encountered:")); + assert!(display_output.contains("Scope: UserInput")); + + assert!(display_output + .contains("Invalid input - Occurrences: 2 (Root Cause: Missing field)")); + + assert_snapshot!(display.to_string(), display_output); + } + DisplayOptionKind::Timing => { + assert!(display_output.contains("Execution Time:")); + } + DisplayOptionKind::Metrics => { + assert!(display_output.contains("Metrics:")); + + assert!(display_output.contains("execution_time: 5s")); + + assert_snapshot!(display.to_string(), display_output); + } + DisplayOptionKind::All => { + assert!(display_output.contains("Issues Encountered:")); + assert!(display_output.contains("Scope: UserInput")); + + assert!(display_output + .contains("Invalid input - Occurrences: 2 (Root Cause: Missing field)")); + + assert!(display_output.contains("Execution Time:")); + + assert!(display_output.contains("Metrics:")); + + assert!(display_output.contains("execution_time: 5s")); + } + } + } +} From 5908230e8cfb965ef43b24c96fc8ce53fda29347 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:55:22 +0100 Subject: [PATCH 02/18] feat: enhance Summary struct to use EcoString Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- Cargo.lock | 3 + crates/core/Cargo.toml | 2 +- crates/core/src/error/summary.rs | 154 ++++++++++++++++++------------- 3 files changed, 93 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50abfeb7..daf10f3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1222,6 +1222,9 @@ name = "ecow" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42fc0a93992b20c58b99e59d61eaf1635a25bfbe49e4275c34ba0aee98119ba" +dependencies = [ + "serde", +] [[package]] name = "either" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 645770ae..37c710b5 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -98,7 +98,7 @@ runtime-format = "0.1.3" bytes = { workspace = true } bytesize = "1.3.0" chrono = { version = "0.4.38", default-features = false, features = ["clock", "serde"] } -ecow = "0.2.3" +ecow = { version = "0.2.3", features = ["serde"] } enum-map = { workspace = true } enum-map-derive = "0.17.0" enumset = { version = "1.1.5", features = ["serde"] } diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 0ff287d1..d4e438c8 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -31,14 +31,15 @@ use std::{ collections::{BTreeMap, HashSet}, fmt::{self, Display}, - string::ToString, time::Instant, }; -pub type IssueIdentifier = String; +use ecow::EcoString; + +pub type IssueIdentifier = EcoString; pub type Issues = BTreeMap>; -pub type Metrics = BTreeMap; +pub type Metrics = BTreeMap; #[derive( Debug, @@ -63,13 +64,13 @@ pub enum IssueScope { #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] pub struct CondensedIssue { /// High-level description of the problem - message: String, + message: EcoString, /// Number of occurrences count: usize, /// Optional diagnostic information, e.g. an error message - root_cause: Option, + root_cause: Option, } #[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq, derive_more::Display)] @@ -84,7 +85,7 @@ pub enum DisplayOptionKind { #[derive(Debug, Clone)] pub struct Summary { /// Name of the active context, e.g. a command or operation - context: String, + context: EcoString, /// Start time of the collection // Instant cannot be (de-)serialized, for an implementation see: @@ -109,7 +110,7 @@ impl Summary { /// Constructor to create an initial empty Summary pub fn new(context: &str) -> Self { Self { - context: context.to_string(), + context: context.into(), start_time: Instant::now(), end_time: None, issues: Issues::default(), @@ -129,17 +130,17 @@ impl Summary { .issues .entry(scope) .or_default() - .entry(message.to_string()) + .entry(message.into()) .and_modify(|val| { val.count += 1; if val.root_cause.is_none() { - val.root_cause = root_cause.map(ToString::to_string); + val.root_cause = root_cause.map(Into::into); } }) .or_insert(CondensedIssue { - message: message.to_string(), + message: message.into(), count: 1, - root_cause: root_cause.map(ToString::to_string), + root_cause: root_cause.map(Into::into), }); } @@ -147,9 +148,9 @@ impl Summary { pub fn add_metric(&mut self, key: &str, value: &str) { _ = self .metrics - .entry(key.to_string()) - .and_modify(|val| *val = value.to_string()) - .or_insert(value.to_string()); + .entry(key.into()) + .and_modify(|val| *val = value.into()) + .or_insert(value.into()); } pub fn export_issues(&mut self) -> bool { @@ -178,62 +179,89 @@ impl Summary { } } +// Display Helpers +impl Summary { + fn should_display_timing(&self) -> bool { + !self.display.is_disjoint(&HashSet::from([ + DisplayOptionKind::Timing, + DisplayOptionKind::All, + ])) + } + + fn should_display_issues(&self) -> bool { + !self.display.is_disjoint(&HashSet::from([ + DisplayOptionKind::Issues, + DisplayOptionKind::All, + ])) + } + + fn should_display_metrics(&self) -> bool { + !self.display.is_disjoint(&HashSet::from([ + DisplayOptionKind::Metrics, + DisplayOptionKind::All, + ])) + } + + fn display_timing(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f)?; + if let Some(end_time) = self.end_time { + let duration = end_time.duration_since(self.start_time); + let human_duration = humantime::format_duration(duration); + + writeln!(f, "Execution Time: {human_duration}")?; + } + + Ok(()) + } + + fn display_issues(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f)?; + writeln!(f, "Issues Encountered:")?; + for (scope, scoped_issues) in &self.issues { + writeln!(f, " Scope: {scope}")?; + for (message, issue) in scoped_issues { + let root_cause_info = issue + .root_cause + .as_ref() + .map_or_else(String::new, |root| format!(" (Root Cause: {root})")); + + writeln!( + f, + " {} - Occurrences: {}{}", + message, issue.count, root_cause_info + )?; + } + } + + Ok(()) + } + + fn display_metrics(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f)?; + writeln!(f, "Metrics:")?; + for (key, value) in &self.metrics { + writeln!(f, " {key}: {value}")?; + } + + Ok(()) + } +} + impl Display for Summary { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // General context information writeln!(f, "Context: {}", self.context)?; - // Display Duration - if !self.display.is_disjoint(&HashSet::from([ - DisplayOptionKind::Timing, - DisplayOptionKind::All, - ])) { - writeln!(f)?; - if let Some(end_time) = self.end_time { - let duration = end_time.duration_since(self.start_time); - let human_duration = humantime::format_duration(duration); - writeln!(f, "Execution Time: {human_duration}")?; - } + if self.should_display_timing() { + self.display_timing(f)?; } - // Display Issues - if !self.issues.is_empty() - && !self.display.is_disjoint(&HashSet::from([ - DisplayOptionKind::Issues, - DisplayOptionKind::All, - ])) - { - writeln!(f)?; - writeln!(f, "Issues Encountered:")?; - for (scope, scoped_issues) in &self.issues { - writeln!(f, " Scope: {scope}")?; - for (message, issue) in scoped_issues { - let root_cause_info = issue - .root_cause - .as_ref() - .map_or_else(String::new, |root| format!(" (Root Cause: {root})")); - - writeln!( - f, - " {} - Occurrences: {}{}", - message, issue.count, root_cause_info - )?; - } - } + if !self.issues.is_empty() && self.should_display_issues() { + self.display_issues(f)?; } - // Additional metrics - if !self.metrics.is_empty() - && !self.display.is_disjoint(&HashSet::from([ - DisplayOptionKind::Metrics, - DisplayOptionKind::All, - ])) - { - writeln!(f)?; - writeln!(f, "Metrics:")?; - for (key, value) in &self.metrics { - writeln!(f, " {key}: {value}")?; - } + if !self.metrics.is_empty() && self.should_display_metrics() { + self.display_metrics(f)?; } Ok(()) @@ -242,8 +270,6 @@ impl Display for Summary { #[cfg(test)] mod tests { - use std::{thread::sleep, time::Duration}; - use insta::assert_snapshot; use rstest::rstest; @@ -253,8 +279,6 @@ mod tests { fn test_summary_completion_and_display_passes() { let mut summary = Summary::new("test_command"); - sleep(Duration::from_millis(250)); - summary.complete(); assert!(summary.end_time.is_some()); From 9b6dcb7b244ddaa5791a22f272d988dfc216e2e0 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:58:23 +0100 Subject: [PATCH 03/18] refactor: remove unnecessary Deserialize derive from CondensedIssue and update export logic Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index d4e438c8..4a51737a 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -52,7 +52,6 @@ pub type Metrics = BTreeMap; Ord, derive_more::Display, serde::Serialize, - serde::Deserialize, )] pub enum IssueScope { #[default] @@ -61,7 +60,7 @@ pub enum IssueScope { UserInput, } -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, serde::Serialize)] pub struct CondensedIssue { /// High-level description of the problem message: EcoString, @@ -73,7 +72,9 @@ pub struct CondensedIssue { root_cause: Option, } -#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq, derive_more::Display)] +#[derive( + Debug, Clone, Copy, Default, Hash, PartialEq, Eq, derive_more::Display, serde::Serialize, +)] pub enum DisplayOptionKind { #[default] Issues, @@ -150,7 +151,7 @@ impl Summary { .metrics .entry(key.into()) .and_modify(|val| *val = value.into()) - .or_insert(value.into()); + .or_insert_with(|| value.into()); } pub fn export_issues(&mut self) -> bool { @@ -170,7 +171,7 @@ impl Summary { } pub fn export_none(&mut self) { - self.display.clear() + self.display.clear(); } pub fn set_export(&mut self, option: DisplayOptionKind) -> bool { From 9611df58d411bcd60af74cd90e63500e32805456 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Fri, 22 Nov 2024 00:10:58 +0100 Subject: [PATCH 04/18] feat: update Metrics type to use MetricValue enum and modify add_metric method Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 4a51737a..3279a770 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -39,7 +39,16 @@ use ecow::EcoString; pub type IssueIdentifier = EcoString; pub type Issues = BTreeMap>; -pub type Metrics = BTreeMap; + +#[derive(Debug, Clone, derive_more::Display, derive_more::From, serde::Serialize)] +#[display("{metric}")] +pub enum MetricValue { + Int(i64), + Float(f64), + String(EcoString), +} + +pub type Metrics = BTreeMap; #[derive( Debug, @@ -146,12 +155,12 @@ impl Summary { } /// Adds a custom metric - pub fn add_metric(&mut self, key: &str, value: &str) { + pub fn add_metric(&mut self, key: &str, value: MetricValue) { _ = self .metrics .entry(key.into()) - .and_modify(|val| *val = value.into()) - .or_insert_with(|| value.into()); + .and_modify(|val| *val = value.clone()) + .or_insert_with(|| value); } pub fn export_issues(&mut self) -> bool { @@ -297,9 +306,14 @@ mod tests { assert_eq!(summary.issues.len(), 1); - let user_input_issues = summary.issues.get(&IssueScope::UserInput).unwrap(); + let user_input_issues = summary + .issues + .get(&IssueScope::UserInput) + .expect("Expected to find UserInput issues in the summary, but none were found"); - let issue = user_input_issues.get("Invalid input").unwrap(); + let issue = user_input_issues.get("Invalid input").expect( + "Expected to find an issue with the message 'Invalid input', but none were found", + ); assert_eq!(issue.count, 1); @@ -369,7 +383,7 @@ mod tests { Some("Inconsistent state on disk"), ); - summary.add_metric("execution_time", "5s"); + summary.add_metric("execution_time (sec)", 5.into()); summary.complete(); @@ -393,7 +407,7 @@ mod tests { DisplayOptionKind::Metrics => { assert!(display_output.contains("Metrics:")); - assert!(display_output.contains("execution_time: 5s")); + assert!(display_output.contains("execution_time (sec): 5")); assert_snapshot!(display.to_string(), display_output); } @@ -408,7 +422,7 @@ mod tests { assert!(display_output.contains("Metrics:")); - assert!(display_output.contains("execution_time: 5s")); + assert!(display_output.contains("execution_time (sec): 5")); } } } From 22652567c3288a160ce4d8c932d67de7ed6eb723 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Fri, 22 Nov 2024 00:27:10 +0100 Subject: [PATCH 05/18] refactor: simplify Metrics type and update add_issue and add_metric methods to use EcoString Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 40 +++++++++++++++++--------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 3279a770..2906cedf 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -40,15 +40,7 @@ pub type IssueIdentifier = EcoString; pub type Issues = BTreeMap>; -#[derive(Debug, Clone, derive_more::Display, derive_more::From, serde::Serialize)] -#[display("{metric}")] -pub enum MetricValue { - Int(i64), - Float(f64), - String(EcoString), -} - -pub type Metrics = BTreeMap; +pub type Metrics = BTreeMap; #[derive( Debug, @@ -118,7 +110,7 @@ pub struct Summary { impl Summary { /// Constructor to create an initial empty Summary - pub fn new(context: &str) -> Self { + pub fn new(context: impl Into) -> Self { Self { context: context.into(), start_time: Instant::now(), @@ -135,27 +127,37 @@ impl Summary { } /// Adds a new issue to the summary, condensing similar issues - pub fn add_issue(&mut self, scope: IssueScope, message: &str, root_cause: Option<&str>) { + pub fn add_issue( + &mut self, + scope: IssueScope, + message: impl Into, + root_cause: Option>, + ) { + let message = message.into(); + let root_cause = root_cause.map(Into::into); + _ = self .issues .entry(scope) .or_default() - .entry(message.into()) + .entry(message.clone()) .and_modify(|val| { val.count += 1; if val.root_cause.is_none() { - val.root_cause = root_cause.map(Into::into); + val.root_cause.clone_from(&root_cause); } }) .or_insert(CondensedIssue { - message: message.into(), + message, count: 1, - root_cause: root_cause.map(Into::into), + root_cause, }); } /// Adds a custom metric - pub fn add_metric(&mut self, key: &str, value: MetricValue) { + pub fn add_metric(&mut self, key: impl Into, value: impl Into) { + let value = value.into(); + _ = self .metrics .entry(key.into()) @@ -383,7 +385,7 @@ mod tests { Some("Inconsistent state on disk"), ); - summary.add_metric("execution_time (sec)", 5.into()); + summary.add_metric("execution_time", "5s"); summary.complete(); @@ -407,7 +409,7 @@ mod tests { DisplayOptionKind::Metrics => { assert!(display_output.contains("Metrics:")); - assert!(display_output.contains("execution_time (sec): 5")); + assert!(display_output.contains("execution_time: 5s")); assert_snapshot!(display.to_string(), display_output); } @@ -422,7 +424,7 @@ mod tests { assert!(display_output.contains("Metrics:")); - assert!(display_output.contains("execution_time (sec): 5")); + assert!(display_output.contains("execution_time: 5s")); } } } From 8bbace611967e70d18ae3a3c57ec3045634add30 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Sun, 24 Nov 2024 23:20:46 +0100 Subject: [PATCH 06/18] feat: add IssueCategory enum and logging for issues in Summary Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 71 +++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 2906cedf..56980488 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -42,6 +42,25 @@ pub type Issues = BTreeMap pub type Metrics = BTreeMap; +#[derive( + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + derive_more::Display, + serde::Serialize, +)] +pub enum IssueCategory { + #[default] + Warning, + Error, + Info, +} + #[derive( Debug, Clone, @@ -66,6 +85,8 @@ pub struct CondensedIssue { /// High-level description of the problem message: EcoString, + category: IssueCategory, + /// Number of occurrences count: usize, @@ -106,6 +127,9 @@ pub struct Summary { /// Display this data display: HashSet, + + /// Log enabled + log_enabled: bool, } impl Summary { @@ -118,6 +142,7 @@ impl Summary { issues: Issues::default(), metrics: BTreeMap::default(), display: HashSet::from([DisplayOptionKind::default()]), + log_enabled: false, } } @@ -130,11 +155,16 @@ impl Summary { pub fn add_issue( &mut self, scope: IssueScope, + category: IssueCategory, message: impl Into, root_cause: Option>, ) { - let message = message.into(); let root_cause = root_cause.map(Into::into); + let message = message.into(); + + if self.log_enabled { + Self::log_issue(scope, category, &message, &root_cause); + } _ = self .issues @@ -148,6 +178,7 @@ impl Summary { } }) .or_insert(CondensedIssue { + category, message, count: 1, root_cause, @@ -189,6 +220,31 @@ impl Summary { self.display.clear(); self.display.insert(option) } + + fn log_issue( + scope: IssueScope, + category: IssueCategory, + message: EcoString, + root_cause: Option<&EcoString>, + ) { + let mut to_print = String::new(); + + if root_cause.is_none() { + writeln!(to_print, "in scope '{scope}': {message}",) + } else { + let root_cause = format_root_cause(root_cause); + writeln!( + to_print, + "in scope '{scope}': {message} (Root Cause: {root_cause})", + ) + } + + match category { + IssueCategory::Error => log::error(to_print), + IssueCategory::Warning => log::warn(to_print), + IssueCategory::Info => log::info(to_print), + } + } } // Display Helpers @@ -232,10 +288,7 @@ impl Summary { for (scope, scoped_issues) in &self.issues { writeln!(f, " Scope: {scope}")?; for (message, issue) in scoped_issues { - let root_cause_info = issue - .root_cause - .as_ref() - .map_or_else(String::new, |root| format!(" (Root Cause: {root})")); + let root_cause_info = format_root_cause(issue.root_cause); writeln!( f, @@ -259,6 +312,14 @@ impl Summary { } } +fn format_root_cause(root_cause: Option) -> String { + let root_cause_info = root_cause + .as_ref() + .map_or_else(String::new, |root| format!(" (Root Cause: {root})")); + + root_cause_info +} + impl Display for Summary { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // General context information From 1f40182b8717789b284b65a8e9738da5ab948e4d Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:42:41 +0100 Subject: [PATCH 07/18] feat: refactor issue handling to categorize by IssueCategory and improve logging Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- ...c_core__error__summary__tests__Issues.snap | 3 + crates/core/src/error/summary.rs | 67 ++++++++++--------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/crates/core/src/error/snapshots/rustic_core__error__summary__tests__Issues.snap b/crates/core/src/error/snapshots/rustic_core__error__summary__tests__Issues.snap index e85da02d..48ca75de 100644 --- a/crates/core/src/error/snapshots/rustic_core__error__summary__tests__Issues.snap +++ b/crates/core/src/error/snapshots/rustic_core__error__summary__tests__Issues.snap @@ -1,11 +1,14 @@ --- source: crates/core/src/error/summary.rs expression: display_output +snapshot_kind: text --- Context: Check Issues Encountered: +Warning Scope: Internal Pack not found - Occurrences: 1 (Root Cause: Inconsistent state on disk) +Error Scope: UserInput Invalid input - Occurrences: 2 (Root Cause: Missing field) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 56980488..9754d745 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -30,15 +30,13 @@ use std::{ collections::{BTreeMap, HashSet}, - fmt::{self, Display}, + fmt::{self, Display, Write}, time::Instant, }; use ecow::EcoString; -pub type IssueIdentifier = EcoString; - -pub type Issues = BTreeMap>; +pub type Issues = BTreeMap>; pub type Metrics = BTreeMap; @@ -154,8 +152,8 @@ impl Summary { /// Adds a new issue to the summary, condensing similar issues pub fn add_issue( &mut self, - scope: IssueScope, category: IssueCategory, + scope: IssueScope, message: impl Into, root_cause: Option>, ) { @@ -163,14 +161,15 @@ impl Summary { let message = message.into(); if self.log_enabled { - Self::log_issue(scope, category, &message, &root_cause); + // We ignore the result here, as we don't want to propagate the error + _ = Self::log_issue(scope, category, &message, &root_cause); } _ = self .issues - .entry(scope) + .entry(category) .or_default() - .entry(message.clone()) + .entry(scope) .and_modify(|val| { val.count += 1; if val.root_cause.is_none() { @@ -224,26 +223,25 @@ impl Summary { fn log_issue( scope: IssueScope, category: IssueCategory, - message: EcoString, - root_cause: Option<&EcoString>, - ) { + message: &EcoString, + root_cause: &Option, + ) -> fmt::Result { let mut to_print = String::new(); if root_cause.is_none() { - writeln!(to_print, "in scope '{scope}': {message}",) + writeln!(to_print, "in scope '{scope}': {message}",)?; } else { let root_cause = format_root_cause(root_cause); - writeln!( - to_print, - "in scope '{scope}': {message} (Root Cause: {root_cause})", - ) + writeln!(to_print, "in scope '{scope}': {message}{root_cause}",)?; } match category { - IssueCategory::Error => log::error(to_print), - IssueCategory::Warning => log::warn(to_print), - IssueCategory::Info => log::info(to_print), + IssueCategory::Error => log::error!("{to_print}"), + IssueCategory::Warning => log::warn!("{to_print}"), + IssueCategory::Info => log::info!("{to_print}"), } + + Ok(()) } } @@ -284,16 +282,19 @@ impl Summary { fn display_issues(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f)?; + writeln!(f, "Issues Encountered:")?; - for (scope, scoped_issues) in &self.issues { - writeln!(f, " Scope: {scope}")?; - for (message, issue) in scoped_issues { - let root_cause_info = format_root_cause(issue.root_cause); + + for (category, scoped_issues) in &self.issues { + writeln!(f, "{category}")?; + for (scope, issue) in scoped_issues { + writeln!(f, " Scope: {scope}")?; + let root_cause_info = format_root_cause(&issue.root_cause); writeln!( f, - " {} - Occurrences: {}{}", - message, issue.count, root_cause_info + " {} - Occurrences: {}{root_cause_info}", + issue.message, issue.count )?; } } @@ -312,7 +313,7 @@ impl Summary { } } -fn format_root_cause(root_cause: Option) -> String { +fn format_root_cause(root_cause: &Option) -> String { let root_cause_info = root_cause .as_ref() .map_or_else(String::new, |root| format!(" (Root Cause: {root})")); @@ -362,6 +363,7 @@ mod tests { let mut summary = Summary::new("test_command"); summary.add_issue( + IssueCategory::Error, IssueScope::UserInput, "Invalid input", Some("Missing field"), @@ -371,10 +373,10 @@ mod tests { let user_input_issues = summary .issues - .get(&IssueScope::UserInput) + .get(&IssueCategory::Error) .expect("Expected to find UserInput issues in the summary, but none were found"); - let issue = user_input_issues.get("Invalid input").expect( + let issue = user_input_issues.get(&IssueScope::UserInput).expect( "Expected to find an issue with the message 'Invalid input', but none were found", ); @@ -388,12 +390,14 @@ mod tests { let mut summary = Summary::new("test_command"); summary.add_issue( + IssueCategory::Error, IssueScope::UserInput, "Invalid input", Some("Missing field"), ); summary.add_issue( + IssueCategory::Error, IssueScope::UserInput, "Invalid input", Some("Missing field"), @@ -401,9 +405,9 @@ mod tests { assert_eq!(summary.issues.len(), 1); - let user_input_issues = summary.issues.get(&IssueScope::UserInput).unwrap(); + let user_input_issues = summary.issues.get(&IssueCategory::Error).unwrap(); - let issue = user_input_issues.get("Invalid input").unwrap(); + let issue = user_input_issues.get(&IssueScope::UserInput).unwrap(); assert_eq!(issue.count, 2); } @@ -429,18 +433,21 @@ mod tests { _ = summary.set_export(display); summary.add_issue( + IssueCategory::Error, IssueScope::UserInput, "Invalid input", Some("Missing field"), ); summary.add_issue( + IssueCategory::Error, IssueScope::UserInput, "Invalid input", Some("Missing field"), ); summary.add_issue( + IssueCategory::Warning, IssueScope::Internal, "Pack not found", Some("Inconsistent state on disk"), From efb345e672998cae85098ccf680926444cbc6258 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:50:13 +0100 Subject: [PATCH 08/18] feat: add merge method to Summary and corresponding unit test Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 9754d745..e8b7b2d4 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -243,6 +243,11 @@ impl Summary { Ok(()) } + + pub fn merge(&mut self, other: Summary) { + self.issues.extend(other.issues); + self.metrics.extend(other.metrics); + } } // Display Helpers @@ -496,4 +501,38 @@ mod tests { } } } + + #[test] + fn test_merge_summaries_passes() { + let mut summary = Summary::new("Check Trees"); + + summary.add_issue( + IssueCategory::Error, + IssueScope::UserInput, + "Invalid input", + Some("Missing field"), + ); + + summary.add_metric("execution_time", "5s"); + + summary.complete(); + + let mut other_summary = Summary::new("Check Packs"); + + other_summary.add_issue( + IssueCategory::Error, + IssueScope::UserInput, + "Invalid input", + Some("Missing field"), + ); + + other_summary.add_metric("execution_time", "5s"); + + other_summary.complete(); + + summary.merge(other_summary); + + assert_eq!(summary.issues.len(), 1); + assert_eq!(summary.metrics.len(), 1); + } } From bbafe09a5af2f408ea194a795043ccf5e2dbe9f3 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:59:15 +0100 Subject: [PATCH 09/18] feat: add TODO comments for refactoring merge method in Summary Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index e8b7b2d4..18404f97 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -244,6 +244,14 @@ impl Summary { Ok(()) } + // ! TODO: Refactor this method to merge the context of each summary + // ! + // ? How do we merge the context of each summary? + // ? We can't merge the context, as it's a unique identifier for each summary. + // ? We could add a new field to each CondensedIssue to store the context of the + // ? merged summaries. + // ? + // ? How do we merge the other fields? E.g. Timing, Metrics, etc. pub fn merge(&mut self, other: Summary) { self.issues.extend(other.issues); self.metrics.extend(other.metrics); From a437de46272a09fb7eb473bbdafdd5aaefff2ef8 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:09:42 +0100 Subject: [PATCH 10/18] feat: add methods to log errors, warnings, and info in Summary Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 18404f97..066a6c21 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -149,6 +149,33 @@ impl Summary { self.end_time = Some(Instant::now()); } + pub fn add_error( + &mut self, + scope: IssueScope, + message: impl Into, + root_cause: Option>, + ) { + self.add_issue(IssueCategory::Error, scope, message, root_cause); + } + + pub fn add_warning( + &mut self, + scope: IssueScope, + message: impl Into, + root_cause: Option>, + ) { + self.add_issue(IssueCategory::Warning, scope, message, root_cause); + } + + pub fn add_info( + &mut self, + scope: IssueScope, + message: impl Into, + root_cause: Option>, + ) { + self.add_issue(IssueCategory::Info, scope, message, root_cause); + } + /// Adds a new issue to the summary, condensing similar issues pub fn add_issue( &mut self, From 8e13aa95d632c754184b30c10c8dcfc207a9db47 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:19:51 +0100 Subject: [PATCH 11/18] feat: add method to enable logging in Summary Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 066a6c21..cf362004 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -149,6 +149,10 @@ impl Summary { self.end_time = Some(Instant::now()); } + pub fn enable_log(&mut self) { + self.log_enabled = true; + } + pub fn add_error( &mut self, scope: IssueScope, From f16996752fa595a59b7ff461b7b2af9c856bd81e Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:18:09 +0100 Subject: [PATCH 12/18] feat: enhance logging functionality and update root cause handling in Summary Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index cf362004..c2b99640 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -149,15 +149,20 @@ impl Summary { self.end_time = Some(Instant::now()); } - pub fn enable_log(&mut self) { + pub fn enable_log(&mut self) -> Self { self.log_enabled = true; + self.clone() + } + + pub fn is_error(&self) -> bool { + self.issues.contains_key(&IssueCategory::Error) } pub fn add_error( &mut self, scope: IssueScope, message: impl Into, - root_cause: Option>, + root_cause: impl Into>, ) { self.add_issue(IssueCategory::Error, scope, message, root_cause); } @@ -166,7 +171,7 @@ impl Summary { &mut self, scope: IssueScope, message: impl Into, - root_cause: Option>, + root_cause: impl Into>, ) { self.add_issue(IssueCategory::Warning, scope, message, root_cause); } @@ -175,7 +180,7 @@ impl Summary { &mut self, scope: IssueScope, message: impl Into, - root_cause: Option>, + root_cause: impl Into>, ) { self.add_issue(IssueCategory::Info, scope, message, root_cause); } @@ -186,9 +191,9 @@ impl Summary { category: IssueCategory, scope: IssueScope, message: impl Into, - root_cause: Option>, + root_cause: impl Into>, ) { - let root_cause = root_cause.map(Into::into); + let root_cause = root_cause.into(); let message = message.into(); if self.log_enabled { From 6d6131740b12217f63f17d985fc6896daf6826ef Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:24:33 +0100 Subject: [PATCH 13/18] feat: update merge method to use Self type and improve handling of optional fields in Summary Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index c2b99640..5835a45d 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -288,7 +288,7 @@ impl Summary { // ? merged summaries. // ? // ? How do we merge the other fields? E.g. Timing, Metrics, etc. - pub fn merge(&mut self, other: Summary) { + pub fn merge(&mut self, other: Self) { self.issues.extend(other.issues); self.metrics.extend(other.metrics); } @@ -415,7 +415,7 @@ mod tests { IssueCategory::Error, IssueScope::UserInput, "Invalid input", - Some("Missing field"), + Some("Missing field".into()), ); assert_eq!(summary.issues.len(), 1); @@ -442,14 +442,14 @@ mod tests { IssueCategory::Error, IssueScope::UserInput, "Invalid input", - Some("Missing field"), + Some("Missing field".into()), ); summary.add_issue( IssueCategory::Error, IssueScope::UserInput, "Invalid input", - Some("Missing field"), + Some("Missing field".into()), ); assert_eq!(summary.issues.len(), 1); @@ -485,21 +485,21 @@ mod tests { IssueCategory::Error, IssueScope::UserInput, "Invalid input", - Some("Missing field"), + Some("Missing field".into()), ); summary.add_issue( IssueCategory::Error, IssueScope::UserInput, "Invalid input", - Some("Missing field"), + Some("Missing field".into()), ); summary.add_issue( IssueCategory::Warning, IssueScope::Internal, "Pack not found", - Some("Inconsistent state on disk"), + Some("Inconsistent state on disk".into()), ); summary.add_metric("execution_time", "5s"); @@ -554,7 +554,7 @@ mod tests { IssueCategory::Error, IssueScope::UserInput, "Invalid input", - Some("Missing field"), + Some("Missing field".into()), ); summary.add_metric("execution_time", "5s"); @@ -567,7 +567,7 @@ mod tests { IssueCategory::Error, IssueScope::UserInput, "Invalid input", - Some("Missing field"), + Some("Missing field".into()), ); other_summary.add_metric("execution_time", "5s"); From a27dc6e0e84dd75277025ed3f7523aceff2c4966 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:31:16 +0100 Subject: [PATCH 14/18] feat: add methods to convert Summary into boxed and arc mutex types Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 5835a45d..21d5a86e 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -31,6 +31,7 @@ use std::{ collections::{BTreeMap, HashSet}, fmt::{self, Display, Write}, + sync::{Arc, Mutex}, time::Instant, }; @@ -292,6 +293,14 @@ impl Summary { self.issues.extend(other.issues); self.metrics.extend(other.metrics); } + + pub fn into_boxed(self) -> Box { + Box::new(self) + } + + pub fn into_arc_mutex(self) -> Arc> { + Arc::new(Mutex::new(self)) + } } // Display Helpers From ddf7828910ddd5b5de3b63a5e5abd4f5f41b05bd Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:50:20 +0100 Subject: [PATCH 15/18] feat: rename is_error method to contains_error and improve error checking logic Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 21d5a86e..f1f3b9b5 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -155,8 +155,9 @@ impl Summary { self.clone() } - pub fn is_error(&self) -> bool { + pub fn contains_error(&self) -> bool { self.issues.contains_key(&IssueCategory::Error) + && !self.issues[&IssueCategory::Error].is_empty() } pub fn add_error( From 4b3d073e009a127f60f12663e5ee028c0d6e3c50 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:10:42 +0100 Subject: [PATCH 16/18] feat: add method to retrieve Summary from Arc with error handling Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index f1f3b9b5..7e5e678b 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -37,6 +37,8 @@ use std::{ use ecow::EcoString; +use crate::error::{ErrorKind, RusticError, RusticResult}; + pub type Issues = BTreeMap>; pub type Metrics = BTreeMap; @@ -302,6 +304,21 @@ impl Summary { pub fn into_arc_mutex(self) -> Arc> { Arc::new(Mutex::new(self)) } + + pub fn retrieve_from_arc_mutex(arc_mutex: Arc>) -> RusticResult { + Arc::try_unwrap(arc_mutex) + .map_err(|_err| { + RusticError::new(ErrorKind::Internal, "Error unwrapping Mutex from Arc.") + })? + .into_inner() + .map_err(|err| { + RusticError::with_source( + ErrorKind::Internal, + "Mutex poisoned while getting summary for check command.", + err, + ) + }) + } } // Display Helpers From 08a66fb049ed4aba4ad2d3528f0fe57242de9e72 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:15:18 +0100 Subject: [PATCH 17/18] feat: update retrieve_from_arc_mutex method to use Self type Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 7e5e678b..0c7c00d8 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -305,7 +305,7 @@ impl Summary { Arc::new(Mutex::new(self)) } - pub fn retrieve_from_arc_mutex(arc_mutex: Arc>) -> RusticResult { + pub fn retrieve_from_arc_mutex(arc_mutex: Arc>) -> RusticResult { Arc::try_unwrap(arc_mutex) .map_err(|_err| { RusticError::new(ErrorKind::Internal, "Error unwrapping Mutex from Arc.") From f30bb6ecde19f35247e0a670a3db3fc07536fd56 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:22:51 +0100 Subject: [PATCH 18/18] feat: add assertions to tests for contains_error method in Summary Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/core/src/error/summary.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/core/src/error/summary.rs b/crates/core/src/error/summary.rs index 0c7c00d8..96dd450b 100644 --- a/crates/core/src/error/summary.rs +++ b/crates/core/src/error/summary.rs @@ -447,6 +447,8 @@ mod tests { assert_eq!(summary.issues.len(), 1); + assert!(summary.contains_error()); + let user_input_issues = summary .issues .get(&IssueCategory::Error) @@ -481,6 +483,8 @@ mod tests { assert_eq!(summary.issues.len(), 1); + assert!(summary.contains_error()); + let user_input_issues = summary.issues.get(&IssueCategory::Error).unwrap(); let issue = user_input_issues.get(&IssueScope::UserInput).unwrap(); @@ -533,6 +537,8 @@ mod tests { summary.complete(); + assert!(summary.contains_error()); + let display_output = format!("{summary}"); assert!(display_output.contains("Context: Check")); @@ -601,6 +607,8 @@ mod tests { other_summary.complete(); + assert!(summary.contains_error()); + summary.merge(other_summary); assert_eq!(summary.issues.len(), 1);