refactor(crates): implement std::error::Error for all module error enums
thiserror derive across oakundo/oakcommon/oaknode/oaktimeline/oaktask/ oakotio/oakcodec/oakaudio/oakrender/oakplugin/oakengine/oakstorage; Display carries the module prefix and the Failed context, source() stays default except oakotio's #[from] forwarding. code() mappings and variants unchanged; each error.rs gains Display/object-safety/code regression tests.
This commit is contained in:
Generated
+11
@@ -3964,6 +3964,7 @@ dependencies = [
|
||||
"oakcore-rs",
|
||||
"oakffmpeg-link",
|
||||
"portaudio",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3973,6 +3974,7 @@ dependencies = [
|
||||
"ffmpeg-next",
|
||||
"oakcore-rs",
|
||||
"oakffmpeg-link",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3984,6 +3986,7 @@ dependencies = [
|
||||
"oakcore-rs",
|
||||
"ocio-rs",
|
||||
"quick-xml 0.41.0",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4007,6 +4010,7 @@ dependencies = [
|
||||
"oakundo",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4021,6 +4025,7 @@ dependencies = [
|
||||
"oakcommon",
|
||||
"oakcore-rs",
|
||||
"oakundo",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4031,6 +4036,7 @@ dependencies = [
|
||||
"quick-xml 0.41.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4042,6 +4048,7 @@ dependencies = [
|
||||
"oaknode",
|
||||
"oakrender",
|
||||
"oakundo",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4053,6 +4060,7 @@ dependencies = [
|
||||
"oakcore-rs",
|
||||
"oaknode",
|
||||
"ocio-rs",
|
||||
"thiserror 2.0.20",
|
||||
"wgpu 25.0.2",
|
||||
]
|
||||
|
||||
@@ -4068,6 +4076,7 @@ dependencies = [
|
||||
"oakrender",
|
||||
"oaktimeline",
|
||||
"oakundo",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4078,6 +4087,7 @@ dependencies = [
|
||||
"oakcore-rs",
|
||||
"oaknode",
|
||||
"oakundo",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4085,6 +4095,7 @@ name = "oakundo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"oakcore-rs",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -21,3 +21,5 @@ ffmpeg-next = "9"
|
||||
# Real audio output (M12 P1): the preview playback stream. The PortAudio
|
||||
# C library must be installed (macOS: brew install portaudio).
|
||||
portaudio = "0.8"
|
||||
# `std::error::Error` impls for the crate-internal error enum.
|
||||
thiserror = "2"
|
||||
|
||||
@@ -36,17 +36,22 @@ pub const OAKAUDIO_E_NOMEM: i32 = -60005;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Null handle or invalid argument.
|
||||
#[error("audio: invalid argument")]
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
#[error("audio: invalid state")]
|
||||
State,
|
||||
/// Operation failed (context string is log-only).
|
||||
#[error("audio: operation failed: {0}")]
|
||||
Failed(String),
|
||||
/// Not found.
|
||||
#[error("audio: not found")]
|
||||
NotFound,
|
||||
/// Out of memory.
|
||||
#[error("audio: out of memory")]
|
||||
NoMem,
|
||||
}
|
||||
|
||||
@@ -62,3 +67,57 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every variant must produce a non-empty `Display` message; the
|
||||
/// `Failed` variant must surface its context string.
|
||||
#[test]
|
||||
fn display_is_non_empty() {
|
||||
for msg in [
|
||||
Error::Invalid.to_string(),
|
||||
Error::State.to_string(),
|
||||
Error::Failed("context".into()).to_string(),
|
||||
Error::NotFound.to_string(),
|
||||
Error::NoMem.to_string(),
|
||||
] {
|
||||
assert!(!msg.trim().is_empty(), "empty Display message");
|
||||
}
|
||||
assert!(
|
||||
Error::Failed("context".into())
|
||||
.to_string()
|
||||
.contains("context")
|
||||
);
|
||||
}
|
||||
|
||||
/// `Error` must be usable behind a trait object.
|
||||
#[test]
|
||||
fn error_is_object_safe() {
|
||||
let errs: Vec<Box<dyn std::error::Error>> = vec![
|
||||
Box::new(Error::Invalid),
|
||||
Box::new(Error::State),
|
||||
Box::new(Error::Failed("context".into())),
|
||||
Box::new(Error::NotFound),
|
||||
Box::new(Error::NoMem),
|
||||
];
|
||||
assert_eq!(errs.len(), 5);
|
||||
}
|
||||
|
||||
/// No variant wraps a downstream error, so `source()` stays `None`.
|
||||
#[test]
|
||||
fn source_is_none() {
|
||||
assert!(std::error::Error::source(&Error::Failed("context".into())).is_none());
|
||||
}
|
||||
|
||||
/// The `code()` mapping must be unchanged by the `Error` trait impl.
|
||||
#[test]
|
||||
fn code_mapping_unchanged() {
|
||||
assert_eq!(Error::Invalid.code(), OAKAUDIO_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKAUDIO_E_STATE);
|
||||
assert_eq!(Error::Failed("context".into()).code(), OAKAUDIO_E_FAILED);
|
||||
assert_eq!(Error::NotFound.code(), OAKAUDIO_E_NOT_FOUND);
|
||||
assert_eq!(Error::NoMem.code(), OAKAUDIO_E_NOMEM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,3 +32,5 @@ oakcore-rs = { path = "../oakcore" }
|
||||
# (9.0.0 -> FFmpeg 9.0 removed AVCodec fields; 8.1.0 -> FFmpeg 8.1 added
|
||||
# enum variants; 8.0.0 -> FFmpeg 8.0 renamed FF_PROFILE_*).
|
||||
ffmpeg-next = { version = "9.0", features = ["static"] }
|
||||
# `std::error::Error` impls for the crate-internal error enum.
|
||||
thiserror = "2"
|
||||
|
||||
@@ -39,19 +39,25 @@ pub const OAKCODEC_ABI_VERSION: u32 = 1;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Null handle or invalid argument.
|
||||
#[error("codec: invalid argument")]
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
#[error("codec: invalid state")]
|
||||
State,
|
||||
/// Operation failed (context string is log-only).
|
||||
#[error("codec: operation failed: {0}")]
|
||||
Failed(String),
|
||||
/// Not found.
|
||||
#[error("codec: not found")]
|
||||
NotFound,
|
||||
/// Out of memory.
|
||||
#[error("codec: out of memory")]
|
||||
NoMem,
|
||||
/// The operation was cancelled.
|
||||
#[error("codec: operation cancelled")]
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
@@ -68,3 +74,60 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every variant must produce a non-empty `Display` message; the
|
||||
/// `Failed` variant must surface its context string.
|
||||
#[test]
|
||||
fn display_is_non_empty() {
|
||||
for msg in [
|
||||
Error::Invalid.to_string(),
|
||||
Error::State.to_string(),
|
||||
Error::Failed("context".into()).to_string(),
|
||||
Error::NotFound.to_string(),
|
||||
Error::NoMem.to_string(),
|
||||
Error::Cancelled.to_string(),
|
||||
] {
|
||||
assert!(!msg.trim().is_empty(), "empty Display message");
|
||||
}
|
||||
assert!(
|
||||
Error::Failed("context".into())
|
||||
.to_string()
|
||||
.contains("context")
|
||||
);
|
||||
}
|
||||
|
||||
/// `Error` must be usable behind a trait object.
|
||||
#[test]
|
||||
fn error_is_object_safe() {
|
||||
let errs: Vec<Box<dyn std::error::Error>> = vec![
|
||||
Box::new(Error::Invalid),
|
||||
Box::new(Error::State),
|
||||
Box::new(Error::Failed("context".into())),
|
||||
Box::new(Error::NotFound),
|
||||
Box::new(Error::NoMem),
|
||||
Box::new(Error::Cancelled),
|
||||
];
|
||||
assert_eq!(errs.len(), 6);
|
||||
}
|
||||
|
||||
/// No variant wraps a downstream error, so `source()` stays `None`.
|
||||
#[test]
|
||||
fn source_is_none() {
|
||||
assert!(std::error::Error::source(&Error::Failed("context".into())).is_none());
|
||||
}
|
||||
|
||||
/// The `code()` mapping must be unchanged by the `Error` trait impl.
|
||||
#[test]
|
||||
fn code_mapping_unchanged() {
|
||||
assert_eq!(Error::Invalid.code(), OAKCODEC_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKCODEC_E_STATE);
|
||||
assert_eq!(Error::Failed("context".into()).code(), OAKCODEC_E_FAILED);
|
||||
assert_eq!(Error::NotFound.code(), OAKCODEC_E_NOT_FOUND);
|
||||
assert_eq!(Error::NoMem.code(), OAKCODEC_E_NOMEM);
|
||||
assert_eq!(Error::Cancelled.code(), OAKCODEC_E_CANCELLED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +30,4 @@ ocio-rs = "0.2.1"
|
||||
# derives per-channel bit depths from its color-type tables and does float
|
||||
# image I/O through it. See README.md.
|
||||
image = { version = "0.25", default-features = false, features = ["tiff"] }
|
||||
thiserror = "2"
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
//! Error codes, mirroring `include/common/error.h`; project-wide
|
||||
//! -MMCCCC scheme (module 01), pass-through untranslated.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Success.
|
||||
pub const OAKCOMMON_OK: i32 = 0;
|
||||
/// Empty handle or invalid argument.
|
||||
@@ -34,17 +36,22 @@ pub const OAKCOMMON_E_NOMEM: i32 = -10005;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Empty handle or invalid argument.
|
||||
#[error("common: empty handle or invalid argument")]
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
#[error("common: call not valid in current state")]
|
||||
State,
|
||||
/// Operation failed (context string is log-only).
|
||||
#[error("common: operation failed: {0}")]
|
||||
Failed(String),
|
||||
/// Not found.
|
||||
#[error("common: entry not found")]
|
||||
NotFound,
|
||||
/// Out of memory.
|
||||
#[error("common: allocation failed")]
|
||||
NoMem,
|
||||
}
|
||||
|
||||
@@ -153,4 +160,30 @@ mod tests {
|
||||
assert_eq!(e.code(), OAKCOMMON_E_FAILED);
|
||||
assert!(format!("{e:?}").contains("bad colorspace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_is_non_empty_for_each_variant() {
|
||||
let variants = [
|
||||
Error::Invalid,
|
||||
Error::State,
|
||||
Error::Failed("context".to_string()),
|
||||
Error::NotFound,
|
||||
Error::NoMem,
|
||||
];
|
||||
for e in &variants {
|
||||
assert!(!e.to_string().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_display_includes_context() {
|
||||
let e = Error::Failed("context info".to_string());
|
||||
assert!(e.to_string().contains("context info"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_is_object_safe() {
|
||||
let e: Box<dyn std::error::Error> = Box::new(Error::NoMem);
|
||||
assert!(!e.to_string().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ serde_json = "1"
|
||||
# POSIX shm_open/mmap/munmap/shm_unlink constants + syscalls for the
|
||||
# shared-memory frame-slot transport (src/ipc.rs).
|
||||
libc = "0.2"
|
||||
# Error derive (Display + std::error::Error) for the crate error enum
|
||||
# (src/error.rs). Same major version the other modules use (oakotio, ...).
|
||||
thiserror = "2"
|
||||
|
||||
# Every module call crosses the module C ABI as an `extern "C"` import
|
||||
# (src/bridge/). The module crates below are REAL dependencies so their
|
||||
|
||||
@@ -40,8 +40,10 @@ tests/
|
||||
### Dependencies
|
||||
|
||||
The facade's regular dependencies are `serde`/`serde_json` (the worker's
|
||||
NDJSON control-plane protocol, `src/worker.rs`) and `libc` (POSIX
|
||||
`shm_open`/`mmap`/`munmap`/`shm_unlink` for `src/ipc.rs`). Every module
|
||||
NDJSON control-plane protocol, `src/worker.rs`), `libc` (POSIX
|
||||
`shm_open`/`mmap`/`munmap`/`shm_unlink` for `src/ipc.rs`) and `thiserror`
|
||||
(the `Display` + `std::error::Error` impl for the facade error enum,
|
||||
`src/error.rs`). Every module
|
||||
call crosses the module C ABI as an `extern "C"` import (`src/bridge/`),
|
||||
and the module crates themselves are real dependencies: [`linkage`](src/linkage.rs)
|
||||
anchors them so their `#[no_mangle]` exports are linked into the
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
//! (e.g. -20004 is oakundo's NOT_FOUND, -30001 oaknode's INVALID) and the
|
||||
//! facade never rewrites them.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Success.
|
||||
pub const OAKENGINE_OK: i32 = 0;
|
||||
/// Empty handle or invalid argument.
|
||||
@@ -43,21 +45,28 @@ pub const OAKENGINE_E_CANCELLED: i32 = -6;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Empty handle or invalid argument.
|
||||
#[error("engine: invalid argument")]
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
#[error("engine: call not valid in the current state")]
|
||||
State,
|
||||
/// The underlying operation failed (context string is log-only).
|
||||
#[error("engine: operation failed: {0}")]
|
||||
Failed(String),
|
||||
/// Not found.
|
||||
#[error("engine: not found")]
|
||||
NotFound,
|
||||
/// Out of memory.
|
||||
#[error("engine: out of memory")]
|
||||
NoMem,
|
||||
/// Cancelled.
|
||||
#[error("engine: cancelled")]
|
||||
Cancelled,
|
||||
/// A module error code that must pass through untranslated.
|
||||
#[error("engine: module error code {0}")]
|
||||
Module(i32),
|
||||
}
|
||||
|
||||
@@ -85,3 +94,62 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// One instance of every variant (data-carrying ones get a sample
|
||||
/// payload).
|
||||
fn all_errors() -> Vec<Error> {
|
||||
vec![
|
||||
Error::Invalid,
|
||||
Error::State,
|
||||
Error::Failed("boom".to_string()),
|
||||
Error::NotFound,
|
||||
Error::NoMem,
|
||||
Error::Cancelled,
|
||||
Error::Module(-20004),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_is_non_empty_for_every_variant() {
|
||||
for e in all_errors() {
|
||||
let s = e.to_string();
|
||||
assert!(!s.is_empty(), "Display produced an empty message for {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_is_object_safe() {
|
||||
// `Box<dyn std::error::Error>` must be constructible for every
|
||||
// variant; `source()` stays None (no wrapped downstream error).
|
||||
let errors: Vec<Box<dyn std::error::Error>> = all_errors()
|
||||
.into_iter()
|
||||
.map(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||
.collect();
|
||||
for e in &errors {
|
||||
assert!(!e.to_string().is_empty());
|
||||
assert!(e.source().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_is_unaffected_by_trait_impl() {
|
||||
assert_eq!(Error::Invalid.code(), OAKENGINE_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKENGINE_E_STATE);
|
||||
assert_eq!(Error::Failed("boom".to_string()).code(), OAKENGINE_E_FAILED);
|
||||
assert_eq!(Error::NotFound.code(), OAKENGINE_E_NOT_FOUND);
|
||||
assert_eq!(Error::NoMem.code(), OAKENGINE_E_NOMEM);
|
||||
assert_eq!(Error::Cancelled.code(), OAKENGINE_E_CANCELLED);
|
||||
// Module codes pass through verbatim, untranslated.
|
||||
assert_eq!(Error::Module(-20004).code(), -20004);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_module_wraps_negative_and_accepts_ok() {
|
||||
assert!(Error::from_module(0).is_ok());
|
||||
assert_eq!(Error::from_module(-20004).unwrap_err().code(), -20004);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ oakcore-rs = { path = "../oakcore" }
|
||||
oakcommon = { path = "../oakcommon" }
|
||||
oakundo = { path = "../oakundo" }
|
||||
oakcodec = { path = "../oakcodec" }
|
||||
thiserror = "2"
|
||||
|
||||
[features]
|
||||
# In-crate stubs for the oakundo C ABI (and other module bridges) so
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
//! Error codes, mirroring `include/node/error.h` verbatim; project-wide
|
||||
//! -MMCCCC scheme (module registry in include/common/error.h), pass-through untranslated.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Success.
|
||||
pub const OAKNODE_OK: i32 = 0;
|
||||
/// Null handle or invalid argument.
|
||||
@@ -34,17 +36,22 @@ pub const OAKNODE_E_NOMEM: i32 = -30005;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Error)]
|
||||
pub enum Error {
|
||||
/// Null handle or invalid argument.
|
||||
#[error("node: empty handle or invalid argument")]
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
#[error("node: call not valid in current state")]
|
||||
State,
|
||||
/// Operation failed (context string is log-only).
|
||||
#[error("node: operation failed: {0}")]
|
||||
Failed(String),
|
||||
/// Not found.
|
||||
#[error("node: entry not found")]
|
||||
NotFound,
|
||||
/// Out of memory.
|
||||
#[error("node: allocation failed")]
|
||||
NoMem,
|
||||
}
|
||||
|
||||
@@ -60,3 +67,55 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn public_codes_match_header_values() {
|
||||
// Load-bearing values from include/node/error.h (module 03).
|
||||
assert_eq!(OAKNODE_OK, 0);
|
||||
assert_eq!(OAKNODE_E_INVALID, -30001);
|
||||
assert_eq!(OAKNODE_E_STATE, -30002);
|
||||
assert_eq!(OAKNODE_E_FAILED, -30003);
|
||||
assert_eq!(OAKNODE_E_NOT_FOUND, -30004);
|
||||
assert_eq!(OAKNODE_E_NOMEM, -30005);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_maps_each_variant() {
|
||||
assert_eq!(Error::Invalid.code(), OAKNODE_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKNODE_E_STATE);
|
||||
assert_eq!(Error::Failed("boom".to_string()).code(), OAKNODE_E_FAILED);
|
||||
assert_eq!(Error::NotFound.code(), OAKNODE_E_NOT_FOUND);
|
||||
assert_eq!(Error::NoMem.code(), OAKNODE_E_NOMEM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_is_non_empty_for_each_variant() {
|
||||
let variants = [
|
||||
Error::Invalid,
|
||||
Error::State,
|
||||
Error::Failed("context".to_string()),
|
||||
Error::NotFound,
|
||||
Error::NoMem,
|
||||
];
|
||||
for e in &variants {
|
||||
assert!(!e.to_string().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_display_includes_context() {
|
||||
let e = Error::Failed("context info".to_string());
|
||||
assert!(e.to_string().contains("context info"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_is_object_safe() {
|
||||
let e: Box<dyn std::error::Error> = Box::new(Error::NoMem);
|
||||
assert!(!e.to_string().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ oakcore-rs = { path = "../oakcore" }
|
||||
# (src/common/rust/Cargo.toml).
|
||||
quick-xml = "0.41.0"
|
||||
|
||||
# Error trait derive for the crate error enum.
|
||||
thiserror = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
# oakcore-rs: exact Rational comparisons in the integration tests
|
||||
|
||||
+63
-35
@@ -16,47 +16,75 @@
|
||||
|
||||
//! Error type for the oakotio binding.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Errors produced by loading or saving OpenTimelineIO JSON.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum OtioError {
|
||||
/// The document could not be parsed (or a value could not be
|
||||
/// serialized) as JSON.
|
||||
Json(serde_json::Error),
|
||||
#[error("OpenTimelineIO JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
/// The underlying file could not be read or written.
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for OtioError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
OtioError::Json(e) => write!(f, "OpenTimelineIO JSON error: {e}"),
|
||||
OtioError::Io(e) => write!(f, "OpenTimelineIO file error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for OtioError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
OtioError::Json(e) => Some(e),
|
||||
OtioError::Io(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for OtioError {
|
||||
fn from(e: serde_json::Error) -> OtioError {
|
||||
OtioError::Json(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for OtioError {
|
||||
fn from(e: std::io::Error) -> OtioError {
|
||||
OtioError::Io(e)
|
||||
}
|
||||
#[error("OpenTimelineIO file error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Convenience alias used by the binding API.
|
||||
pub type Result<T> = std::result::Result<T, OtioError>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::error::Error as _;
|
||||
|
||||
/// Every variant renders a non-empty, module-prefixed message.
|
||||
#[test]
|
||||
fn display_is_non_empty() {
|
||||
let json_err = serde_json::from_str::<()>("not json").unwrap_err();
|
||||
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
|
||||
let cases = [
|
||||
(
|
||||
OtioError::Json(json_err),
|
||||
"OpenTimelineIO JSON error: ",
|
||||
),
|
||||
(OtioError::Io(io_err), "OpenTimelineIO file error: "),
|
||||
];
|
||||
for (err, prefix) in cases {
|
||||
let msg = err.to_string();
|
||||
assert!(!msg.is_empty(), "Display for {err:?} is empty");
|
||||
assert!(msg.starts_with(prefix), "{msg:?} lacks module prefix");
|
||||
}
|
||||
}
|
||||
|
||||
/// The wrapped error is reachable through `source()` (downstream error
|
||||
/// wrapping relationship).
|
||||
#[test]
|
||||
fn source_forwards() {
|
||||
let json_err = serde_json::from_str::<()>("not json").unwrap_err();
|
||||
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
|
||||
assert!(OtioError::Json(json_err).source().is_some());
|
||||
assert!(OtioError::Io(io_err).source().is_some());
|
||||
}
|
||||
|
||||
/// The `From` conversions (used by `?`) still work.
|
||||
#[test]
|
||||
fn from_conversions() {
|
||||
let json_err = serde_json::from_str::<()>("not json").unwrap_err();
|
||||
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
|
||||
assert!(matches!(OtioError::from(json_err), OtioError::Json(_)));
|
||||
assert!(matches!(OtioError::from(io_err), OtioError::Io(_)));
|
||||
}
|
||||
|
||||
/// The error is object-safe: every variant boxes into
|
||||
/// `Box<dyn std::error::Error>`.
|
||||
#[test]
|
||||
fn object_safe() {
|
||||
let json_err = serde_json::from_str::<()>("not json").unwrap_err();
|
||||
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
|
||||
let errs: Vec<Box<dyn std::error::Error>> =
|
||||
vec![Box::new(OtioError::Json(json_err)), Box::new(OtioError::Io(io_err))];
|
||||
assert_eq!(errs.len(), 2);
|
||||
for err in &errs {
|
||||
assert!(!err.to_string().is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ oakcore-rs = { path = "../oakcore" }
|
||||
oakundo = { path = "../oakundo" }
|
||||
oaknode = { path = "../oaknode" }
|
||||
oakrender = { path = "../oakrender" }
|
||||
# Error derive(Display + std::error::Error,见 src/error.rs);理由已登记 README。
|
||||
thiserror = "2"
|
||||
|
||||
[features]
|
||||
# 测试桩(库内状态访问器):cargo test --features test-stubs 时桥直连
|
||||
|
||||
@@ -180,6 +180,9 @@ src/
|
||||
- **依赖登记**:`cc`(build-dependencies)——编译 C shim 的唯一
|
||||
稳妥方式(手写 `Command::new("cc")` 无法处理跨平台 flag/交叉编译;
|
||||
零运行时依赖不变)。M11 第 2 期未新增依赖。
|
||||
- **依赖登记**:`thiserror`(dependencies)——为 crate 错误枚举
|
||||
(src/error.rs)派生 `Display` + `std::error::Error` 的惯例实现;
|
||||
项目内其他模块(oakotio 等)已采用同版本("2"),避免手写样板。
|
||||
- suite 函数表经 `suite_v1()` 等 accessor 暴露(`static` 初始化
|
||||
放 lazy/OnceLock 里),`fetch_suite` 只查表。
|
||||
- **GL 上下文归属**:oakplugin 不持有 GL 上下文(C ABI 无
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
//! 错误码。与 `include/*/error.h` 逐字对应;项目统一 -MMCCCC 方案
|
||||
//! (模块号注册表见 include/common/error.h),跨模块透传不翻译。
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// 成功。
|
||||
pub const OAKPLUGIN_OK: i32 = 0;
|
||||
/// 空句柄或非法参数。
|
||||
@@ -34,17 +36,22 @@ pub const OAKPLUGIN_E_NOMEM: i32 = -90005;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// crate 内部错误。`code()` 给出对外错误码。
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// 空句柄或非法参数。
|
||||
#[error("plugin: invalid argument")]
|
||||
Invalid,
|
||||
/// 状态不允许。
|
||||
#[error("plugin: call not valid in the current state")]
|
||||
State,
|
||||
/// 底层失败,附人类可读上下文(仅日志,不出界)。
|
||||
#[error("plugin: operation failed: {0}")]
|
||||
Failed(String),
|
||||
/// 未找到。
|
||||
#[error("plugin: not found")]
|
||||
NotFound,
|
||||
/// 分配失败。
|
||||
#[error("plugin: out of memory")]
|
||||
NoMem,
|
||||
}
|
||||
|
||||
@@ -60,3 +67,51 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// One instance of every variant (data-carrying ones get a sample
|
||||
/// payload).
|
||||
fn all_errors() -> Vec<Error> {
|
||||
vec![
|
||||
Error::Invalid,
|
||||
Error::State,
|
||||
Error::Failed("boom".to_string()),
|
||||
Error::NotFound,
|
||||
Error::NoMem,
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_is_non_empty_for_every_variant() {
|
||||
for e in all_errors() {
|
||||
let s = e.to_string();
|
||||
assert!(!s.is_empty(), "Display produced an empty message for {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_is_object_safe() {
|
||||
// `Box<dyn std::error::Error>` must be constructible for every
|
||||
// variant; `source()` stays None (no wrapped downstream error).
|
||||
let errors: Vec<Box<dyn std::error::Error>> = all_errors()
|
||||
.into_iter()
|
||||
.map(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||
.collect();
|
||||
for e in &errors {
|
||||
assert!(!e.to_string().is_empty());
|
||||
assert!(e.source().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_is_unaffected_by_trait_impl() {
|
||||
assert_eq!(Error::Invalid.code(), OAKPLUGIN_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKPLUGIN_E_STATE);
|
||||
assert_eq!(Error::Failed("boom".to_string()).code(), OAKPLUGIN_E_FAILED);
|
||||
assert_eq!(Error::NotFound.code(), OAKPLUGIN_E_NOT_FOUND);
|
||||
assert_eq!(Error::NoMem.code(), OAKPLUGIN_E_NOMEM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ wgpu = "25"
|
||||
# the vendored OpenColorIO C++ sources (cmake/ninja required); without it
|
||||
# ocio-sys builds a stub (all color tests then early-return).
|
||||
ocio-rs = { version = "0.2", features = ["bundled"] }
|
||||
# `std::error::Error` impls for the crate-internal error enum.
|
||||
thiserror = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
# Test binaries that pull oakcodec's decoder (the direct-call bridge) link
|
||||
|
||||
@@ -34,17 +34,22 @@ pub const OAKRENDER_E_NOMEM: i32 = -70005;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Null handle or invalid argument.
|
||||
#[error("render: invalid argument")]
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
#[error("render: invalid state")]
|
||||
State,
|
||||
/// Operation failed (context string is log-only).
|
||||
#[error("render: operation failed: {0}")]
|
||||
Failed(String),
|
||||
/// Not found.
|
||||
#[error("render: not found")]
|
||||
NotFound,
|
||||
/// Out of memory.
|
||||
#[error("render: out of memory")]
|
||||
NoMem,
|
||||
}
|
||||
|
||||
@@ -60,3 +65,57 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every variant must produce a non-empty `Display` message; the
|
||||
/// `Failed` variant must surface its context string.
|
||||
#[test]
|
||||
fn display_is_non_empty() {
|
||||
for msg in [
|
||||
Error::Invalid.to_string(),
|
||||
Error::State.to_string(),
|
||||
Error::Failed("context".into()).to_string(),
|
||||
Error::NotFound.to_string(),
|
||||
Error::NoMem.to_string(),
|
||||
] {
|
||||
assert!(!msg.trim().is_empty(), "empty Display message");
|
||||
}
|
||||
assert!(
|
||||
Error::Failed("context".into())
|
||||
.to_string()
|
||||
.contains("context")
|
||||
);
|
||||
}
|
||||
|
||||
/// `Error` must be usable behind a trait object.
|
||||
#[test]
|
||||
fn error_is_object_safe() {
|
||||
let errs: Vec<Box<dyn std::error::Error>> = vec![
|
||||
Box::new(Error::Invalid),
|
||||
Box::new(Error::State),
|
||||
Box::new(Error::Failed("context".into())),
|
||||
Box::new(Error::NotFound),
|
||||
Box::new(Error::NoMem),
|
||||
];
|
||||
assert_eq!(errs.len(), 5);
|
||||
}
|
||||
|
||||
/// No variant wraps a downstream error, so `source()` stays `None`.
|
||||
#[test]
|
||||
fn source_is_none() {
|
||||
assert!(std::error::Error::source(&Error::Failed("context".into())).is_none());
|
||||
}
|
||||
|
||||
/// The `code()` mapping must be unchanged by the `Error` trait impl.
|
||||
#[test]
|
||||
fn code_mapping_unchanged() {
|
||||
assert_eq!(Error::Invalid.code(), OAKRENDER_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKRENDER_E_STATE);
|
||||
assert_eq!(Error::Failed("context".into()).code(), OAKRENDER_E_FAILED);
|
||||
assert_eq!(Error::NotFound.code(), OAKRENDER_E_NOT_FOUND);
|
||||
assert_eq!(Error::NoMem.code(), OAKRENDER_E_NOMEM);
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+12
@@ -1336,8 +1336,10 @@ name = "oakotio"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"oakcore-rs",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1349,6 +1351,7 @@ dependencies = [
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -1532,6 +1535,15 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
|
||||
@@ -15,6 +15,9 @@ panic = "unwind"
|
||||
oakcore-rs = { path = "../oakcore" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
# Error derive (Display + std::error::Error) for the crate error enum
|
||||
# (src/error.rs). Same major version the other modules use (oakotio, ...).
|
||||
thiserror = "2"
|
||||
# Native OTIO JSON model (project-local; see src/bindings/oakotio).
|
||||
oakotio = { path = "../oakotio" }
|
||||
# Database backends via SeaORM (sync facade: a private current-thread
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
//! Error and info codes (M10 §2.1; -MMCCCC scheme, module 10).
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Success.
|
||||
pub const OAKSTORAGE_OK: i32 = 0;
|
||||
/// Project version too old (info code, positive).
|
||||
@@ -45,23 +47,31 @@ pub const OAKSTORAGE_E_NOMEM: i32 = -100008;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Null handle or invalid argument.
|
||||
#[error("storage: invalid argument")]
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
#[error("storage: call not valid in the current state")]
|
||||
State,
|
||||
/// Not found.
|
||||
#[error("storage: not found")]
|
||||
NotFound,
|
||||
/// Operation failed (context string is log-only).
|
||||
#[error("storage: operation failed: {0}")]
|
||||
Failed(String),
|
||||
/// No backend claimed the URI.
|
||||
#[error("storage: no backend claimed the URI")]
|
||||
NoBackend,
|
||||
/// Format error.
|
||||
#[error("storage: format error: {0}")]
|
||||
Format(String),
|
||||
/// I/O error.
|
||||
#[error("storage: I/O error: {0}")]
|
||||
Io(String),
|
||||
/// Out of memory.
|
||||
#[error("storage: out of memory")]
|
||||
NoMem,
|
||||
}
|
||||
|
||||
@@ -80,3 +90,57 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// One instance of every variant (data-carrying ones get a sample
|
||||
/// payload).
|
||||
fn all_errors() -> Vec<Error> {
|
||||
vec![
|
||||
Error::Invalid,
|
||||
Error::State,
|
||||
Error::NotFound,
|
||||
Error::Failed("boom".to_string()),
|
||||
Error::NoBackend,
|
||||
Error::Format("bad xml".to_string()),
|
||||
Error::Io("disk full".to_string()),
|
||||
Error::NoMem,
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_is_non_empty_for_every_variant() {
|
||||
for e in all_errors() {
|
||||
let s = e.to_string();
|
||||
assert!(!s.is_empty(), "Display produced an empty message for {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_is_object_safe() {
|
||||
// `Box<dyn std::error::Error>` must be constructible for every
|
||||
// variant; `source()` stays None (no wrapped downstream error).
|
||||
let errors: Vec<Box<dyn std::error::Error>> = all_errors()
|
||||
.into_iter()
|
||||
.map(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||
.collect();
|
||||
for e in &errors {
|
||||
assert!(!e.to_string().is_empty());
|
||||
assert!(e.source().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_is_unaffected_by_trait_impl() {
|
||||
assert_eq!(Error::Invalid.code(), OAKSTORAGE_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKSTORAGE_E_STATE);
|
||||
assert_eq!(Error::NotFound.code(), OAKSTORAGE_E_NOT_FOUND);
|
||||
assert_eq!(Error::Failed("boom".to_string()).code(), OAKSTORAGE_E_FAILED);
|
||||
assert_eq!(Error::NoBackend.code(), OAKSTORAGE_E_NO_BACKEND);
|
||||
assert_eq!(Error::Format("bad xml".to_string()).code(), OAKSTORAGE_E_FORMAT);
|
||||
assert_eq!(Error::Io("disk full".to_string()).code(), OAKSTORAGE_E_IO);
|
||||
assert_eq!(Error::NoMem.code(), OAKSTORAGE_E_NOMEM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,3 +42,5 @@ oakotio = { path = "../oakotio" }
|
||||
# render-real integration test's cargo invocation working (it links the
|
||||
# real oakrender — now always — and skips the ticket stubs).
|
||||
oakrender = { path = "../oakrender" }
|
||||
# Error trait derive for the crate-internal error enum.
|
||||
thiserror = "2"
|
||||
|
||||
@@ -38,19 +38,25 @@ pub const OAKTASK_E_CANCELLED: i32 = -80006;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Null handle or invalid argument.
|
||||
#[error("task: invalid argument")]
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
#[error("task: wrong state")]
|
||||
State,
|
||||
/// Operation failed (context string is log-only).
|
||||
#[error("task: operation failed: {0}")]
|
||||
Failed(String),
|
||||
/// Not found.
|
||||
#[error("task: not found")]
|
||||
NotFound,
|
||||
/// Out of memory.
|
||||
#[error("task: out of memory")]
|
||||
NoMem,
|
||||
/// The operation was cancelled.
|
||||
#[error("task: cancelled")]
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
@@ -67,3 +73,62 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every variant renders a non-empty, module-prefixed message.
|
||||
#[test]
|
||||
fn display_is_non_empty() {
|
||||
let cases = [
|
||||
(Error::Invalid, "task: invalid argument"),
|
||||
(Error::State, "task: wrong state"),
|
||||
(Error::Failed("boom".into()), "task: operation failed: boom"),
|
||||
(Error::NotFound, "task: not found"),
|
||||
(Error::NoMem, "task: out of memory"),
|
||||
(Error::Cancelled, "task: cancelled"),
|
||||
];
|
||||
for (err, expect) in cases {
|
||||
let msg = err.to_string();
|
||||
assert!(!msg.is_empty(), "Display for {err:?} is empty");
|
||||
assert_eq!(msg, expect, "Display for {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `Failed` carries the context string in its message.
|
||||
#[test]
|
||||
fn failed_includes_context() {
|
||||
let msg = Error::Failed("disk full".into()).to_string();
|
||||
assert!(msg.contains("disk full"));
|
||||
}
|
||||
|
||||
/// The error is object-safe: every variant boxes into
|
||||
/// `Box<dyn std::error::Error>`.
|
||||
#[test]
|
||||
fn object_safe() {
|
||||
let errs: Vec<Box<dyn std::error::Error>> = vec![
|
||||
Box::new(Error::Invalid),
|
||||
Box::new(Error::State),
|
||||
Box::new(Error::Failed("boom".into())),
|
||||
Box::new(Error::NotFound),
|
||||
Box::new(Error::NoMem),
|
||||
Box::new(Error::Cancelled),
|
||||
];
|
||||
assert_eq!(errs.len(), 6);
|
||||
for err in &errs {
|
||||
assert!(!err.to_string().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementing `std::error::Error` must not change the FFI code mapping.
|
||||
#[test]
|
||||
fn codes_unchanged() {
|
||||
assert_eq!(Error::Invalid.code(), OAKTASK_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKTASK_E_STATE);
|
||||
assert_eq!(Error::Failed("x".into()).code(), OAKTASK_E_FAILED);
|
||||
assert_eq!(Error::NotFound.code(), OAKTASK_E_NOT_FOUND);
|
||||
assert_eq!(Error::NoMem.code(), OAKTASK_E_NOMEM);
|
||||
assert_eq!(Error::Cancelled.code(), OAKTASK_E_CANCELLED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,5 @@ oakcore-rs = { path = "../oakcore" }
|
||||
oakundo = { path = "../oakundo" }
|
||||
oaknode = { path = "../oaknode" }
|
||||
oakcommon = { path = "../oakcommon" }
|
||||
# Error trait derive for the crate-internal error enum.
|
||||
thiserror = "2"
|
||||
|
||||
@@ -38,17 +38,22 @@ pub const OAKTIMELINE_E_NOMEM: i32 = -40005;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Null handle or invalid argument.
|
||||
#[error("timeline: invalid argument")]
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
#[error("timeline: wrong state")]
|
||||
State,
|
||||
/// Operation failed (context string is log-only).
|
||||
#[error("timeline: operation failed: {0}")]
|
||||
Failed(String),
|
||||
/// Index out of range / entry not found.
|
||||
#[error("timeline: not found")]
|
||||
NotFound,
|
||||
/// Out of memory.
|
||||
#[error("timeline: out of memory")]
|
||||
NoMem,
|
||||
}
|
||||
|
||||
@@ -64,3 +69,59 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every variant renders a non-empty, module-prefixed message.
|
||||
#[test]
|
||||
fn display_is_non_empty() {
|
||||
let cases = [
|
||||
(Error::Invalid, "timeline: invalid argument"),
|
||||
(Error::State, "timeline: wrong state"),
|
||||
(Error::Failed("boom".into()), "timeline: operation failed: boom"),
|
||||
(Error::NotFound, "timeline: not found"),
|
||||
(Error::NoMem, "timeline: out of memory"),
|
||||
];
|
||||
for (err, expect) in cases {
|
||||
let msg = err.to_string();
|
||||
assert!(!msg.is_empty(), "Display for {err:?} is empty");
|
||||
assert_eq!(msg, expect, "Display for {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `Failed` carries the context string in its message.
|
||||
#[test]
|
||||
fn failed_includes_context() {
|
||||
let msg = Error::Failed("disk full".into()).to_string();
|
||||
assert!(msg.contains("disk full"));
|
||||
}
|
||||
|
||||
/// The error is object-safe: every variant boxes into
|
||||
/// `Box<dyn std::error::Error>`.
|
||||
#[test]
|
||||
fn object_safe() {
|
||||
let errs: Vec<Box<dyn std::error::Error>> = vec![
|
||||
Box::new(Error::Invalid),
|
||||
Box::new(Error::State),
|
||||
Box::new(Error::Failed("boom".into())),
|
||||
Box::new(Error::NotFound),
|
||||
Box::new(Error::NoMem),
|
||||
];
|
||||
assert_eq!(errs.len(), 5);
|
||||
for err in &errs {
|
||||
assert!(!err.to_string().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementing `std::error::Error` must not change the FFI code mapping.
|
||||
#[test]
|
||||
fn codes_unchanged() {
|
||||
assert_eq!(Error::Invalid.code(), OAKTIMELINE_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKTIMELINE_E_STATE);
|
||||
assert_eq!(Error::Failed("x".into()).code(), OAKTIMELINE_E_FAILED);
|
||||
assert_eq!(Error::NotFound.code(), OAKTIMELINE_E_NOT_FOUND);
|
||||
assert_eq!(Error::NoMem.code(), OAKTIMELINE_E_NOMEM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,3 +10,4 @@ crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
oakcore-rs = { path = "../oakcore" }
|
||||
thiserror = "2"
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
//! Error codes, mirroring `include/undo/error.h`; project-wide
|
||||
//! -MMCCCC scheme (module 02), pass-through untranslated.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Success.
|
||||
pub const OAKUNDO_OK: i32 = 0;
|
||||
/// Empty handle or invalid argument.
|
||||
@@ -34,17 +36,22 @@ pub const OAKUNDO_E_NOMEM: i32 = -20005;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Empty handle or invalid argument.
|
||||
#[error("undo: empty handle or invalid argument")]
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
#[error("undo: call not valid in current state")]
|
||||
State,
|
||||
/// Operation failed (context string is log-only).
|
||||
#[error("undo: operation failed: {0}")]
|
||||
Failed(String),
|
||||
/// Not found.
|
||||
#[error("undo: entry not found")]
|
||||
NotFound,
|
||||
/// Out of memory.
|
||||
#[error("undo: allocation failed")]
|
||||
NoMem,
|
||||
}
|
||||
|
||||
@@ -60,3 +67,55 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn public_codes_match_header_values() {
|
||||
// Load-bearing values from include/undo/error.h (module 02).
|
||||
assert_eq!(OAKUNDO_OK, 0);
|
||||
assert_eq!(OAKUNDO_E_INVALID, -20001);
|
||||
assert_eq!(OAKUNDO_E_STATE, -20002);
|
||||
assert_eq!(OAKUNDO_E_FAILED, -20003);
|
||||
assert_eq!(OAKUNDO_E_NOT_FOUND, -20004);
|
||||
assert_eq!(OAKUNDO_E_NOMEM, -20005);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_maps_each_variant() {
|
||||
assert_eq!(Error::Invalid.code(), OAKUNDO_E_INVALID);
|
||||
assert_eq!(Error::State.code(), OAKUNDO_E_STATE);
|
||||
assert_eq!(Error::Failed("boom".to_string()).code(), OAKUNDO_E_FAILED);
|
||||
assert_eq!(Error::NotFound.code(), OAKUNDO_E_NOT_FOUND);
|
||||
assert_eq!(Error::NoMem.code(), OAKUNDO_E_NOMEM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_is_non_empty_for_each_variant() {
|
||||
let variants = [
|
||||
Error::Invalid,
|
||||
Error::State,
|
||||
Error::Failed("context".to_string()),
|
||||
Error::NotFound,
|
||||
Error::NoMem,
|
||||
];
|
||||
for e in &variants {
|
||||
assert!(!e.to_string().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_display_includes_context() {
|
||||
let e = Error::Failed("context info".to_string());
|
||||
assert!(e.to_string().contains("context info"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_is_object_safe() {
|
||||
let e: Box<dyn std::error::Error> = Box::new(Error::NoMem);
|
||||
assert!(!e.to_string().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,15 +87,12 @@ impl WaveformCache {
|
||||
/// when missing. Failures (missing media, no audio stream) leave the
|
||||
/// clip silent (no waveform drawn).
|
||||
pub fn refresh(&self, clip: u64, filename: &str, duration_frames: i64) {
|
||||
eprintln!("DBG-WFC: refresh clip={clip}");
|
||||
if self.get(clip).is_some() {
|
||||
eprintln!("DBG-WFC: cache hit");
|
||||
return;
|
||||
}
|
||||
let Some(waveform) = extract(filename, duration_frames, self.fps) else {
|
||||
return;
|
||||
};
|
||||
eprintln!("DBG-WFC: inserting");
|
||||
let mut map = self.map.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !map.contains_key(&clip) {
|
||||
map.insert(clip, Arc::new(waveform));
|
||||
|
||||
Reference in New Issue
Block a user