Skip to main content

nros_core/
error.rs

1//! Unified error types for nros
2//!
3//! This module provides comprehensive error types that align with rclrs patterns
4//! while maintaining `no_std` compatibility for embedded systems.
5//!
6//! # Error Types
7//!
8//! - [`NanoRosError`] - Main unified error type
9//! - [`RclReturnCode`] - RCL-compatible error codes
10//! - [`ErrorContext`] - Context information (topic, service, node names)
11//! - [`NanoRosErrorFilter`] - Trait for filtering expected errors
12//! - [`TakeFailedAsNone`] - Trait for converting take failures to `Option`
13//!
14//! # Example
15//!
16//! ```
17//! use nros_core::{NanoRosError, RclReturnCode};
18//!
19//! fn publish_message() -> Result<(), NanoRosError> {
20//!     Err(NanoRosError::timeout())
21//! }
22//!
23//! let result = publish_message();
24//! assert!(result.unwrap_err().is_timeout());
25//! ```
26
27use core::fmt;
28use nros_serdes::{DeserError, SerError};
29
30/// RCL-compatible return codes
31///
32/// These codes match the RCL C library return codes for interoperability.
33/// Most codes are organized by category (1xx for init, 2xx for node, etc.).
34#[repr(i32)]
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum RclReturnCode {
37    /// Success
38    Ok = 0,
39    /// Unspecified error
40    Error = 1,
41    /// Timeout occurred
42    Timeout = 2,
43    /// Unsupported operation
44    Unsupported = 3,
45    /// Failed to allocate memory
46    BadAlloc = 10,
47    /// Argument to function was invalid
48    InvalidArgument = 11,
49
50    // 1xx: Initialization errors
51    /// Already initialized
52    AlreadyInit = 100,
53    /// Not yet initialized
54    NotInit = 101,
55    /// Topic name does not pass validation
56    TopicNameInvalid = 103,
57    /// Service name does not pass validation
58    ServiceNameInvalid = 104,
59    /// Already shutdown
60    AlreadyShutdown = 106,
61
62    // 2xx: Node errors
63    /// Invalid node
64    NodeInvalid = 200,
65    /// Invalid node name
66    NodeInvalidName = 201,
67    /// Invalid node namespace
68    NodeInvalidNamespace = 202,
69
70    // 3xx: Publisher errors
71    /// Invalid publisher
72    PublisherInvalid = 300,
73
74    // 4xx: Subscription errors
75    /// Invalid subscription
76    SubscriptionInvalid = 400,
77    /// Failed to take a message from the subscription
78    SubscriptionTakeFailed = 401,
79
80    // 5xx: Client errors
81    /// Invalid client
82    ClientInvalid = 500,
83    /// Failed to take a response from the client
84    ClientTakeFailed = 501,
85
86    // 6xx: Service errors
87    /// Invalid service
88    ServiceInvalid = 600,
89    /// Failed to take a request from the service
90    ServiceTakeFailed = 601,
91
92    // 8xx: Timer errors
93    /// Invalid timer
94    TimerInvalid = 800,
95    /// Timer was canceled
96    TimerCanceled = 801,
97
98    // 21xx: Action errors
99    /// Action goal accepted
100    ActionGoalAccepted = 2100,
101    /// Action goal rejected
102    ActionGoalRejected = 2101,
103    /// Action client is invalid
104    ActionClientInvalid = 2102,
105    /// Action client failed to take response
106    ActionClientTakeFailed = 2103,
107    /// Action server is invalid
108    ActionServerInvalid = 2200,
109    /// Action server failed to take request
110    ActionServerTakeFailed = 2201,
111    /// Action goal handle invalid
112    ActionGoalHandleInvalid = 2300,
113}
114
115impl RclReturnCode {
116    /// Returns the numeric value of this return code
117    pub const fn as_i32(self) -> i32 {
118        self as i32
119    }
120
121    /// Try to convert from an i32 value
122    pub fn try_from_i32(value: i32) -> Option<Self> {
123        match value {
124            0 => Some(Self::Ok),
125            1 => Some(Self::Error),
126            2 => Some(Self::Timeout),
127            3 => Some(Self::Unsupported),
128            10 => Some(Self::BadAlloc),
129            11 => Some(Self::InvalidArgument),
130            100 => Some(Self::AlreadyInit),
131            101 => Some(Self::NotInit),
132            103 => Some(Self::TopicNameInvalid),
133            104 => Some(Self::ServiceNameInvalid),
134            106 => Some(Self::AlreadyShutdown),
135            200 => Some(Self::NodeInvalid),
136            201 => Some(Self::NodeInvalidName),
137            202 => Some(Self::NodeInvalidNamespace),
138            300 => Some(Self::PublisherInvalid),
139            400 => Some(Self::SubscriptionInvalid),
140            401 => Some(Self::SubscriptionTakeFailed),
141            500 => Some(Self::ClientInvalid),
142            501 => Some(Self::ClientTakeFailed),
143            600 => Some(Self::ServiceInvalid),
144            601 => Some(Self::ServiceTakeFailed),
145            800 => Some(Self::TimerInvalid),
146            801 => Some(Self::TimerCanceled),
147            2100 => Some(Self::ActionGoalAccepted),
148            2101 => Some(Self::ActionGoalRejected),
149            2102 => Some(Self::ActionClientInvalid),
150            2103 => Some(Self::ActionClientTakeFailed),
151            2200 => Some(Self::ActionServerInvalid),
152            2201 => Some(Self::ActionServerTakeFailed),
153            2300 => Some(Self::ActionGoalHandleInvalid),
154            _ => None,
155        }
156    }
157}
158
159impl fmt::Display for RclReturnCode {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        let msg = match self {
162            Self::Ok => "Operation successful (RCL_RET_OK)",
163            Self::Error => "Unspecified error (RCL_RET_ERROR)",
164            Self::Timeout => "Timeout occurred (RCL_RET_TIMEOUT)",
165            Self::Unsupported => "Unsupported operation (RCL_RET_UNSUPPORTED)",
166            Self::BadAlloc => "Failed to allocate memory (RCL_RET_BAD_ALLOC)",
167            Self::InvalidArgument => "Invalid argument (RCL_RET_INVALID_ARGUMENT)",
168            Self::AlreadyInit => "Already initialized (RCL_RET_ALREADY_INIT)",
169            Self::NotInit => "Not initialized (RCL_RET_NOT_INIT)",
170            Self::TopicNameInvalid => "Invalid topic name (RCL_RET_TOPIC_NAME_INVALID)",
171            Self::ServiceNameInvalid => "Invalid service name (RCL_RET_SERVICE_NAME_INVALID)",
172            Self::AlreadyShutdown => "Already shutdown (RCL_RET_ALREADY_SHUTDOWN)",
173            Self::NodeInvalid => "Invalid node (RCL_RET_NODE_INVALID)",
174            Self::NodeInvalidName => "Invalid node name (RCL_RET_NODE_INVALID_NAME)",
175            Self::NodeInvalidNamespace => "Invalid node namespace (RCL_RET_NODE_INVALID_NAMESPACE)",
176            Self::PublisherInvalid => "Invalid publisher (RCL_RET_PUBLISHER_INVALID)",
177            Self::SubscriptionInvalid => "Invalid subscription (RCL_RET_SUBSCRIPTION_INVALID)",
178            Self::SubscriptionTakeFailed => {
179                "Failed to take message (RCL_RET_SUBSCRIPTION_TAKE_FAILED)"
180            }
181            Self::ClientInvalid => "Invalid client (RCL_RET_CLIENT_INVALID)",
182            Self::ClientTakeFailed => "Failed to take response (RCL_RET_CLIENT_TAKE_FAILED)",
183            Self::ServiceInvalid => "Invalid service (RCL_RET_SERVICE_INVALID)",
184            Self::ServiceTakeFailed => "Failed to take request (RCL_RET_SERVICE_TAKE_FAILED)",
185            Self::TimerInvalid => "Invalid timer (RCL_RET_TIMER_INVALID)",
186            Self::TimerCanceled => "Timer was canceled (RCL_RET_TIMER_CANCELED)",
187            Self::ActionGoalAccepted => "Action goal accepted (RCL_RET_ACTION_GOAL_ACCEPTED)",
188            Self::ActionGoalRejected => "Action goal rejected (RCL_RET_ACTION_GOAL_REJECTED)",
189            Self::ActionClientInvalid => "Invalid action client (RCL_RET_ACTION_CLIENT_INVALID)",
190            Self::ActionClientTakeFailed => {
191                "Action client take failed (RCL_RET_ACTION_CLIENT_TAKE_FAILED)"
192            }
193            Self::ActionServerInvalid => "Invalid action server (RCL_RET_ACTION_SERVER_INVALID)",
194            Self::ActionServerTakeFailed => {
195                "Action server take failed (RCL_RET_ACTION_SERVER_TAKE_FAILED)"
196            }
197            Self::ActionGoalHandleInvalid => {
198                "Invalid action goal handle (RCL_RET_ACTION_GOAL_HANDLE_INVALID)"
199            }
200        };
201        write!(f, "{}", msg)
202    }
203}
204
205/// Main error type for nros operations
206///
207/// This error type provides comprehensive coverage of all failure modes in nros,
208/// with optional context information (topic name, service name, etc.) when available.
209///
210/// # Error Categories
211///
212/// - **Serialization**: CDR encoding/decoding failures
213/// - **Transport**: Network and communication failures
214/// - **Node/Context**: Node creation and management failures
215/// - **Publisher/Subscriber**: Pub/sub failures
216/// - **Service/Client**: Service call failures
217/// - **Action**: Action server/client failures
218/// - **Timer**: Timer management failures
219/// - **Parameter**: Parameter declaration/access failures
220///
221/// # Example
222///
223/// ```
224/// use nros_core::NanoRosError;
225///
226/// let err = NanoRosError::timeout();
227/// assert!(err.is_timeout());
228/// assert!(!err.is_take_failed());
229///
230/// // Errors with context
231/// let err = NanoRosError::topic_name_invalid("/invalid topic!");
232/// assert!(err.context().is_some());
233/// ```
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct NanoRosError {
236    /// The error code
237    code: RclReturnCode,
238    /// Optional context (topic name, service name, node name, etc.)
239    context: Option<ErrorContext>,
240    /// Nested error for serialization/deserialization failures
241    nested: Option<NestedError>,
242}
243
244/// Context information for errors
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub enum ErrorContext {
247    /// Topic name that caused the error
248    Topic(&'static str),
249    /// Service name that caused the error
250    Service(&'static str),
251    /// Node name that caused the error
252    Node(&'static str),
253    /// Action name that caused the error
254    Action(&'static str),
255    /// Timer ID that caused the error
256    Timer(usize),
257    /// Parameter name that caused the error
258    Parameter(&'static str),
259    /// Custom context message
260    Custom(&'static str),
261}
262
263impl fmt::Display for ErrorContext {
264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265        match self {
266            Self::Topic(name) => write!(f, "topic '{}'", name),
267            Self::Service(name) => write!(f, "service '{}'", name),
268            Self::Node(name) => write!(f, "node '{}'", name),
269            Self::Action(name) => write!(f, "action '{}'", name),
270            Self::Timer(id) => write!(f, "timer {}", id),
271            Self::Parameter(name) => write!(f, "parameter '{}'", name),
272            Self::Custom(msg) => write!(f, "{}", msg),
273        }
274    }
275}
276
277/// Nested error for wrapping serialization/deserialization errors
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub enum NestedError {
280    /// Serialization error
281    Ser(SerError),
282    /// Deserialization error
283    Deser(DeserError),
284}
285
286impl NanoRosError {
287    // === Constructors ===
288
289    /// Create a new error with the given code
290    pub const fn new(code: RclReturnCode) -> Self {
291        Self {
292            code,
293            context: None,
294            nested: None,
295        }
296    }
297
298    /// Create a new error with context
299    pub const fn with_context(code: RclReturnCode, context: ErrorContext) -> Self {
300        Self {
301            code,
302            context: Some(context),
303            nested: None,
304        }
305    }
306
307    /// Create a timeout error
308    pub const fn timeout() -> Self {
309        Self::new(RclReturnCode::Timeout)
310    }
311
312    /// Create an invalid argument error
313    pub const fn invalid_argument() -> Self {
314        Self::new(RclReturnCode::InvalidArgument)
315    }
316
317    /// Create an unsupported operation error
318    pub const fn unsupported() -> Self {
319        Self::new(RclReturnCode::Unsupported)
320    }
321
322    /// Create an allocation failure error
323    pub const fn bad_alloc() -> Self {
324        Self::new(RclReturnCode::BadAlloc)
325    }
326
327    /// Create a generic error
328    pub const fn error() -> Self {
329        Self::new(RclReturnCode::Error)
330    }
331
332    // === Node errors ===
333
334    /// Create an invalid node error
335    pub const fn node_invalid() -> Self {
336        Self::new(RclReturnCode::NodeInvalid)
337    }
338
339    /// Create an invalid node name error with context
340    pub const fn node_invalid_name(name: &'static str) -> Self {
341        Self::with_context(RclReturnCode::NodeInvalidName, ErrorContext::Node(name))
342    }
343
344    /// Create an invalid node namespace error with context
345    pub const fn node_invalid_namespace(namespace: &'static str) -> Self {
346        Self::with_context(
347            RclReturnCode::NodeInvalidNamespace,
348            ErrorContext::Custom(namespace),
349        )
350    }
351
352    // === Topic errors ===
353
354    /// Create an invalid topic name error with context
355    pub const fn topic_name_invalid(topic: &'static str) -> Self {
356        Self::with_context(RclReturnCode::TopicNameInvalid, ErrorContext::Topic(topic))
357    }
358
359    /// Create a publisher invalid error
360    pub const fn publisher_invalid() -> Self {
361        Self::new(RclReturnCode::PublisherInvalid)
362    }
363
364    /// Create a subscription invalid error
365    pub const fn subscription_invalid() -> Self {
366        Self::new(RclReturnCode::SubscriptionInvalid)
367    }
368
369    /// Create a subscription take failed error
370    pub const fn subscription_take_failed() -> Self {
371        Self::new(RclReturnCode::SubscriptionTakeFailed)
372    }
373
374    // === Service errors ===
375
376    /// Create an invalid service name error with context
377    pub const fn service_name_invalid(service: &'static str) -> Self {
378        Self::with_context(
379            RclReturnCode::ServiceNameInvalid,
380            ErrorContext::Service(service),
381        )
382    }
383
384    /// Create a service invalid error
385    pub const fn service_invalid() -> Self {
386        Self::new(RclReturnCode::ServiceInvalid)
387    }
388
389    /// Create a service take failed error
390    pub const fn service_take_failed() -> Self {
391        Self::new(RclReturnCode::ServiceTakeFailed)
392    }
393
394    /// Create a client invalid error
395    pub const fn client_invalid() -> Self {
396        Self::new(RclReturnCode::ClientInvalid)
397    }
398
399    /// Create a client take failed error
400    pub const fn client_take_failed() -> Self {
401        Self::new(RclReturnCode::ClientTakeFailed)
402    }
403
404    // === Timer errors ===
405
406    /// Create a timer invalid error
407    pub const fn timer_invalid() -> Self {
408        Self::new(RclReturnCode::TimerInvalid)
409    }
410
411    /// Create a timer canceled error
412    pub const fn timer_canceled() -> Self {
413        Self::new(RclReturnCode::TimerCanceled)
414    }
415
416    // === Action errors ===
417
418    /// Create an action goal rejected error
419    pub const fn action_goal_rejected() -> Self {
420        Self::new(RclReturnCode::ActionGoalRejected)
421    }
422
423    /// Create an action client invalid error
424    pub const fn action_client_invalid() -> Self {
425        Self::new(RclReturnCode::ActionClientInvalid)
426    }
427
428    /// Create an action server invalid error
429    pub const fn action_server_invalid() -> Self {
430        Self::new(RclReturnCode::ActionServerInvalid)
431    }
432
433    /// Create an action goal handle invalid error
434    pub const fn action_goal_handle_invalid() -> Self {
435        Self::new(RclReturnCode::ActionGoalHandleInvalid)
436    }
437
438    // === Init/Shutdown errors ===
439
440    /// Create an already initialized error
441    pub const fn already_init() -> Self {
442        Self::new(RclReturnCode::AlreadyInit)
443    }
444
445    /// Create a not initialized error
446    pub const fn not_init() -> Self {
447        Self::new(RclReturnCode::NotInit)
448    }
449
450    /// Create an already shutdown error
451    pub const fn already_shutdown() -> Self {
452        Self::new(RclReturnCode::AlreadyShutdown)
453    }
454
455    // === Serialization errors ===
456
457    /// Create a serialization error
458    pub fn serialization(err: SerError) -> Self {
459        Self {
460            code: RclReturnCode::Error,
461            context: None,
462            nested: Some(NestedError::Ser(err)),
463        }
464    }
465
466    /// Create a deserialization error
467    pub fn deserialization(err: DeserError) -> Self {
468        Self {
469            code: RclReturnCode::Error,
470            context: None,
471            nested: Some(NestedError::Deser(err)),
472        }
473    }
474
475    // === Query methods ===
476
477    /// Returns the error code
478    pub const fn code(&self) -> RclReturnCode {
479        self.code
480    }
481
482    /// Returns the error context, if any
483    pub const fn context(&self) -> Option<&ErrorContext> {
484        self.context.as_ref()
485    }
486
487    /// Returns the nested error, if any
488    pub const fn nested(&self) -> Option<&NestedError> {
489        self.nested.as_ref()
490    }
491
492    /// Returns true if this error was due to a timeout
493    pub const fn is_timeout(&self) -> bool {
494        matches!(self.code, RclReturnCode::Timeout)
495    }
496
497    /// Returns true if this error was because a take operation failed
498    /// (subscription, service, client, or action take failed)
499    pub const fn is_take_failed(&self) -> bool {
500        matches!(
501            self.code,
502            RclReturnCode::SubscriptionTakeFailed
503                | RclReturnCode::ServiceTakeFailed
504                | RclReturnCode::ClientTakeFailed
505                | RclReturnCode::ActionServerTakeFailed
506                | RclReturnCode::ActionClientTakeFailed
507        )
508    }
509
510    /// Returns true if this is an action-related error
511    pub const fn is_action_error(&self) -> bool {
512        matches!(
513            self.code,
514            RclReturnCode::ActionGoalAccepted
515                | RclReturnCode::ActionGoalRejected
516                | RclReturnCode::ActionClientInvalid
517                | RclReturnCode::ActionClientTakeFailed
518                | RclReturnCode::ActionServerInvalid
519                | RclReturnCode::ActionServerTakeFailed
520                | RclReturnCode::ActionGoalHandleInvalid
521        )
522    }
523
524    /// Returns true if this is a serialization or deserialization error
525    pub const fn is_serialization_error(&self) -> bool {
526        self.nested.is_some()
527    }
528}
529
530impl fmt::Display for NanoRosError {
531    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
532        // Write the main error message
533        write!(f, "{}", self.code)?;
534
535        // Add context if available
536        if let Some(ctx) = &self.context {
537            write!(f, " ({})", ctx)?;
538        }
539
540        // Add nested error if available
541        if let Some(nested) = &self.nested {
542            match nested {
543                NestedError::Ser(e) => write!(f, ": {}", e)?,
544                NestedError::Deser(e) => write!(f, ": {}", e)?,
545            }
546        }
547
548        Ok(())
549    }
550}
551
552// Implement std::error::Error when std is available
553#[cfg(feature = "std")]
554impl std::error::Error for NanoRosError {
555    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
556        // Nested errors don't implement std::error::Error in no_std
557        // so we can't return them as source. This is a limitation of no_std.
558        None
559    }
560}
561
562#[cfg(feature = "std")]
563impl std::error::Error for RclReturnCode {}
564
565// === Conversions ===
566
567impl From<SerError> for NanoRosError {
568    fn from(e: SerError) -> Self {
569        Self::serialization(e)
570    }
571}
572
573impl From<DeserError> for NanoRosError {
574    fn from(e: DeserError) -> Self {
575        Self::deserialization(e)
576    }
577}
578
579impl From<RclReturnCode> for NanoRosError {
580    fn from(code: RclReturnCode) -> Self {
581        Self::new(code)
582    }
583}
584
585// === Error filtering (matching rclrs patterns) ===
586
587/// A helper trait to handle common error filtering patterns
588///
589/// This trait provides methods similar to rclrs for filtering errors
590/// that are expected in normal operation (timeouts, take failures).
591pub trait NanoRosErrorFilter {
592    /// The output type after filtering
593    type Output;
594
595    /// If the result was a timeout error, change it to `Ok(())`
596    fn timeout_ok(self) -> Self::Output;
597
598    /// If a take operation failed, change the result to `Ok(())`
599    fn take_failed_ok(self) -> Self::Output;
600
601    /// Filter out both timeouts and take failures
602    fn ignore_non_errors(self) -> Self::Output
603    where
604        Self: Sized,
605        Self::Output: From<Self>,
606    {
607        // Default implementation chains the two filters
608        self.timeout_ok()
609    }
610}
611
612impl NanoRosErrorFilter for Result<(), NanoRosError> {
613    type Output = Result<(), NanoRosError>;
614
615    fn timeout_ok(self) -> Self::Output {
616        match self {
617            Ok(()) => Ok(()),
618            Err(e) if e.is_timeout() => Ok(()),
619            Err(e) => Err(e),
620        }
621    }
622
623    fn take_failed_ok(self) -> Self::Output {
624        match self {
625            Ok(()) => Ok(()),
626            Err(e) if e.is_take_failed() => Ok(()),
627            Err(e) => Err(e),
628        }
629    }
630
631    fn ignore_non_errors(self) -> Self::Output {
632        self.timeout_ok().take_failed_ok()
633    }
634}
635
636/// A helper trait to convert take failures to None
637///
638/// This is useful when you want to distinguish between "no data available"
639/// (returns None) and actual errors (returns Err).
640pub trait TakeFailedAsNone {
641    /// The value type
642    type T;
643
644    /// If the take failed, return `Ok(None)`. Otherwise return `Ok(Some(value))`.
645    fn take_failed_as_none(self) -> Result<Option<Self::T>, NanoRosError>;
646}
647
648impl<T> TakeFailedAsNone for Result<T, NanoRosError> {
649    type T = T;
650
651    fn take_failed_as_none(self) -> Result<Option<T>, NanoRosError> {
652        match self {
653            Ok(value) => Ok(Some(value)),
654            Err(e) if e.is_take_failed() => Ok(None),
655            Err(e) => Err(e),
656        }
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    extern crate alloc;
663    use alloc::format;
664
665    use super::*;
666
667    #[test]
668    fn test_rcl_return_code_display() {
669        assert!(format!("{}", RclReturnCode::Ok).contains("RCL_RET_OK"));
670        assert!(format!("{}", RclReturnCode::Timeout).contains("RCL_RET_TIMEOUT"));
671        assert!(
672            format!("{}", RclReturnCode::NodeInvalidName).contains("RCL_RET_NODE_INVALID_NAME")
673        );
674    }
675
676    #[test]
677    fn test_rcl_return_code_try_from() {
678        assert_eq!(RclReturnCode::try_from_i32(0), Some(RclReturnCode::Ok));
679        assert_eq!(RclReturnCode::try_from_i32(2), Some(RclReturnCode::Timeout));
680        assert_eq!(
681            RclReturnCode::try_from_i32(201),
682            Some(RclReturnCode::NodeInvalidName)
683        );
684        assert_eq!(RclReturnCode::try_from_i32(9999), None);
685    }
686
687    #[test]
688    fn test_nros_error_timeout() {
689        let err = NanoRosError::timeout();
690        assert!(err.is_timeout());
691        assert!(!err.is_take_failed());
692        assert_eq!(err.code(), RclReturnCode::Timeout);
693    }
694
695    #[test]
696    fn test_nros_error_take_failed() {
697        let err = NanoRosError::subscription_take_failed();
698        assert!(err.is_take_failed());
699        assert!(!err.is_timeout());
700
701        let err = NanoRosError::client_take_failed();
702        assert!(err.is_take_failed());
703
704        let err = NanoRosError::service_take_failed();
705        assert!(err.is_take_failed());
706    }
707
708    #[test]
709    fn test_nros_error_with_context() {
710        let err = NanoRosError::topic_name_invalid("/bad topic");
711        assert!(err.context().is_some());
712        if let Some(ErrorContext::Topic(name)) = err.context() {
713            assert_eq!(*name, "/bad topic");
714        } else {
715            panic!("Expected Topic context");
716        }
717    }
718
719    #[test]
720    fn test_nros_error_display() {
721        let err = NanoRosError::timeout();
722        let msg = format!("{}", err);
723        assert!(msg.contains("Timeout"));
724
725        let err = NanoRosError::topic_name_invalid("/test");
726        let msg = format!("{}", err);
727        assert!(msg.contains("topic"));
728        assert!(msg.contains("/test"));
729    }
730
731    #[test]
732    fn test_nros_error_from_ser_error() {
733        let err: NanoRosError = SerError::BufferTooSmall.into();
734        assert!(err.is_serialization_error());
735        assert!(matches!(err.nested(), Some(NestedError::Ser(_))));
736    }
737
738    #[test]
739    fn test_nros_error_from_deser_error() {
740        let err: NanoRosError = DeserError::UnexpectedEof.into();
741        assert!(err.is_serialization_error());
742        assert!(matches!(err.nested(), Some(NestedError::Deser(_))));
743    }
744
745    #[test]
746    fn test_error_filter_timeout_ok() {
747        let result: Result<(), NanoRosError> = Err(NanoRosError::timeout());
748        assert!(result.timeout_ok().is_ok());
749
750        let result: Result<(), NanoRosError> = Err(NanoRosError::error());
751        assert!(result.timeout_ok().is_err());
752
753        let result: Result<(), NanoRosError> = Ok(());
754        assert!(result.timeout_ok().is_ok());
755    }
756
757    #[test]
758    fn test_error_filter_take_failed_ok() {
759        let result: Result<(), NanoRosError> = Err(NanoRosError::subscription_take_failed());
760        assert!(result.take_failed_ok().is_ok());
761
762        let result: Result<(), NanoRosError> = Err(NanoRosError::error());
763        assert!(result.take_failed_ok().is_err());
764    }
765
766    #[test]
767    fn test_take_failed_as_none() {
768        let result: Result<i32, NanoRosError> = Err(NanoRosError::subscription_take_failed());
769        assert_eq!(result.take_failed_as_none().unwrap(), None);
770
771        let result: Result<i32, NanoRosError> = Ok(42);
772        assert_eq!(result.take_failed_as_none().unwrap(), Some(42));
773
774        let result: Result<i32, NanoRosError> = Err(NanoRosError::timeout());
775        assert!(result.take_failed_as_none().is_err());
776    }
777
778    #[test]
779    fn test_action_error_detection() {
780        let err = NanoRosError::action_goal_rejected();
781        assert!(err.is_action_error());
782
783        let err = NanoRosError::action_client_invalid();
784        assert!(err.is_action_error());
785
786        let err = NanoRosError::timeout();
787        assert!(!err.is_action_error());
788    }
789
790    #[test]
791    fn test_error_context_display() {
792        let ctx = ErrorContext::Topic("/my_topic");
793        assert!(format!("{}", ctx).contains("topic"));
794        assert!(format!("{}", ctx).contains("/my_topic"));
795
796        let ctx = ErrorContext::Service("/my_service");
797        assert!(format!("{}", ctx).contains("service"));
798
799        let ctx = ErrorContext::Timer(42);
800        assert!(format!("{}", ctx).contains("42"));
801    }
802}