1use crate::types::TagId;
4
5#[derive(Debug, thiserror::Error)]
20pub enum DriverError {
21 #[error("failed to connect to driver")]
24 Connect(#[source] Box<dyn std::error::Error + Send + Sync>),
25
26 #[error("driver operation failed")]
31 Operation(#[source] Box<dyn std::error::Error + Send + Sync>),
32
33 #[error("tag '{tag}': {message}")]
37 InvalidTagValue { tag: TagId, message: String },
38
39 #[error("'{operation}' is not supported by this driver")]
43 Unsupported { operation: &'static str },
44
45 #[error("indexed-search operation rejected: {message}")]
48 IndexOperationRejected { message: String },
49
50 #[error(
52 "gateway does not support {operation}; upgrade the OPC DA bridge gateway to a compatible version"
53 )]
54 IncompatibleGateway { operation: &'static str },
55
56 #[error("browse session or cursor is no longer valid; reopen the tag browser")]
58 BrowseStateInvalid,
59}
60
61pub type DriverResult<T> = std::result::Result<T, DriverError>;
64
65#[derive(Debug)]
67struct PoisonedLock {
68 detail: String,
69}
70
71impl std::fmt::Display for PoisonedLock {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 let detail = &self.detail;
74 write!(f, "mutex poisoned: {detail}")
75 }
76}
77
78impl std::error::Error for PoisonedLock {
79 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
80 None
81 }
82}
83
84pub(crate) fn poisoned_lock<T>(lock: std::sync::LockResult<T>) -> DriverResult<T> {
90 lock.map_err(|poisoned| {
91 DriverError::Operation(Box::new(PoisonedLock {
92 detail: poisoned.to_string(),
93 }))
94 })
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use std::io;
101
102 #[test]
103 fn connect_error_displays_without_source_text_but_keeps_source_chain() {
104 let source = io::Error::other("refused");
105 let err = DriverError::Connect(Box::new(source));
106 assert_eq!(err.to_string(), "failed to connect to driver");
107 assert!(std::error::Error::source(&err).is_some());
108 }
109
110 #[test]
111 fn operation_error_displays_without_source_text_but_keeps_source_chain() {
112 let source = io::Error::other("timed out");
113 let err = DriverError::Operation(Box::new(source));
114 assert_eq!(err.to_string(), "driver operation failed");
115 assert!(std::error::Error::source(&err).is_some());
116 }
117
118 #[test]
119 fn invalid_tag_value_names_the_tag_and_reason() {
120 let err = DriverError::InvalidTagValue {
121 tag: "Area1.LIC101.MODE".to_string(),
122 message: "unrecognized mode code 'FOO'".to_string(),
123 };
124 assert_eq!(
125 err.to_string(),
126 "tag 'Area1.LIC101.MODE': unrecognized mode code 'FOO'"
127 );
128 }
129
130 #[test]
131 fn unsupported_names_the_operation() {
132 let err = DriverError::Unsupported {
133 operation: "browse",
134 };
135 assert_eq!(err.to_string(), "'browse' is not supported by this driver");
136 }
137
138 #[test]
139 fn incompatible_gateway_names_the_upgrade_action() {
140 let err = DriverError::IncompatibleGateway {
141 operation: "paged browse",
142 };
143 assert!(
144 err.to_string()
145 .contains("upgrade the OPC DA bridge gateway")
146 );
147 }
148
149 #[test]
150 fn indexed_search_rejection_names_gateway_reason() {
151 let err = DriverError::IndexOperationRejected {
152 message: "server is not enrolled for namespace indexing".to_string(),
153 };
154 assert_eq!(
155 err.to_string(),
156 "indexed-search operation rejected: server is not enrolled for namespace indexing"
157 );
158 }
159
160 #[test]
161 fn invalid_browse_state_is_actionable() {
162 assert!(
163 DriverError::BrowseStateInvalid
164 .to_string()
165 .contains("reopen the tag browser")
166 );
167 }
168
169 #[test]
170 fn poisoned_lock_returns_the_guard_or_an_operation_error() {
171 let live = std::sync::Mutex::new(1);
172 let guard = poisoned_lock(live.lock()).unwrap();
173 assert_eq!(*guard, 1);
174 drop(guard);
175
176 let poisoned = std::sync::Mutex::new(0);
177 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
178 let _guard = poisoned.lock().unwrap();
179 panic!("poison the mutex");
180 }));
181 let err = poisoned_lock(poisoned.lock()).unwrap_err();
182 let source = std::error::Error::source(&err).expect("operation error has a source");
183 assert!(source.to_string().contains("mutex poisoned"));
184 assert!(std::error::Error::source(source).is_none());
185 }
186}