Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions crates/core/src/decode/mappings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ pub mod budget;
pub mod context;
pub mod storage;
pub mod value;

#[cfg(test)]
mod severity_tests;
282 changes: 282 additions & 0 deletions crates/core/src/decode/mappings/severity_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
/// Tests that verify each HostError variant maps to the correct `Severity` level.
///
/// Coverage: Fatal, Error, Warning, and Info are each exercised at least once.
#[cfg(test)]
mod tests {
use crate::decode::mappings::{budget, context, value};
use crate::taxonomy::loader::TaxonomyDatabase;
use crate::taxonomy::schema::ErrorCategory;
use crate::types::report::Severity;

// ------------------------------------------------------------------
// ErrorSeverity → Severity conversion
// ------------------------------------------------------------------

#[test]
fn error_severity_critical_maps_to_fatal() {
// Budget code 0 (CPUExceeded) is ErrorSeverity::Critical → Severity::Fatal
let detail = budget::lookup(0).expect("budget code 0 exists");
let severity: Severity = detail.severity.clone().into();
assert_eq!(severity, Severity::Fatal);
}

#[test]
fn error_severity_error_maps_to_error() {
// Budget code 8 (ExceededLimit) is ErrorSeverity::Error → Severity::Error
let detail = budget::lookup(8).expect("budget code 8 exists");
let severity: Severity = detail.severity.clone().into();
assert_eq!(severity, Severity::Error);
}

#[test]
fn error_severity_warning_maps_to_warning() {
use crate::decode::mappings::budget::ErrorSeverity;
let severity: Severity = ErrorSeverity::Warning.into();
assert_eq!(severity, Severity::Warning);
}

#[test]
fn error_severity_info_maps_to_info() {
use crate::decode::mappings::budget::ErrorSeverity;
let severity: Severity = ErrorSeverity::Info.into();
assert_eq!(severity, Severity::Info);
}

// ------------------------------------------------------------------
// Value mapping severity checks
// ------------------------------------------------------------------

#[test]
fn value_internal_error_maps_to_fatal() {
// Value code 4 (InternalError) is ErrorSeverity::Critical → Fatal
let detail = value::lookup(4).expect("value code 4 exists");
let severity: Severity = detail.severity.clone().into();
assert_eq!(severity, Severity::Fatal);
}

#[test]
fn value_invalid_input_maps_to_error() {
let detail = value::lookup(6).expect("value code 6 exists");
let severity: Severity = detail.severity.clone().into();
assert_eq!(severity, Severity::Error);
}

// ------------------------------------------------------------------
// Context mapping severity checks (uses Severity directly)
// ------------------------------------------------------------------

#[test]
fn context_internal_error_is_fatal() {
let detail = context::lookup(7).expect("context code 7 exists");
assert_eq!(detail.severity, Severity::Fatal);
}

#[test]
fn context_invalid_action_is_error() {
let detail = context::lookup(6).expect("context code 6 exists");
assert_eq!(detail.severity, Severity::Error);
}

// ------------------------------------------------------------------
// Warning severity — mapping table and build_report
// ------------------------------------------------------------------

#[test]
fn storage_near_expiry_maps_to_warning() {
use crate::decode::mappings::storage;
// Storage code 4 (NearExpiry) is the canonical Warning entry.
let detail = storage::lookup(4).expect("storage code 4 exists");
assert_eq!(detail.severity, Severity::Warning);
}

#[test]
fn taxonomy_warning_severity_is_correctly_parsed() {
let db = TaxonomyDatabase::load_embedded().expect("taxonomy loads");
let entry = db
.lookup(&ErrorCategory::Storage, 4)
.expect("storage code 4 in taxonomy");
assert_eq!(entry.severity, "Warning");
}

// ------------------------------------------------------------------
// Taxonomy-driven build_report severity checks
// ------------------------------------------------------------------

#[test]
fn build_report_context_internal_error_is_fatal() {
use crate::decode::host_error::ClassifiedError;
use crate::decode::report::build_report;

let classified = ClassifiedError {
category: ErrorCategory::Context,
error_code: 7,
is_contract_error: false,
contract_id: None,
raw_data: serde_json::Value::Null,
};
let report = build_report(&classified).expect("report should build");
assert_eq!(report.severity, Severity::Fatal);
}

#[test]
fn build_report_auth_missing_auth_is_error() {
use crate::decode::host_error::ClassifiedError;
use crate::decode::report::build_report;

let classified = ClassifiedError {
category: ErrorCategory::Auth,
error_code: 2,
is_contract_error: false,
contract_id: None,
raw_data: serde_json::Value::Null,
};
let report = build_report(&classified).expect("report should build");
assert_eq!(report.severity, Severity::Error);
}

#[test]
fn build_report_storage_near_expiry_is_warning() {
use crate::decode::host_error::ClassifiedError;
use crate::decode::report::build_report;

let classified = ClassifiedError {
category: ErrorCategory::Storage,
error_code: 4,
is_contract_error: false,
contract_id: None,
raw_data: serde_json::Value::Null,
};
let report = build_report(&classified).expect("report should build");
assert_eq!(report.severity, Severity::Warning);
}

#[test]
fn build_report_budget_approaching_limit_is_info() {
use crate::decode::host_error::ClassifiedError;
use crate::decode::report::build_report;

let classified = ClassifiedError {
category: ErrorCategory::Budget,
error_code: 3,
is_contract_error: false,
contract_id: None,
raw_data: serde_json::Value::Null,
};
let report = build_report(&classified).expect("report should build");
assert_eq!(report.severity, Severity::Info);
}

#[test]
fn build_report_unknown_code_defaults_to_error() {
use crate::decode::host_error::ClassifiedError;
use crate::decode::report::build_report;

let classified = ClassifiedError {
category: ErrorCategory::Budget,
error_code: 9999,
is_contract_error: false,
contract_id: None,
raw_data: serde_json::Value::Null,
};
let report = build_report(&classified).expect("report should build");
assert_eq!(report.severity, Severity::Error);
}

// ------------------------------------------------------------------
// Taxonomy severity parsing
// ------------------------------------------------------------------

#[test]
fn taxonomy_fatal_severity_is_correctly_parsed() {
let db = TaxonomyDatabase::load_embedded().expect("taxonomy loads");
let entry = db
.lookup(&ErrorCategory::Context, 7)
.expect("context code 7 in taxonomy");
assert_eq!(entry.severity, "Fatal");
}

#[test]
fn taxonomy_error_severity_is_correctly_parsed() {
let db = TaxonomyDatabase::load_embedded().expect("taxonomy loads");
let entry = db
.lookup(&ErrorCategory::Auth, 1)
.expect("auth code 1 in taxonomy");
assert_eq!(entry.severity, "Error");
}

// ------------------------------------------------------------------
// Exhaustive: every mapping-table entry has a valid severity
// ------------------------------------------------------------------

#[test]
fn all_budget_entries_have_valid_severity() {
use crate::decode::mappings::budget::BUDGET_ERROR_DETAILS;

for entry in BUDGET_ERROR_DETAILS {
let sev: Severity = entry.severity.clone().into();
assert!(
matches!(sev, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info),
"Unexpected severity for budget code {}: {:?}",
entry.code,
sev
);
}
}

#[test]
fn all_value_entries_have_valid_severity() {
use crate::decode::mappings::value::VALUE_ERROR_DETAILS;

for entry in VALUE_ERROR_DETAILS {
let sev: Severity = entry.severity.clone().into();
assert!(
matches!(sev, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info),
"Unexpected severity for value code {}: {:?}",
entry.code,
sev
);
}
}

#[test]
fn all_storage_entries_have_valid_severity() {
use crate::decode::mappings::storage::STORAGE_ERROR_DETAILS;

for entry in STORAGE_ERROR_DETAILS {
assert!(
matches!(entry.severity, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info),
"Unexpected severity for storage code {}: {:?}",
entry.code,
entry.severity
);
}
}

#[test]
fn all_context_entries_have_valid_severity() {
use crate::decode::mappings::context::CONTEXT_ERROR_DETAILS;

for entry in CONTEXT_ERROR_DETAILS {
assert!(
matches!(entry.severity, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info),
"Unexpected severity for context code {}: {:?}",
entry.code,
entry.severity
);
}
}

#[test]
fn all_auth_entries_have_valid_severity() {
use crate::decode::mappings::auth::AUTH_ERROR_DETAILS;

for entry in AUTH_ERROR_DETAILS {
assert!(
matches!(entry.severity, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info),
"Unexpected severity for auth code {}: {:?}",
entry.code,
entry.severity
);
}
}
}
12 changes: 8 additions & 4 deletions crates/core/src/decode/mappings/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ pub const STORAGE_ERROR_DETAILS: &[StorageErrorDetail] = &[
summary: "An internal storage failure occurred. This code alone reveals nothing; check the diagnostic events to get more signal on the underlying issue.",
severity: Severity::Error,
},
StorageErrorDetail {
code: 4,
name: "NearExpiry",
summary: "The accessed ledger entry is approaching its expiration ledger and will soon become read-only unless extended.",
severity: Severity::Warning,
},
];

