Skip to main content

opendeviationbar_core/
errors.rs

1//! Processing error types
2//!
3//! Extracted from processor.rs (Phase 2a refactoring)
4
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7use thiserror::Error;
8
9/// Processing errors
10#[derive(Error, Debug)]
11pub enum ProcessingError {
12    #[error(
13        "Trades not sorted at index {index}: prev=({prev_time}, {prev_id}), curr=({curr_time}, {curr_id})"
14    )]
15    UnsortedTrades {
16        index: usize,
17        prev_time: i64,
18        prev_id: i64,
19        curr_time: i64,
20        curr_id: i64,
21    },
22
23    #[error("Empty trade data")]
24    EmptyData,
25
26    #[error(
27        "Invalid threshold: {threshold_decimal_bps} dbps. Valid range: 1-100,000 dbps (0.001%-100%)"
28    )]
29    InvalidThreshold { threshold_decimal_bps: u32 },
30}
31
32#[cfg(feature = "python")]
33impl From<ProcessingError> for PyErr {
34    fn from(err: ProcessingError) -> PyErr {
35        match err {
36            ProcessingError::UnsortedTrades {
37                index,
38                prev_time,
39                prev_id,
40                curr_time,
41                curr_id,
42            } => pyo3::exceptions::PyValueError::new_err(format!(
43                "Trades not sorted at index {}: prev=({}, {}), curr=({}, {})",
44                index, prev_time, prev_id, curr_time, curr_id
45            )),
46            ProcessingError::EmptyData => {
47                pyo3::exceptions::PyValueError::new_err("Empty trade data")
48            }
49            ProcessingError::InvalidThreshold {
50                threshold_decimal_bps,
51            } => pyo3::exceptions::PyValueError::new_err(format!(
52                "Invalid threshold: {} dbps. Valid range: 1-100,000 dbps (0.001%-100%)",
53                threshold_decimal_bps
54            )),
55        }
56    }
57}
58
59// Issue #96 Task #87: Test coverage for error Display formatting
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn test_unsorted_trades_display() {
66        let err = ProcessingError::UnsortedTrades {
67            index: 42,
68            prev_time: 1000,
69            prev_id: 100,
70            curr_time: 999,
71            curr_id: 101,
72        };
73        let msg = err.to_string();
74        assert!(msg.contains("index 42"));
75        assert!(msg.contains("prev=(1000, 100)"));
76        assert!(msg.contains("curr=(999, 101)"));
77    }
78
79    #[test]
80    fn test_empty_data_display() {
81        let err = ProcessingError::EmptyData;
82        assert_eq!(err.to_string(), "Empty trade data");
83    }
84
85    #[test]
86    fn test_invalid_threshold_display() {
87        let err = ProcessingError::InvalidThreshold {
88            threshold_decimal_bps: 0,
89        };
90        let msg = err.to_string();
91        assert!(msg.contains("0 dbps"));
92        assert!(msg.contains("Valid range"));
93    }
94
95    #[test]
96    fn test_invalid_threshold_large_value() {
97        let err = ProcessingError::InvalidThreshold {
98            threshold_decimal_bps: 999_999,
99        };
100        assert!(err.to_string().contains("999999 dbps"));
101    }
102
103    #[test]
104    fn test_error_is_send_sync() {
105        fn assert_send<T: Send>() {}
106        fn assert_sync<T: Sync>() {}
107        // ProcessingError must be Send+Sync for cross-thread use
108        assert_send::<ProcessingError>();
109        assert_sync::<ProcessingError>();
110    }
111
112    #[test]
113    fn test_error_debug_impl() {
114        let err = ProcessingError::EmptyData;
115        let debug = format!("{:?}", err);
116        assert!(debug.contains("EmptyData"));
117    }
118
119    #[test]
120    fn test_unsorted_trades_boundary_values() {
121        let err = ProcessingError::UnsortedTrades {
122            index: usize::MAX,
123            prev_time: i64::MIN,
124            prev_id: i64::MAX,
125            curr_time: 0,
126            curr_id: 0,
127        };
128        // Should not panic on extreme values
129        let msg = err.to_string();
130        assert!(!msg.is_empty());
131    }
132}