use std::fmt;
use rs_teststand_sys::ComError;
use crate::error_codes::code_name;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("{}", EngineDisplay { code: *code, dispid: *dispid })]
Engine {
code: i32,
dispid: i32,
},
#[error("{}", ComDisplay { hresult: *hresult, dispid: *dispid })]
Com {
hresult: i32,
dispid: i32,
},
#[error("unexpected value type: expected {expected}, got {actual}")]
UnexpectedType {
expected: &'static str,
actual: &'static str,
},
}
impl Error {
#[must_use]
pub const fn code_name(&self) -> Option<&'static str> {
match self {
Self::Engine { code, .. } => code_name(*code),
Self::Com { hresult, .. } => code_name(*hresult),
Self::UnexpectedType { .. } => None,
}
}
#[must_use]
pub const fn code(&self) -> Option<i32> {
match self {
Self::Engine { code, .. } => Some(*code),
Self::Com { hresult, .. } => Some(*hresult),
Self::UnexpectedType { .. } => None,
}
}
}
struct EngineDisplay {
code: i32,
dispid: i32,
}
impl fmt::Display for EngineDisplay {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match code_name(self.code) {
Some(name) => write!(formatter, "engine error {name} ({})", self.code)?,
None => write!(formatter, "engine error {}", self.code)?,
}
if self.dispid != 0 {
write!(formatter, " from DISPID {:#x}", self.dispid)?;
}
Ok(())
}
}
struct ComDisplay {
hresult: i32,
dispid: i32,
}
impl fmt::Display for ComDisplay {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"COM call failed (HRESULT {:#010x})",
self.hresult
)?;
if self.dispid != 0 {
write!(formatter, " from DISPID {:#x}", self.dispid)?;
}
Ok(())
}
}
impl From<ComError> for Error {
fn from(error: ComError) -> Self {
match error {
ComError::Hresult { code, dispid, .. } => {
if code_name(code).is_some() {
Self::Engine { code, dispid }
} else {
Self::Com {
hresult: code,
dispid,
}
}
}
ComError::UnexpectedType { expected, actual } => {
Self::UnexpectedType { expected, actual }
}
}
}
}
#[cfg(test)]
mod tests {
use super::Error;
use rs_teststand_sys::ComError;
#[test]
fn known_engine_code_becomes_a_named_error() {
let error = Error::from(ComError::hresult(-17000, "test"));
assert!(
matches!(error, Error::Engine { code: -17000, .. }),
"expected Engine variant, got {error:?}"
);
assert_eq!(error.code_name(), Some("TS_Err_OutOfMemory"));
assert!(
error.to_string().contains("TS_Err_OutOfMemory"),
"message should name the error, got {error}"
);
}
#[test]
fn a_member_failure_names_the_dispid_that_refused() {
let error = Error::from(ComError::member(-17308, "IDispatch::Invoke (call)", 0x1f5));
let message = error.to_string();
assert!(
message.contains("TS_Err_UnexpectedType"),
"should name the code, got {message}"
);
assert!(
message.contains("0x1f5"),
"should name the member that refused, got {message}"
);
}
#[test]
fn a_failure_outside_a_member_call_omits_the_dispid() {
let error = Error::from(ComError::hresult(-17000, "CoCreateInstance"));
assert!(!error.to_string().contains("DISPID"), "got {error}");
}
#[test]
fn unknown_code_stays_an_opaque_com_error() {
let error = Error::from(ComError::hresult(-2_147_209_215, "test"));
assert!(
matches!(error, Error::Com { .. }),
"expected Com variant, got {error:?}"
);
assert_eq!(error.code_name(), None);
assert_eq!(error.code(), Some(-2_147_209_215));
}
#[test]
fn type_mismatch_carries_no_code() {
let error = Error::from(ComError::UnexpectedType {
expected: "I32",
actual: "Str",
});
assert_eq!(error.code(), None);
assert_eq!(error.code_name(), None);
}
}