pub fn lookup(code: u32) -> Option<&'static StorageErrorDetail> {
Expand All @@ -58,10 +64,8 @@ mod tests {

#[test]
fn table_covers_known_storage_codes() {
assert_eq!(STORAGE_ERROR_DETAILS.len(), 4);
assert!(STORAGE_ERROR_DETAILS
.iter()
.all(|detail| detail.severity == Severity::Error));
assert_eq!(STORAGE_ERROR_DETAILS.len(), 5);
assert!(lookup(4).is_some());
assert!(lookup(99).is_none());
}
}
26 changes: 26 additions & 0 deletions crates/core/src/taxonomy/data/budget.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,29 @@ requires_upgrade = true
related_errors = ["host.budget.limit_exceeded.memory"]
source_file = "soroban-env-host/src/budget.rs"
documentation_url = "https://soroban.stellar.org/docs/fundamentals/fees-and-metering"

[[errors]]
id = "host.budget.approaching_limit"
category = "budget"
code = 3
name = "ApproachingLimit"
severity = "Info"
since_protocol = 20
summary = "The transaction is approaching its resource budget limit."
detailed_explanation = """
This is a diagnostic signal, not a failure. The execution consumed a high fraction of the \
allocated budget but did not exceed it. Use this to identify contracts that are close to the \
limit and may fail under slightly heavier load.
"""

