1#![allow(dead_code)]
34use serde::{Deserialize, Serialize};
35use std::collections::HashMap;
36use std::fmt;
37use thiserror::Error;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
41#[repr(u32)]
42pub enum ErrorCode {
43 TensorCreationFailed = 1000,
45 TensorOperationFailed = 1001,
46 ShapeMismatch = 1002,
47 DTypeMismatch = 1003,
48 DeviceMismatch = 1004,
49 TensorNotFound = 1005,
50
51 AllocationFailed = 2000,
53 MemoryPoolExhausted = 2001,
54 OutOfMemory = 2002,
55 MemoryLeakDetected = 2003,
56 InvalidPointer = 2004,
57 DanglingPointer = 2005,
58
59 InvalidConversion = 3000,
61 TypeNotSupported = 3001,
62 PrecisionLoss = 3002,
63 OverflowDetected = 3003,
64 UnderflowDetected = 3004,
65
66 InvalidParameter = 4000,
68 ParameterOutOfRange = 4001,
69 NullPointer = 4002,
70 InvalidShape = 4003,
71 InvalidDType = 4004,
72 InvalidDevice = 4005,
73
74 OperationNotSupported = 5000,
76 OperationFailed = 5001,
77 BroadcastingFailed = 5002,
78 MatrixNotInvertible = 5003,
79 DivisionByZero = 5004,
80
81 PythonError = 6000,
83 JavaError = 6001,
84 CSharpError = 6002,
85 GoError = 6003,
86 SwiftError = 6004,
87 WasmError = 6005,
88
89 FileNotFound = 7000,
91 FileReadError = 7001,
92 FileWriteError = 7002,
93 SerializationFailed = 7003,
94 DeserializationFailed = 7004,
95
96 ModuleNotFound = 8000,
98 ModuleInitFailed = 8001,
99 ModuleLoadError = 8002,
100
101 OwnershipConflict = 9000,
103 DataRace = 9001,
104 DeadlockDetected = 9002,
105
106 Unknown = 10000,
108 Internal = 10001,
109}
110
111impl ErrorCode {
112 pub fn category(&self) -> ErrorCategory {
114 match *self as u32 {
115 1000..=1999 => ErrorCategory::Tensor,
116 2000..=2999 => ErrorCategory::Memory,
117 3000..=3999 => ErrorCategory::TypeConversion,
118 4000..=4999 => ErrorCategory::Validation,
119 5000..=5999 => ErrorCategory::Operation,
120 6000..=6999 => ErrorCategory::LanguageBinding,
121 7000..=7999 => ErrorCategory::IO,
122 8000..=8999 => ErrorCategory::Module,
123 9000..=9999 => ErrorCategory::CrossLanguage,
124 _ => ErrorCategory::Unknown,
125 }
126 }
127
128 pub fn default_severity(&self) -> Severity {
130 match self {
131 Self::AllocationFailed
132 | Self::OutOfMemory
133 | Self::DeadlockDetected
134 | Self::DataRace => Severity::Critical,
135
136 Self::ShapeMismatch
137 | Self::DTypeMismatch
138 | Self::InvalidParameter
139 | Self::OperationFailed => Severity::Error,
140
141 Self::PrecisionLoss | Self::MemoryLeakDetected => Severity::Warning,
142
143 _ => Severity::Error,
144 }
145 }
146}
147
148impl fmt::Display for ErrorCode {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 write!(f, "{:?} ({})", self, *self as u32)
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
156pub enum ErrorCategory {
157 Tensor,
158 Memory,
159 TypeConversion,
160 Validation,
161 Operation,
162 LanguageBinding,
163 IO,
164 Module,
165 CrossLanguage,
166 Unknown,
167}
168
169impl fmt::Display for ErrorCategory {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 match self {
172 Self::Tensor => write!(f, "Tensor"),
173 Self::Memory => write!(f, "Memory"),
174 Self::TypeConversion => write!(f, "Type Conversion"),
175 Self::Validation => write!(f, "Validation"),
176 Self::Operation => write!(f, "Operation"),
177 Self::LanguageBinding => write!(f, "Language Binding"),
178 Self::IO => write!(f, "I/O"),
179 Self::Module => write!(f, "Module"),
180 Self::CrossLanguage => write!(f, "Cross-Language"),
181 Self::Unknown => write!(f, "Unknown"),
182 }
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
188pub enum Severity {
189 Info,
191 Warning,
193 Error,
195 Critical,
197}
198
199impl fmt::Display for Severity {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 match self {
202 Self::Critical => write!(f, "CRITICAL"),
203 Self::Error => write!(f, "ERROR"),
204 Self::Warning => write!(f, "WARNING"),
205 Self::Info => write!(f, "INFO"),
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
212pub struct SourceLocation {
213 pub file: String,
214 pub line: u32,
215 pub column: u32,
216}
217
218impl fmt::Display for SourceLocation {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 write!(f, "{}:{}:{}", self.file, self.line, self.column)
221 }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct EnhancedError {
227 pub code: ErrorCode,
229
230 pub message: String,
232
233 pub severity: Severity,
235
236 pub category: ErrorCategory,
238
239 pub location: Option<SourceLocation>,
241
242 pub context: HashMap<String, String>,
244
245 pub suggestions: Vec<String>,
247
248 pub timestamp: chrono::DateTime<chrono::Utc>,
250
251 pub causes: Vec<String>,
253}
254
255impl EnhancedError {
256 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
258 Self {
259 code,
260 message: message.into(),
261 severity: code.default_severity(),
262 category: code.category(),
263 location: None,
264 context: HashMap::new(),
265 suggestions: Vec::new(),
266 timestamp: chrono::Utc::now(),
267 causes: Vec::new(),
268 }
269 }
270
271 pub fn to_ffi_error(&self) -> FfiError {
273 FfiError::Enhanced(self.clone())
274 }
275
276 pub fn to_json(&self) -> Result<String, serde_json::Error> {
278 serde_json::to_string_pretty(self)
279 }
280
281 pub fn is_recoverable(&self) -> bool {
283 self.severity < Severity::Critical
284 }
285}
286
287impl fmt::Display for EnhancedError {
288 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289 writeln!(f, "[{}] {} - {}", self.severity, self.code, self.message)?;
290
291 if let Some(ref loc) = self.location {
292 writeln!(f, " at {}", loc)?;
293 }
294
295 if !self.context.is_empty() {
296 writeln!(f, " Context:")?;
297 for (key, value) in &self.context {
298 writeln!(f, " {}: {}", key, value)?;
299 }
300 }
301
302 if !self.suggestions.is_empty() {
303 writeln!(f, " Suggestions:")?;
304 for suggestion in &self.suggestions {
305 writeln!(f, " - {}", suggestion)?;
306 }
307 }
308
309 if !self.causes.is_empty() {
310 writeln!(f, " Caused by:")?;
311 for cause in &self.causes {
312 writeln!(f, " {}", cause)?;
313 }
314 }
315
316 Ok(())
317 }
318}
319
320impl std::error::Error for EnhancedError {}
321
322pub struct ErrorBuilder {
324 error: EnhancedError,
325}
326
327impl ErrorBuilder {
328 pub fn new(code: ErrorCode) -> Self {
330 Self {
331 error: EnhancedError::new(code, ""),
332 }
333 }
334
335 pub fn message(mut self, message: impl Into<String>) -> Self {
337 self.error.message = message.into();
338 self
339 }
340
341 pub fn severity(mut self, severity: Severity) -> Self {
343 self.error.severity = severity;
344 self
345 }
346
347 pub fn source_location(mut self, file: &str, line: u32, column: u32) -> Self {
349 self.error.location = Some(SourceLocation {
350 file: file.to_string(),
351 line,
352 column,
353 });
354 self
355 }
356
357 pub fn context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
359 self.error.context.insert(key.into(), value.into());
360 self
361 }
362
363 pub fn contexts(mut self, contexts: HashMap<String, String>) -> Self {
365 self.error.context.extend(contexts);
366 self
367 }
368
369 pub fn suggestion(mut self, suggestion: impl Into<String>) -> Self {
371 self.error.suggestions.push(suggestion.into());
372 self
373 }
374
375 pub fn suggestions(mut self, suggestions: Vec<String>) -> Self {
377 self.error.suggestions.extend(suggestions);
378 self
379 }
380
381 pub fn cause(mut self, cause: impl std::fmt::Display) -> Self {
383 self.error.causes.push(cause.to_string());
384 self
385 }
386
387 pub fn build(self) -> EnhancedError {
389 self.error
390 }
391
392 pub fn build_ffi(self) -> FfiError {
394 FfiError::Enhanced(self.error)
395 }
396}
397
398#[derive(Error, Debug, Clone)]
400pub enum FfiError {
401 #[error("{0}")]
403 Enhanced(EnhancedError),
404
405 #[error("Tensor error: {message}")]
406 Tensor { message: String },
407
408 #[error("Shape mismatch: expected {expected:?}, got {actual:?}")]
409 ShapeMismatch {
410 expected: Vec<usize>,
411 actual: Vec<usize>,
412 },
413
414 #[error("Data type mismatch: expected {expected}, got {actual}")]
415 DTypeMismatch { expected: String, actual: String },
416
417 #[error("Invalid conversion: {message}")]
418 InvalidConversion { message: String },
419
420 #[error("Python error: {message}")]
421 Python { message: String },
422
423 #[error("NumPy error: {message}")]
424 NumPy { message: String },
425
426 #[error("Memory allocation failed: {message}")]
427 AllocationFailed { message: String },
428
429 #[error("Invalid parameter: {parameter} = {value}")]
430 InvalidParameter { parameter: String, value: String },
431
432 #[error("Operation not supported: {operation}")]
433 UnsupportedOperation { operation: String },
434
435 #[error("Module error: {message}")]
436 Module { message: String },
437
438 #[error("Memory pool error: {message}")]
439 MemoryPool { message: String },
440
441 #[error("Cross-language ownership conflict: {message}")]
442 OwnershipConflict { message: String },
443
444 #[error("Device transfer error: {message}")]
445 DeviceTransfer { message: String },
446}
447
448#[cfg(feature = "python")]
449impl From<FfiError> for pyo3::PyErr {
450 fn from(err: FfiError) -> Self {
451 match err {
452 FfiError::Enhanced(enhanced) => {
453 use pyo3::exceptions::*;
455 let msg = enhanced.to_string();
456 match enhanced.category {
457 ErrorCategory::Memory => PyMemoryError::new_err(msg),
458 ErrorCategory::TypeConversion => PyTypeError::new_err(msg),
459 ErrorCategory::Validation => PyValueError::new_err(msg),
460 ErrorCategory::IO => PyIOError::new_err(msg),
461 ErrorCategory::Module => PyModuleNotFoundError::new_err(msg),
462 _ => PyRuntimeError::new_err(msg),
463 }
464 }
465 FfiError::Tensor { message } => pyo3::exceptions::PyRuntimeError::new_err(message),
466 FfiError::ShapeMismatch { expected, actual } => {
467 pyo3::exceptions::PyValueError::new_err(format!(
468 "Shape mismatch: expected {:?}, got {:?}",
469 expected, actual
470 ))
471 }
472 FfiError::DTypeMismatch { expected, actual } => pyo3::exceptions::PyTypeError::new_err(
473 format!("Data type mismatch: expected {}, got {}", expected, actual),
474 ),
475 FfiError::InvalidConversion { message } => {
476 pyo3::exceptions::PyValueError::new_err(message)
477 }
478 FfiError::Python { message } => pyo3::exceptions::PyRuntimeError::new_err(message),
479 FfiError::NumPy { message } => {
480 pyo3::exceptions::PyRuntimeError::new_err(format!("NumPy error: {}", message))
481 }
482 FfiError::AllocationFailed { message } => {
483 pyo3::exceptions::PyMemoryError::new_err(message)
484 }
485 FfiError::InvalidParameter { parameter, value } => {
486 pyo3::exceptions::PyValueError::new_err(format!(
487 "Invalid parameter: {} = {}",
488 parameter, value
489 ))
490 }
491 FfiError::UnsupportedOperation { operation } => {
492 pyo3::exceptions::PyNotImplementedError::new_err(format!(
493 "Operation not supported: {}",
494 operation
495 ))
496 }
497 FfiError::Module { message } => {
498 pyo3::exceptions::PyModuleNotFoundError::new_err(message)
499 }
500 FfiError::MemoryPool { message } => {
501 pyo3::exceptions::PyMemoryError::new_err(format!("Memory pool error: {}", message))
502 }
503 FfiError::OwnershipConflict { message } => pyo3::exceptions::PyRuntimeError::new_err(
504 format!("Ownership conflict: {}", message),
505 ),
506 FfiError::DeviceTransfer { message } => pyo3::exceptions::PyRuntimeError::new_err(
507 format!("Device transfer error: {}", message),
508 ),
509 }
510 }
511}
512
513impl From<std::fmt::Error> for FfiError {
515 fn from(err: std::fmt::Error) -> Self {
516 FfiError::InvalidConversion {
517 message: format!("Formatting error: {}", err),
518 }
519 }
520}
521
522impl From<std::io::Error> for FfiError {
523 fn from(err: std::io::Error) -> Self {
524 FfiError::InvalidConversion {
525 message: format!("IO error: {}", err),
526 }
527 }
528}
529
530impl From<torsh_core::error::TorshError> for FfiError {
531 fn from(err: torsh_core::error::TorshError) -> Self {
532 FfiError::Tensor {
533 message: format!("{}", err),
534 }
535 }
536}
537
538#[cfg(feature = "python")]
539pub fn fmt_error_to_pyerr(err: std::fmt::Error) -> pyo3::PyErr {
540 pyo3::exceptions::PyRuntimeError::new_err(format!("Formatting error: {}", err))
541}
542
543#[cfg(feature = "python")]
544pub fn torsh_error_to_pyerr(err: torsh_core::error::TorshError) -> pyo3::PyErr {
545 pyo3::exceptions::PyRuntimeError::new_err(format!("Tensor error: {}", err))
546}
547
548pub type FfiResult<T> = Result<T, FfiError>;
550
551#[cfg(feature = "python")]
552pub mod python_exceptions {
553 use pyo3::exceptions::PyException;
556 use pyo3::prelude::*;
557 use pyo3::types::PyAny;
558 use pyo3::{create_exception, Py, PyErr, Python};
559
560 create_exception!(
562 torsh,
563 TorshError,
564 PyException,
565 "Base exception for ToRSh operations"
566 );
567 create_exception!(torsh, TensorError, TorshError, "Tensor operation error");
568 create_exception!(torsh, ShapeError, TorshError, "Tensor shape related error");
569 create_exception!(torsh, DeviceError, TorshError, "Device operation error");
570 create_exception!(
571 torsh,
572 NumericalError,
573 TorshError,
574 "Numerical computation error"
575 );
576 create_exception!(torsh, MemoryError, TorshError, "Memory management error");
577
578 #[derive(Debug, Clone)]
580 pub struct ErrorContext {
581 pub operation: String,
582 pub file: Option<String>,
583 pub line: Option<u32>,
584 pub suggestion: Option<String>,
585 pub error_code: Option<i32>,
586 pub recoverable: bool,
587 }
588
589 impl ErrorContext {
590 pub fn new(operation: &str) -> Self {
591 Self {
592 operation: operation.to_string(),
593 file: None,
594 line: None,
595 suggestion: None,
596 error_code: None,
597 recoverable: false,
598 }
599 }
600
601 pub fn with_location(mut self, file: &str, line: u32) -> Self {
602 self.file = Some(file.to_string());
603 self.line = Some(line);
604 self
605 }
606
607 pub fn with_suggestion(mut self, suggestion: &str) -> Self {
608 self.suggestion = Some(suggestion.to_string());
609 self
610 }
611
612 pub fn with_error_code(mut self, code: i32) -> Self {
613 self.error_code = Some(code);
614 self
615 }
616
617 pub fn recoverable(mut self) -> Self {
618 self.recoverable = true;
619 self
620 }
621 }
622
623 pub fn create_enhanced_exception(
625 py: Python<'_>,
626 exc_type: &Py<PyAny>,
627 message: &str,
628 context: ErrorContext,
629 ) -> PyErr {
630 let exc = PyErr::new::<PyException, _>((message.to_string(),));
631
632 if let Ok(exception_obj) = exc_type.call1(py, (message.to_string(),)) {
634 let _ = exception_obj.setattr(py, "operation", &context.operation);
635 let _ = exception_obj.setattr(py, "recoverable", context.recoverable);
636
637 if let Some(file) = &context.file {
638 let _ = exception_obj.setattr(py, "source_file", file);
639 }
640 if let Some(line) = context.line {
641 let _ = exception_obj.setattr(py, "source_line", line);
642 }
643 if let Some(suggestion) = &context.suggestion {
644 let _ = exception_obj.setattr(py, "suggestion", suggestion);
645 }
646 if let Some(code) = context.error_code {
647 let _ = exception_obj.setattr(py, "error_code", code);
648 }
649 }
650
651 exc
652 }
653
654 pub fn register_exceptions(m: &Bound<'_, PyModule>) -> PyResult<()> {
656 m.add("TorshError", m.py().get_type::<TorshError>())?;
657 m.add("TensorError", m.py().get_type::<TensorError>())?;
658 m.add("ShapeError", m.py().get_type::<ShapeError>())?;
659 m.add("DeviceError", m.py().get_type::<DeviceError>())?;
660 m.add("NumericalError", m.py().get_type::<NumericalError>())?;
661 m.add("MemoryError", m.py().get_type::<MemoryError>())?;
662 Ok(())
663 }
664
665 pub fn create_shape_error(expected: &[usize], actual: &[usize], operation: &str) -> PyErr {
667 let message = format!(
668 "Shape mismatch in {}: expected {:?}, got {:?}",
669 operation, expected, actual
670 );
671
672 let suggestion = if expected.len() != actual.len() {
673 Some(format!(
674 "Expected tensor with {} dimensions, but got {} dimensions. Consider using reshape() or unsqueeze()/squeeze() operations.",
675 expected.len(), actual.len()
676 ))
677 } else {
678 let mismatched_dims: Vec<_> = expected
679 .iter()
680 .zip(actual.iter())
681 .enumerate()
682 .filter(|(_, (e, a))| e != a)
683 .collect();
684
685 if mismatched_dims.len() == 1 {
686 let (dim, (exp, act)) = mismatched_dims[0];
687 Some(format!(
688 "Dimension {} mismatch: expected {}, got {}. Consider using reshape(), transpose(), or broadcasting operations.",
689 dim, exp, act
690 ))
691 } else {
692 Some("Multiple dimensions don't match. Verify tensor shapes and consider using broadcasting or reshape operations.".to_string())
693 }
694 };
695
696 let context = ErrorContext::new(operation)
697 .with_suggestion(&suggestion.unwrap_or_default())
698 .recoverable();
699
700 Python::attach(|py| {
701 create_enhanced_exception(py, &py.get_type::<ShapeError>().into(), &message, context)
702 })
703 }
704
705 pub fn create_numerical_error(message: &str, operation: &str) -> PyErr {
707 let suggestion = if message.contains("NaN") {
708 Some("Check for division by zero, invalid operations, or uninitialized values. Consider using torch.isnan() to detect NaN values.".to_string())
709 } else if message.contains("inf") || message.contains("infinity") {
710 Some("Values became infinite. Consider gradient clipping, smaller learning rates, or numerical stability improvements.".to_string())
711 } else if message.contains("overflow") {
712 Some("Numerical overflow detected. Try using smaller values, different data types, or scaling your data.".to_string())
713 } else {
714 Some(
715 "Numerical computation failed. Check input values and operation parameters."
716 .to_string(),
717 )
718 };
719
720 let context = ErrorContext::new(operation)
721 .with_suggestion(&suggestion.unwrap_or_default())
722 .recoverable();
723
724 Python::attach(|py| {
725 create_enhanced_exception(
726 py,
727 &py.get_type::<NumericalError>().into(),
728 message,
729 context,
730 )
731 })
732 }
733}
734
735#[cfg(test)]
736mod tests {
737 use super::*;
738
739 #[test]
740 fn test_error_code_category() {
741 assert_eq!(ErrorCode::ShapeMismatch.category(), ErrorCategory::Tensor);
742 assert_eq!(
743 ErrorCode::AllocationFailed.category(),
744 ErrorCategory::Memory
745 );
746 assert_eq!(
747 ErrorCode::InvalidConversion.category(),
748 ErrorCategory::TypeConversion
749 );
750 assert_eq!(
751 ErrorCode::InvalidParameter.category(),
752 ErrorCategory::Validation
753 );
754 }
755
756 #[test]
757 fn test_error_code_severity() {
758 assert_eq!(
759 ErrorCode::OutOfMemory.default_severity(),
760 Severity::Critical
761 );
762 assert_eq!(ErrorCode::ShapeMismatch.default_severity(), Severity::Error);
763 assert_eq!(
764 ErrorCode::PrecisionLoss.default_severity(),
765 Severity::Warning
766 );
767 }
768
769 #[test]
770 fn test_severity_ordering() {
771 assert!(Severity::Critical > Severity::Error);
772 assert!(Severity::Error > Severity::Warning);
773 assert!(Severity::Warning > Severity::Info);
774 }
775
776 #[test]
777 fn test_enhanced_error_creation() {
778 let error = EnhancedError::new(ErrorCode::ShapeMismatch, "Test error");
779 assert_eq!(error.code, ErrorCode::ShapeMismatch);
780 assert_eq!(error.message, "Test error");
781 assert_eq!(error.severity, Severity::Error);
782 assert_eq!(error.category, ErrorCategory::Tensor);
783 assert!(error.is_recoverable());
784 }
785
786 #[test]
787 fn test_error_builder() {
788 let error = ErrorBuilder::new(ErrorCode::ShapeMismatch)
789 .message("Incompatible shapes")
790 .context("operation", "matmul")
791 .context("expected", "[2, 3]")
792 .context("actual", "[3, 2]")
793 .suggestion("Transpose one of the tensors")
794 .severity(Severity::Error)
795 .build();
796
797 assert_eq!(error.message, "Incompatible shapes");
798 assert_eq!(error.context.get("operation"), Some(&"matmul".to_string()));
799 assert_eq!(error.suggestions.len(), 1);
800 assert!(error.is_recoverable());
801 }
802
803 #[test]
804 fn test_error_with_location() {
805 let error = ErrorBuilder::new(ErrorCode::InvalidParameter)
806 .message("Invalid value")
807 .source_location("test.rs", 42, 10)
808 .build();
809
810 assert!(error.location.is_some());
811 let loc = error.location.unwrap();
812 assert_eq!(loc.file, "test.rs");
813 assert_eq!(loc.line, 42);
814 assert_eq!(loc.column, 10);
815 }
816
817 #[test]
818 fn test_error_with_causes() {
819 let error = ErrorBuilder::new(ErrorCode::OperationFailed)
820 .message("Operation failed")
821 .cause("Underlying IO error")
822 .cause("File not found")
823 .build();
824
825 assert_eq!(error.causes.len(), 2);
826 assert!(error.causes.contains(&"Underlying IO error".to_string()));
827 }
828
829 #[test]
830 fn test_error_display() {
831 let error = ErrorBuilder::new(ErrorCode::ShapeMismatch)
832 .message("Test error")
833 .context("op", "test")
834 .suggestion("Fix it")
835 .build();
836
837 let display = error.to_string();
838 assert!(display.contains("ShapeMismatch"));
839 assert!(display.contains("Test error"));
840 assert!(display.contains("op: test"));
841 assert!(display.contains("Fix it"));
842 }
843
844 #[test]
845 fn test_error_json_serialization() {
846 let error = EnhancedError::new(ErrorCode::InvalidConversion, "Test");
847 let json = error.to_json();
848 assert!(json.is_ok());
849 let json_str = json.unwrap();
850 assert!(json_str.contains("InvalidConversion"));
851 assert!(json_str.contains("Test"));
852 }
853
854 #[test]
855 fn test_error_recoverability() {
856 let critical = EnhancedError::new(ErrorCode::OutOfMemory, "OOM");
857 assert!(!critical.is_recoverable());
858
859 let warning = EnhancedError::new(ErrorCode::PrecisionLoss, "Precision");
860 assert!(warning.is_recoverable());
861 }
862
863 #[test]
864 fn test_ffi_error_enhanced_variant() {
865 let enhanced = EnhancedError::new(ErrorCode::TensorCreationFailed, "Failed");
866 let ffi_error = FfiError::Enhanced(enhanced);
867
868 match ffi_error {
869 FfiError::Enhanced(e) => {
870 assert_eq!(e.code, ErrorCode::TensorCreationFailed);
871 }
872 _ => panic!("Expected Enhanced variant"),
873 }
874 }
875
876 #[test]
877 fn test_error_builder_fluent_api() {
878 let error = ErrorBuilder::new(ErrorCode::MatrixNotInvertible)
879 .message("Matrix is singular")
880 .context("determinant", "0.0")
881 .context("condition_number", "inf")
882 .suggestion("Check matrix values")
883 .suggestion("Try adding regularization")
884 .source_location(file!(), line!(), column!())
885 .build();
886
887 assert_eq!(error.suggestions.len(), 2);
888 assert_eq!(error.context.len(), 2);
889 assert!(error.location.is_some());
890 }
891}