use crate::{core::DomainReason, StructError};
use super::{
context::OperationResult, report::DiagnosticReport, ErrorCategory, ErrorIdentityProvider,
ErrorMetadata, OperationContext, SourceFrame,
};
pub const STABLE_SNAPSHOT_SCHEMA_VERSION: &str = "orion-error.snapshot.v2";
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct SnapshotContextFrame {
pub target: Option<String>,
pub action: Option<String>,
pub locator: Option<String>,
pub path: Vec<String>,
pub metadata: ErrorMetadata,
pub fields: Vec<(String, String)>,
pub result: OperationResult,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct StableSnapshotContextFrame {
pub target: Option<String>,
pub action: Option<String>,
pub locator: Option<String>,
pub path: Vec<String>,
pub metadata: ErrorMetadata,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct SnapshotSourceFrame {
pub index: usize,
pub message: String,
pub display: Option<String>,
pub type_name: Option<String>,
pub error_code: Option<i32>,
pub reason: Option<String>,
pub want: Option<String>,
pub path: Option<String>,
pub detail: Option<String>,
pub metadata: ErrorMetadata,
pub is_root_cause: bool,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct StableSnapshotSourceFrame {
pub index: usize,
pub message: String,
pub error_code: Option<i32>,
pub reason: Option<String>,
pub want: Option<String>,
pub path: Option<String>,
pub detail: Option<String>,
pub metadata: ErrorMetadata,
pub is_root_cause: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ErrorSnapshot {
pub reason: String,
pub detail: Option<String>,
pub position: Option<String>,
pub want: Option<String>,
pub path: Option<String>,
pub category: ErrorCategory,
pub code: String,
pub context: Vec<SnapshotContextFrame>,
pub root_metadata: ErrorMetadata,
pub source_frames: Vec<SnapshotSourceFrame>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct StableErrorSnapshot {
pub schema_version: &'static str,
pub reason: String,
pub detail: Option<String>,
pub position: Option<String>,
pub want: Option<String>,
pub path: Option<String>,
#[cfg_attr(feature = "serde", serde(skip))]
pub category: ErrorCategory,
#[cfg_attr(feature = "serde", serde(skip))]
pub code: String,
pub context: Vec<StableSnapshotContextFrame>,
pub root_metadata: ErrorMetadata,
pub source_frames: Vec<StableSnapshotSourceFrame>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorIdentity {
pub code: String,
pub category: ErrorCategory,
pub reason: String,
pub detail: Option<String>,
pub position: Option<String>,
pub want: Option<String>,
pub path: Option<String>,
}
impl ErrorSnapshot {
pub fn stable_context(&self) -> &[SnapshotContextFrame] {
&self.context
}
pub fn stable_source_frames(&self) -> &[SnapshotSourceFrame] {
&self.source_frames
}
pub fn root_source_frame(&self) -> Option<&SnapshotSourceFrame> {
self.source_frames.iter().find(|frame| frame.is_root_cause)
}
pub fn stable_export(&self) -> StableErrorSnapshot {
self.clone().into_stable_export()
}
pub fn into_stable_export(self) -> StableErrorSnapshot {
StableErrorSnapshot {
schema_version: STABLE_SNAPSHOT_SCHEMA_VERSION,
reason: self.reason,
detail: self.detail,
position: self.position,
want: self.want,
path: self.path,
category: self.category,
code: self.code,
context: self.context.into_iter().map(Into::into).collect(),
root_metadata: self.root_metadata,
source_frames: self.source_frames.into_iter().map(Into::into).collect(),
}
}
#[cfg(feature = "serde_json")]
pub fn to_stable_snapshot_json(&self) -> serde_json::Result<serde_json::Value> {
serde_json::to_value(self.stable_export())
}
pub fn report(&self) -> DiagnosticReport {
self.clone().into_report()
}
pub fn into_report(self) -> DiagnosticReport {
DiagnosticReport {
reason: self.reason,
detail: self.detail,
position: self.position,
want: self.want,
path: self.path,
context: self.context.into_iter().map(Into::into).collect(),
root_metadata: self.root_metadata,
source_frames: self.source_frames.into_iter().map(Into::into).collect(),
}
}
}
impl StableErrorSnapshot {
pub fn report(&self) -> DiagnosticReport {
DiagnosticReport {
reason: self.reason.clone(),
detail: self.detail.clone(),
position: self.position.clone(),
want: self.want.clone(),
path: self.path.clone(),
context: self.context.iter().cloned().map(Into::into).collect(),
root_metadata: self.root_metadata.clone(),
source_frames: self.source_frames.iter().cloned().map(Into::into).collect(),
}
}
pub fn into_report(self) -> DiagnosticReport {
DiagnosticReport {
reason: self.reason,
detail: self.detail,
position: self.position,
want: self.want,
path: self.path,
context: self.context.into_iter().map(Into::into).collect(),
root_metadata: self.root_metadata,
source_frames: self.source_frames.into_iter().map(Into::into).collect(),
}
}
}
impl SnapshotContextFrame {
pub fn stable_export(&self) -> StableSnapshotContextFrame {
self.clone().into()
}
}
impl SnapshotSourceFrame {
pub fn stable_export(&self) -> StableSnapshotSourceFrame {
self.clone().into()
}
}
impl From<SnapshotContextFrame> for StableSnapshotContextFrame {
fn from(value: SnapshotContextFrame) -> Self {
StableSnapshotContextFrame {
target: value.target,
action: value.action,
locator: value.locator,
path: value.path,
metadata: value.metadata,
}
}
}
impl From<&SnapshotContextFrame> for StableSnapshotContextFrame {
fn from(value: &SnapshotContextFrame) -> Self {
value.clone().into()
}
}
impl From<StableSnapshotContextFrame> for SnapshotContextFrame {
fn from(value: StableSnapshotContextFrame) -> Self {
SnapshotContextFrame {
target: value.target,
action: value.action,
locator: value.locator,
path: value.path,
metadata: value.metadata,
fields: Vec::new(),
result: OperationResult::Fail,
}
}
}
impl From<&StableSnapshotContextFrame> for SnapshotContextFrame {
fn from(value: &StableSnapshotContextFrame) -> Self {
value.clone().into()
}
}
impl From<SnapshotSourceFrame> for StableSnapshotSourceFrame {
fn from(value: SnapshotSourceFrame) -> Self {
StableSnapshotSourceFrame {
index: value.index,
message: value.message,
error_code: value.error_code,
reason: value.reason,
want: value.want,
path: value.path,
detail: value.detail,
metadata: value.metadata,
is_root_cause: value.is_root_cause,
}
}
}
impl From<&SnapshotSourceFrame> for StableSnapshotSourceFrame {
fn from(value: &SnapshotSourceFrame) -> Self {
value.clone().into()
}
}
impl From<StableSnapshotSourceFrame> for SnapshotSourceFrame {
fn from(value: StableSnapshotSourceFrame) -> Self {
SnapshotSourceFrame {
index: value.index,
message: value.message,
display: None,
type_name: None,
error_code: value.error_code,
reason: value.reason,
want: value.want,
path: value.path,
detail: value.detail,
metadata: value.metadata,
is_root_cause: value.is_root_cause,
}
}
}
impl From<&StableSnapshotSourceFrame> for SnapshotSourceFrame {
fn from(value: &StableSnapshotSourceFrame) -> Self {
value.clone().into()
}
}
impl From<OperationContext> for SnapshotContextFrame {
fn from(value: OperationContext) -> Self {
Self {
target: value.target().clone(),
action: value.action().clone(),
locator: value.locator().clone(),
path: value.normalized_path_segments(),
metadata: value.metadata().clone(),
fields: value.context().items.clone(),
result: value.result().clone(),
}
}
}
impl From<SnapshotContextFrame> for OperationContext {
fn from(value: SnapshotContextFrame) -> Self {
let mut ctx = value
.target
.clone()
.map(OperationContext::from_target)
.unwrap_or_default();
ctx.replace_target_for_report(value.target);
ctx.replace_action_for_report(value.action);
ctx.replace_locator_for_report(value.locator);
ctx.replace_path_for_report(value.path);
ctx.context_mut_for_report().items = value.fields;
ctx.replace_metadata_for_report(value.metadata);
match value.result {
OperationResult::Suc => ctx.mark_suc(),
OperationResult::Fail => {}
OperationResult::Cancel => ctx.mark_cancel(),
}
ctx
}
}
impl From<StableSnapshotContextFrame> for OperationContext {
fn from(value: StableSnapshotContextFrame) -> Self {
SnapshotContextFrame::from(value).into()
}
}
impl From<&StableSnapshotContextFrame> for OperationContext {
fn from(value: &StableSnapshotContextFrame) -> Self {
value.clone().into()
}
}
impl From<SourceFrame> for SnapshotSourceFrame {
fn from(value: SourceFrame) -> Self {
Self {
index: value.index,
message: value.message,
display: value.display,
type_name: value.type_name,
error_code: value.error_code,
reason: value.reason,
want: value.want,
path: value.path,
detail: value.detail,
metadata: value.metadata,
is_root_cause: value.is_root_cause,
}
}
}
impl From<SnapshotSourceFrame> for SourceFrame {
fn from(value: SnapshotSourceFrame) -> Self {
Self {
index: value.index,
message: value.message,
display: value.display,
debug: String::new(),
type_name: value.type_name,
error_code: value.error_code,
reason: value.reason,
want: value.want,
path: value.path,
detail: value.detail,
metadata: value.metadata,
is_root_cause: value.is_root_cause,
}
}
}
impl From<StableSnapshotSourceFrame> for SourceFrame {
fn from(value: StableSnapshotSourceFrame) -> Self {
SnapshotSourceFrame::from(value).into()
}
}
impl From<&StableSnapshotSourceFrame> for SourceFrame {
fn from(value: &StableSnapshotSourceFrame) -> Self {
value.clone().into()
}
}
impl<T> StructError<T>
where
T: DomainReason + ErrorIdentityProvider,
{
pub fn snapshot(&self) -> ErrorSnapshot {
ErrorSnapshot {
reason: self.reason().to_string(),
detail: self.detail().clone(),
position: self.position().clone(),
want: self.target_main(),
path: self.target_path(),
category: self.error_category(),
code: self.stable_code().to_string(),
context: self.contexts().iter().cloned().map(Into::into).collect(),
root_metadata: self.context_metadata(),
source_frames: self
.source_frames()
.iter()
.cloned()
.map(Into::into)
.collect(),
}
}
pub fn into_snapshot(self) -> ErrorSnapshot {
self.snapshot()
}
pub fn identity_snapshot(&self) -> ErrorIdentity {
ErrorIdentity {
code: self.stable_code().to_string(),
category: self.error_category(),
reason: self.reason().to_string(),
detail: self.detail().clone(),
position: self.position().clone(),
want: self.target_main(),
path: self.target_path(),
}
}
}
impl<T> From<&StructError<T>> for ErrorSnapshot
where
T: DomainReason + ErrorIdentityProvider,
{
fn from(value: &StructError<T>) -> Self {
value.snapshot()
}
}
impl<T> From<StructError<T>> for ErrorSnapshot
where
T: DomainReason + ErrorIdentityProvider,
{
fn from(value: StructError<T>) -> Self {
value.into_snapshot()
}
}
impl<T> From<&StructError<T>> for StableErrorSnapshot
where
T: DomainReason + ErrorIdentityProvider,
{
fn from(value: &StructError<T>) -> Self {
value.snapshot().into_stable_export()
}
}
impl<T> From<StructError<T>> for StableErrorSnapshot
where
T: DomainReason + ErrorIdentityProvider,
{
fn from(value: StructError<T>) -> Self {
value.into_snapshot().into_stable_export()
}
}
impl From<&ErrorSnapshot> for StableErrorSnapshot {
fn from(value: &ErrorSnapshot) -> Self {
value.stable_export()
}
}
impl From<ErrorSnapshot> for StableErrorSnapshot {
fn from(value: ErrorSnapshot) -> Self {
value.into_stable_export()
}
}
#[cfg(test)]
mod tests {
use crate::{
core::{context::ContextRecord, DomainReason, ErrorMetadata, SourceFrame},
ErrorCategory, ErrorCode, ErrorIdentityProvider, OperationContext, StructError, UvsReason,
};
use super::{
DiagnosticReport, ErrorSnapshot, SnapshotContextFrame, SnapshotSourceFrame,
StableErrorSnapshot, StableSnapshotContextFrame, StableSnapshotSourceFrame,
STABLE_SNAPSHOT_SCHEMA_VERSION,
};
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
enum TestReason {
#[error("test error")]
TestError,
#[error("{0}")]
Uvs(UvsReason),
}
impl From<UvsReason> for TestReason {
fn from(value: UvsReason) -> Self {
Self::Uvs(value)
}
}
impl DomainReason for TestReason {}
impl ErrorCode for TestReason {
fn error_code(&self) -> i32 {
match self {
TestReason::TestError => 1001,
TestReason::Uvs(reason) => reason.error_code(),
}
}
}
impl ErrorIdentityProvider for TestReason {
fn stable_code(&self) -> &'static str {
match self {
TestReason::TestError => "test.test_error",
TestReason::Uvs(reason) => reason.stable_code(),
}
}
fn error_category(&self) -> ErrorCategory {
match self {
TestReason::TestError => ErrorCategory::Logic,
TestReason::Uvs(reason) => reason.error_category(),
}
}
}
#[test]
fn test_snapshot_captures_runtime_fields_and_source_frames() {
let source = StructError::from(TestReason::TestError).with_context(
OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
);
let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
.with_detail("engine bootstrap failed")
.with_position("src/main.rs:42")
.with_context(
OperationContext::doing("start engine").with_meta("component.name", "engine"),
)
.with_struct_source(source);
let snapshot = err.snapshot();
assert_eq!(snapshot.reason, "system error");
assert_eq!(snapshot.detail.as_deref(), Some("engine bootstrap failed"));
assert_eq!(snapshot.position.as_deref(), Some("src/main.rs:42"));
assert_eq!(snapshot.want.as_deref(), Some("start engine"));
assert_eq!(snapshot.context[0].target.as_deref(), Some("start engine"));
assert_eq!(
snapshot.root_metadata.get_str("component.name"),
Some("engine")
);
assert_eq!(
snapshot.source_frames[0].metadata.get_str("config.kind"),
Some("sink_defaults")
);
}
#[test]
fn test_identity_snapshot_captures_stable_identity_fields() {
let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
.with_detail("engine bootstrap failed")
.with_position("src/main.rs:42")
.with_context(OperationContext::doing("start engine"));
let identity = err.identity_snapshot();
assert_eq!(identity.code, "sys.io_error");
assert_eq!(identity.category, ErrorCategory::Sys);
assert_eq!(identity.reason, "system error");
assert_eq!(identity.detail.as_deref(), Some("engine bootstrap failed"));
assert_eq!(identity.position.as_deref(), Some("src/main.rs:42"));
assert_eq!(identity.want.as_deref(), Some("start engine"));
assert_eq!(identity.path.as_deref(), Some("start engine"));
}
#[test]
fn test_snapshot_preserves_action_and_locator_context_fields() {
let mut ctx = OperationContext::at("config.toml");
ctx.with_doing("parse config");
let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
.with_context(
OperationContext::doing("load config").with_meta("component.name", "engine"),
)
.with_context(ctx);
let snapshot = err.snapshot();
assert_eq!(snapshot.context[0].action.as_deref(), Some("load config"));
assert_eq!(snapshot.context[1].action.as_deref(), Some("parse config"));
assert_eq!(snapshot.context[1].locator.as_deref(), Some("config.toml"));
let report = snapshot.into_report();
assert_eq!(report.context[1].action().as_deref(), Some("parse config"));
assert_eq!(report.context[1].locator().as_deref(), Some("config.toml"));
}
#[test]
fn test_snapshot_report_conversion_preserves_payload() {
let snapshot = ErrorSnapshot {
reason: "system error".to_string(),
detail: Some("engine bootstrap failed".to_string()),
position: Some("src/main.rs:42".to_string()),
want: Some("start engine".to_string()),
path: Some("start engine / load defaults".to_string()),
context: vec![SnapshotContextFrame {
target: Some("start engine".to_string()),
action: None,
locator: None,
path: vec!["start engine".to_string()],
metadata: ErrorMetadata::new(),
fields: vec![],
result: crate::core::context::OperationResult::Fail,
}],
root_metadata: {
let mut metadata = ErrorMetadata::new();
metadata.insert("component.name", "engine");
metadata
},
source_frames: vec![],
category: ErrorCategory::Sys,
code: "sys.test_error".to_string(),
};
let report = snapshot.report();
assert_eq!(report.reason, snapshot.reason);
assert_eq!(report.detail, snapshot.detail);
assert_eq!(report.position, snapshot.position);
assert_eq!(report.want, snapshot.want);
assert_eq!(report.path, snapshot.path);
assert_eq!(
report.context,
snapshot
.context
.clone()
.into_iter()
.map(Into::into)
.collect::<Vec<OperationContext>>()
);
assert_eq!(report.root_metadata, snapshot.root_metadata);
assert_eq!(
report.source_frames,
snapshot
.source_frames
.clone()
.into_iter()
.map(Into::into)
.collect::<Vec<SourceFrame>>()
);
}
#[test]
fn test_snapshot_from_struct_error_matches_snapshot_method() {
let err = StructError::from(TestReason::TestError)
.with_detail("engine bootstrap failed")
.with_context(OperationContext::doing("start engine"));
let via_method = err.snapshot();
let via_from = ErrorSnapshot::from(&err);
assert_eq!(via_from, via_method);
}
#[test]
fn test_snapshot_from_owned_struct_error_matches_snapshot_method() {
let err = StructError::from(TestReason::TestError)
.with_detail("engine bootstrap failed")
.with_context(OperationContext::doing("start engine"));
let via_method = err.snapshot();
let via_from = ErrorSnapshot::from(err);
assert_eq!(via_from, via_method);
}
#[test]
fn test_struct_error_into_snapshot_matches_snapshot_method() {
let err = StructError::from(TestReason::TestError)
.with_detail("engine bootstrap failed")
.with_context(OperationContext::doing("start engine"));
let via_method = err.snapshot();
let via_into = err.into_snapshot();
assert_eq!(via_into, via_method);
}
#[test]
fn test_snapshot_into_report_matches_borrowed_report() {
let snapshot = ErrorSnapshot {
reason: "system error".to_string(),
detail: Some("engine bootstrap failed".to_string()),
position: Some("src/main.rs:42".to_string()),
want: Some("start engine".to_string()),
path: Some("start engine".to_string()),
context: vec![SnapshotContextFrame {
target: Some("start engine".to_string()),
action: None,
locator: None,
path: vec!["start engine".to_string()],
metadata: ErrorMetadata::new(),
fields: vec![("tenant".to_string(), "alpha".to_string())],
result: crate::core::context::OperationResult::Fail,
}],
root_metadata: ErrorMetadata::new(),
source_frames: vec![SnapshotSourceFrame {
index: 0,
message: "db unavailable".to_string(),
display: Some("db unavailable".to_string()),
type_name: Some("std::io::Error".to_string()),
error_code: None,
reason: None,
want: Some("load config".to_string()),
path: Some("load config / read".to_string()),
detail: Some("inner detail".to_string()),
metadata: ErrorMetadata::new(),
is_root_cause: true,
}],
category: ErrorCategory::Sys,
code: "sys.test_error".to_string(),
};
let via_borrowed = snapshot.report();
let via_owned = snapshot.clone().into_report();
let via_from = DiagnosticReport::from(snapshot);
assert_eq!(via_owned, via_borrowed);
assert_eq!(via_from, via_borrowed);
}
#[test]
fn test_snapshot_stable_helpers_prefer_snapshot_native_frames() {
let source = StructError::from(TestReason::TestError)
.with_detail("inner detail")
.with_context(
OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
);
let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
.with_detail("outer detail")
.with_context(OperationContext::doing("start engine"))
.with_struct_source(source);
let snapshot = err.snapshot();
assert_eq!(snapshot.stable_context(), snapshot.context.as_slice());
assert_eq!(
snapshot.stable_source_frames(),
snapshot.source_frames.as_slice()
);
assert_eq!(snapshot.root_source_frame().unwrap().message, "test error");
assert_eq!(
snapshot
.root_source_frame()
.unwrap()
.metadata
.get_str("config.kind"),
Some("sink_defaults")
);
}
#[test]
fn test_snapshot_stable_export_strips_compat_projection_fields() {
let source = StructError::from(TestReason::TestError)
.with_detail("inner detail")
.with_context(
OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
);
let mut outer = OperationContext::at("engine.toml");
outer.with_doing("start engine");
let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
.with_detail("outer detail")
.with_context(outer)
.with_struct_source(source);
let snapshot = err.snapshot();
let stable = snapshot.stable_export();
assert_eq!(stable.schema_version, STABLE_SNAPSHOT_SCHEMA_VERSION);
assert_eq!(stable.reason, snapshot.reason);
assert_eq!(stable.context[0].target.as_deref(), Some("start engine"));
assert_eq!(stable.context[0].action.as_deref(), Some("start engine"));
assert_eq!(stable.context[0].locator.as_deref(), Some("engine.toml"));
assert_eq!(
stable.context[0].path,
vec!["start engine".to_string(), "engine.toml".to_string()]
);
assert_eq!(
stable.source_frames[0].message,
snapshot.source_frames[0].message
);
assert_eq!(
stable.source_frames[0].metadata.get_str("config.kind"),
Some("sink_defaults")
);
}
#[test]
fn test_snapshot_into_stable_export_matches_borrowed_stable_export() {
let snapshot = ErrorSnapshot {
reason: "system error".to_string(),
detail: Some("outer detail".to_string()),
position: Some("src/main.rs:42".to_string()),
want: Some("start engine".to_string()),
path: Some("start engine".to_string()),
context: vec![SnapshotContextFrame {
target: Some("start engine".to_string()),
action: None,
locator: None,
path: vec!["start engine".to_string()],
metadata: ErrorMetadata::new(),
fields: vec![("tenant".to_string(), "alpha".to_string())],
result: crate::core::context::OperationResult::Fail,
}],
root_metadata: ErrorMetadata::new(),
source_frames: vec![SnapshotSourceFrame {
index: 0,
message: "db unavailable".to_string(),
display: Some("db unavailable".to_string()),
type_name: Some("std::io::Error".to_string()),
error_code: None,
reason: None,
want: Some("load config".to_string()),
path: Some("load config / read".to_string()),
detail: Some("inner detail".to_string()),
metadata: ErrorMetadata::new(),
is_root_cause: true,
}],
category: ErrorCategory::Sys,
code: "sys.test_error".to_string(),
};
let via_borrowed = snapshot.stable_export();
let via_owned = snapshot.clone().into_stable_export();
let via_from_borrowed = StableErrorSnapshot::from(&snapshot);
let via_from_owned = StableErrorSnapshot::from(snapshot);
assert_eq!(via_owned, via_borrowed);
assert_eq!(via_from_borrowed, via_borrowed);
assert_eq!(via_from_owned, via_borrowed);
assert_eq!(via_borrowed.schema_version, STABLE_SNAPSHOT_SCHEMA_VERSION);
}
#[test]
fn test_stable_snapshot_from_struct_error_matches_snapshot_stable_export() {
let source = StructError::from(TestReason::TestError)
.with_detail("inner detail")
.with_context(
OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
);
let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
.with_detail("outer detail")
.with_context(OperationContext::doing("start engine"))
.with_struct_source(source);
let via_method = err.snapshot().stable_export();
let via_borrowed = StableErrorSnapshot::from(&err);
let via_owned = StableErrorSnapshot::from(err);
assert_eq!(via_borrowed, via_method);
assert_eq!(via_owned, via_method);
}
#[test]
fn test_snapshot_frame_stable_from_matches_stable_export() {
let context = SnapshotContextFrame {
target: Some("start engine".to_string()),
action: None,
locator: None,
path: vec!["start engine".to_string()],
metadata: ErrorMetadata::new(),
fields: vec![("tenant".to_string(), "alpha".to_string())],
result: crate::core::context::OperationResult::Fail,
};
let source = SnapshotSourceFrame {
index: 0,
message: "db unavailable".to_string(),
display: Some("db unavailable".to_string()),
type_name: Some("std::io::Error".to_string()),
error_code: None,
reason: None,
want: Some("load config".to_string()),
path: Some("load config / read".to_string()),
detail: Some("inner detail".to_string()),
metadata: ErrorMetadata::new(),
is_root_cause: true,
};
assert_eq!(
StableSnapshotContextFrame::from(&context),
context.stable_export()
);
assert_eq!(
StableSnapshotContextFrame::from(context.clone()),
context.stable_export()
);
assert_eq!(
StableSnapshotSourceFrame::from(&source),
source.stable_export()
);
assert_eq!(
StableSnapshotSourceFrame::from(source.clone()),
source.stable_export()
);
}
#[test]
fn test_stable_snapshot_into_report_matches_report() {
let stable = StableErrorSnapshot {
schema_version: STABLE_SNAPSHOT_SCHEMA_VERSION,
reason: "system error".to_string(),
detail: Some("outer detail".to_string()),
position: None,
want: Some("start engine".to_string()),
path: Some("start engine".to_string()),
context: vec![StableSnapshotContextFrame {
target: Some("start engine".to_string()),
action: None,
locator: None,
path: vec!["start engine".to_string()],
metadata: ErrorMetadata::new(),
}],
root_metadata: ErrorMetadata::new(),
source_frames: vec![StableSnapshotSourceFrame {
index: 0,
message: "db unavailable".to_string(),
error_code: None,
reason: None,
want: Some("load config".to_string()),
path: Some("load config / read".to_string()),
detail: Some("inner detail".to_string()),
metadata: ErrorMetadata::new(),
is_root_cause: true,
}],
category: ErrorCategory::Sys,
code: "sys.test_error".to_string(),
};
let via_method = stable.report();
let via_owned = stable.clone().into_report();
assert_eq!(via_owned, via_method);
}
#[test]
fn test_stable_frame_to_compat_frame_defaults_compat_fields() {
let context = StableSnapshotContextFrame {
target: Some("start engine".to_string()),
action: None,
locator: None,
path: vec!["start engine".to_string()],
metadata: ErrorMetadata::new(),
};
let source = StableSnapshotSourceFrame {
index: 0,
message: "db unavailable".to_string(),
error_code: None,
reason: None,
want: Some("load config".to_string()),
path: Some("load config / read".to_string()),
detail: Some("inner detail".to_string()),
metadata: ErrorMetadata::new(),
is_root_cause: true,
};
let compat_context = SnapshotContextFrame::from(&context);
let compat_source = SnapshotSourceFrame::from(&source);
assert_eq!(compat_context.target, context.target);
assert_eq!(compat_context.path, context.path);
assert_eq!(compat_context.fields, Vec::<(String, String)>::new());
assert_eq!(
compat_context.result,
crate::core::context::OperationResult::Fail
);
assert_eq!(compat_source.message, source.message);
assert_eq!(compat_source.display, None);
assert_eq!(compat_source.type_name, None);
}
#[test]
fn test_snapshot_context_frame_roundtrip_to_operation_context() {
let mut ctx = OperationContext::doing("start engine");
ctx.with_doing("load defaults");
ctx.record("tenant", "alpha");
ctx.record_meta("component.name", "engine");
let snapshot_frame = SnapshotContextFrame::from(ctx.clone());
let roundtrip: OperationContext = snapshot_frame.clone().into();
assert_eq!(snapshot_frame.target.as_deref(), Some("start engine"));
assert_eq!(
snapshot_frame.path,
vec!["start engine".to_string(), "load defaults".to_string()]
);
assert_eq!(roundtrip.target().as_deref(), Some("start engine"));
assert_eq!(
roundtrip.path(),
vec!["start engine".to_string(), "load defaults".to_string()]
);
assert_eq!(
roundtrip.metadata().get_str("component.name"),
Some("engine")
);
assert_eq!(
roundtrip.context().items,
vec![("tenant".to_string(), "alpha".to_string())]
);
}
#[test]
fn test_snapshot_context_frame_roundtrip_normalizes_action_locator_path() {
let mut ctx = OperationContext::at("engine.toml");
ctx.with_doing("start engine");
let snapshot_frame = SnapshotContextFrame::from(ctx);
let roundtrip: OperationContext = snapshot_frame.clone().into();
assert_eq!(snapshot_frame.target.as_deref(), Some("start engine"));
assert_eq!(snapshot_frame.action.as_deref(), Some("start engine"));
assert_eq!(snapshot_frame.locator.as_deref(), Some("engine.toml"));
assert_eq!(
snapshot_frame.path,
vec!["start engine".to_string(), "engine.toml".to_string()]
);
assert_eq!(
roundtrip.path(),
vec!["start engine".to_string(), "engine.toml".to_string()]
);
assert_eq!(
roundtrip.path_string().as_deref(),
Some("start engine / engine.toml")
);
}
#[test]
fn test_snapshot_source_frame_roundtrip_to_report_frame() {
let frame = SnapshotSourceFrame {
index: 0,
message: "db unavailable".to_string(),
display: Some("db unavailable".to_string()),
type_name: Some("std::io::Error".to_string()),
error_code: None,
reason: None,
want: Some("load config".to_string()),
path: Some("load config / read".to_string()),
detail: Some("inner detail".to_string()),
metadata: {
let mut metadata = ErrorMetadata::new();
metadata.insert("config.kind", "sink_defaults");
metadata
},
is_root_cause: true,
};
let report_frame: SourceFrame = frame.clone().into();
let roundtrip = SnapshotSourceFrame::from(report_frame);
assert_eq!(roundtrip, frame);
}
#[cfg(feature = "serde_json")]
#[test]
fn test_to_stable_snapshot_json_uses_stable_export_shape() {
let snapshot = ErrorSnapshot {
reason: "system error".to_string(),
detail: Some("outer detail".to_string()),
position: None,
want: Some("start engine".to_string()),
path: Some("start engine".to_string()),
context: vec![SnapshotContextFrame {
target: Some("start engine".to_string()),
action: Some("start engine".to_string()),
locator: Some("engine.toml".to_string()),
path: vec!["start engine".to_string()],
metadata: ErrorMetadata::new(),
fields: vec![("tenant".to_string(), "alpha".to_string())],
result: crate::core::context::OperationResult::Fail,
}],
root_metadata: ErrorMetadata::new(),
source_frames: vec![SnapshotSourceFrame {
index: 0,
message: "db unavailable".to_string(),
display: Some("db unavailable".to_string()),
type_name: Some("std::io::Error".to_string()),
error_code: None,
reason: None,
want: Some("load config".to_string()),
path: Some("load config / read".to_string()),
detail: None,
metadata: ErrorMetadata::new(),
is_root_cause: true,
}],
category: ErrorCategory::Sys,
code: "sys.test_error".to_string(),
};
let json_value = snapshot.to_stable_snapshot_json().unwrap();
assert_eq!(
json_value,
serde_json::to_value(snapshot.stable_export()).unwrap()
);
assert_eq!(
json_value["schema_version"],
serde_json::json!(STABLE_SNAPSHOT_SCHEMA_VERSION)
);
assert_eq!(
json_value["context"][0]["action"],
serde_json::json!("start engine")
);
assert_eq!(
json_value["context"][0]["locator"],
serde_json::json!("engine.toml")
);
assert!(json_value["context"][0].get("fields").is_none());
assert!(json_value["source_frames"][0].get("display").is_none());
}
#[cfg(feature = "serde_json")]
#[test]
fn test_stable_snapshot_json_fields_match_schema_constants() {
let snapshot = ErrorSnapshot {
reason: "system error".to_string(),
detail: Some("outer detail".to_string()),
position: Some("src/main.rs:42".to_string()),
want: Some("start engine".to_string()),
path: Some("start engine".to_string()),
context: vec![SnapshotContextFrame {
target: Some("start engine".to_string()),
action: None,
locator: None,
path: vec!["start engine".to_string()],
metadata: ErrorMetadata::new(),
fields: vec![("tenant".to_string(), "alpha".to_string())],
result: crate::core::context::OperationResult::Fail,
}],
root_metadata: ErrorMetadata::new(),
source_frames: vec![SnapshotSourceFrame {
index: 0,
message: "db unavailable".to_string(),
display: Some("db unavailable".to_string()),
type_name: Some("std::io::Error".to_string()),
error_code: None,
reason: None,
want: Some("load config".to_string()),
path: Some("load config / read".to_string()),
detail: Some("inner detail".to_string()),
metadata: ErrorMetadata::new(),
is_root_cause: true,
}],
category: ErrorCategory::Sys,
code: "sys.test_error".to_string(),
};
let json_value = snapshot.to_stable_snapshot_json().unwrap();
let top_level = json_value.as_object().unwrap();
let context = json_value["context"][0].as_object().unwrap();
let source_frame = json_value["source_frames"][0].as_object().unwrap();
assert_eq!(
sorted_keys(top_level),
sorted_strings(&[
"schema_version",
"reason",
"detail",
"position",
"want",
"path",
"context",
"root_metadata",
"source_frames",
])
);
assert_eq!(
sorted_keys(context),
sorted_strings(&["target", "action", "locator", "path", "metadata"])
);
assert_eq!(
sorted_keys(source_frame),
sorted_strings(&[
"index",
"message",
"error_code",
"reason",
"want",
"path",
"detail",
"metadata",
"is_root_cause",
])
);
}
#[cfg(feature = "serde_json")]
fn sorted_keys(map: &serde_json::Map<String, serde_json::Value>) -> Vec<String> {
let mut keys = map.keys().cloned().collect::<Vec<_>>();
keys.sort();
keys
}
#[cfg(feature = "serde_json")]
fn sorted_strings(values: &[&str]) -> Vec<String> {
let mut values = values
.iter()
.map(|value| value.to_string())
.collect::<Vec<_>>();
values.sort();
values
}
}
#[cfg(doc)]
mod stable_snapshot_compile_fail_docs {
}