[[errors.common_causes]]
description = "Contract logic that scales with input size, approaching the declared limit"
likelihood = "medium"

[[errors.suggested_fixes]]
description = "Profile the contract and reduce unnecessary computation before it becomes a hard failure"
difficulty = "medium"
requires_upgrade = true

related_errors = ["host.budget.limit_exceeded.cpu"]
source_file = "soroban-env-host/src/budget.rs"
27 changes: 27 additions & 0 deletions crates/core/src/taxonomy/data/storage.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,30 @@ requires_upgrade = false

related_errors = ["host.storage.missing_key"]
source_file = "soroban-env-host/src/storage.rs"

[[errors]]
id = "host.storage.near_expiry"
category = "storage"
code = 4
name = "NearExpiry"
severity = "Warning"
since_protocol = 20
summary = "The accessed ledger entry is approaching its expiration ledger and will soon become read-only unless extended."
detailed_explanation = """
Soroban ledger entries have a time-to-live measured in ledgers. This warning indicates that a \
contract read or written an entry that is close to expiring. The operation succeeded, but the \
entry must be extended with extendTTL before it archives or future transactions will fail with \
EntryNotFound. This is not a blocking error — it is a signal to act before the entry is lost.
"""

[[errors.common_causes]]
description = "Contract data was created or last extended many ledgers ago and TTL refresh was missed"
likelihood = "high"

[[errors.suggested_fixes]]
description = "Call extendTTL on the entry to push the expiration ledger further into the future"
difficulty = "easy"
requires_upgrade = false

related_errors = ["host.storage.entry_not_found"]
source_file = "soroban-env-host/src/storage.rs"