1#![cfg_attr(not(feature = "std"), no_std)]
23extern crate alloc;
24
25#[cfg(feature = "reflect")]
26pub use bevy_reflect::Reflect;
27use bincode::de::{BorrowDecoder, Decoder};
28use bincode::enc::Encoder;
29use bincode::enc::write::Writer;
30use bincode::error::{DecodeError, EncodeError};
31use bincode::{BorrowDecode, Decode as dDecode, Decode, Encode, Encode as dEncode};
32use compact_str::CompactString;
33use cu29_clock::{PartialCuTimeRange, Tov};
34use serde::de::{self, SeqAccess, Visitor};
35use serde::{Deserialize, Deserializer, Serialize};
36
37use alloc::borrow::ToOwned;
38use alloc::boxed::Box;
39use alloc::format;
40use alloc::string::{String, ToString};
41use alloc::vec::Vec;
42#[cfg(feature = "std")]
43use core::cell::Cell;
44#[cfg(not(feature = "std"))]
45use core::error::Error as CoreError;
46use core::fmt::{Debug, Display, Formatter};
47#[cfg(feature = "std")]
48use std::error::Error;
49
50#[cfg(not(feature = "std"))]
51use spin::Mutex as SyncMutex;
52
53#[cfg(feature = "std")]
55type DynError = dyn std::error::Error + Send + Sync + 'static;
56#[cfg(not(feature = "std"))]
57type DynError = dyn core::error::Error + Send + Sync + 'static;
58
59#[derive(Debug)]
62struct StringError(String);
63
64impl Display for StringError {
65 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
66 write!(f, "{}", self.0)
67 }
68}
69
70#[cfg(feature = "std")]
71impl std::error::Error for StringError {}
72
73#[cfg(not(feature = "std"))]
74impl core::error::Error for StringError {}
75
76pub struct CuError {
82 message: String,
83 cause: Option<Box<DynError>>,
84}
85
86impl Debug for CuError {
88 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
89 f.debug_struct("CuError")
90 .field("message", &self.message)
91 .field("cause", &self.cause.as_ref().map(|e| e.to_string()))
92 .finish()
93 }
94}
95
96impl Clone for CuError {
98 fn clone(&self) -> Self {
99 CuError {
100 message: self.message.clone(),
101 cause: self
102 .cause
103 .as_ref()
104 .map(|e| Box::new(StringError(e.to_string())) as Box<DynError>),
105 }
106 }
107}
108
109impl Serialize for CuError {
111 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
112 where
113 S: serde::Serializer,
114 {
115 use serde::ser::SerializeStruct;
116 let mut state = serializer.serialize_struct("CuError", 2)?;
117 state.serialize_field("message", &self.message)?;
118 state.serialize_field("cause", &self.cause.as_ref().map(|e| e.to_string()))?;
119 state.end()
120 }
121}
122
123impl<'de> Deserialize<'de> for CuError {
125 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
126 where
127 D: serde::Deserializer<'de>,
128 {
129 #[derive(Deserialize)]
130 struct CuErrorHelper {
131 message: String,
132 cause: Option<String>,
133 }
134
135 let helper = CuErrorHelper::deserialize(deserializer)?;
136 Ok(CuError {
137 message: helper.message,
138 cause: helper
139 .cause
140 .map(|s| Box::new(StringError(s)) as Box<DynError>),
141 })
142 }
143}
144
145impl Display for CuError {
146 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
147 let context_str = match &self.cause {
148 Some(c) => c.to_string(),
149 None => "None".to_string(),
150 };
151 write!(f, "{}\n context:{}", self.message, context_str)?;
152 Ok(())
153 }
154}
155
156#[cfg(not(feature = "std"))]
157impl CoreError for CuError {
158 fn source(&self) -> Option<&(dyn CoreError + 'static)> {
159 self.cause
160 .as_deref()
161 .map(|e| e as &(dyn CoreError + 'static))
162 }
163}
164
165#[cfg(feature = "std")]
166impl Error for CuError {
167 fn source(&self) -> Option<&(dyn Error + 'static)> {
168 self.cause.as_deref().map(|e| e as &(dyn Error + 'static))
169 }
170}
171
172impl From<&str> for CuError {
173 fn from(s: &str) -> CuError {
174 CuError {
175 message: s.to_string(),
176 cause: None,
177 }
178 }
179}
180
181impl From<String> for CuError {
182 fn from(s: String) -> CuError {
183 CuError {
184 message: s,
185 cause: None,
186 }
187 }
188}
189
190impl CuError {
191 pub fn new(message_index: usize) -> CuError {
197 CuError {
198 message: format!("[interned:{}]", message_index),
199 cause: None,
200 }
201 }
202
203 #[cfg(feature = "std")]
213 pub fn new_with_cause<E>(message: &str, cause: E) -> CuError
214 where
215 E: std::error::Error + Send + Sync + 'static,
216 {
217 CuError {
218 message: message.to_string(),
219 cause: Some(Box::new(cause)),
220 }
221 }
222
223 #[cfg(not(feature = "std"))]
225 pub fn new_with_cause<E>(message: &str, cause: E) -> CuError
226 where
227 E: core::error::Error + Send + Sync + 'static,
228 {
229 CuError {
230 message: message.to_string(),
231 cause: Some(Box::new(cause)),
232 }
233 }
234
235 pub fn add_cause(mut self, context: &str) -> CuError {
246 self.cause = Some(Box::new(StringError(context.to_string())));
247 self
248 }
249
250 #[cfg(feature = "std")]
260 pub fn with_cause<E>(mut self, cause: E) -> CuError
261 where
262 E: std::error::Error + Send + Sync + 'static,
263 {
264 self.cause = Some(Box::new(cause));
265 self
266 }
267
268 #[cfg(not(feature = "std"))]
270 pub fn with_cause<E>(mut self, cause: E) -> CuError
271 where
272 E: core::error::Error + Send + Sync + 'static,
273 {
274 self.cause = Some(Box::new(cause));
275 self
276 }
277
278 pub fn cause(&self) -> Option<&(dyn core::error::Error + Send + Sync + 'static)> {
280 self.cause.as_deref()
281 }
282
283 pub fn message(&self) -> &str {
285 &self.message
286 }
287}
288
289#[cfg(feature = "std")]
301pub fn with_cause<E>(message: &str, cause: E) -> CuError
302where
303 E: std::error::Error + Send + Sync + 'static,
304{
305 CuError::new_with_cause(message, cause)
306}
307
308#[cfg(not(feature = "std"))]
310pub fn with_cause<E>(message: &str, cause: E) -> CuError
311where
312 E: core::error::Error + Send + Sync + 'static,
313{
314 CuError::new_with_cause(message, cause)
315}
316
317pub type CuResult<T> = Result<T, CuError>;
319
320#[cfg(feature = "std")]
321thread_local! {
322 static OBSERVED_ENCODE_BYTES: Cell<Option<usize>> = const { Cell::new(None) };
323}
324
325#[cfg(not(feature = "std"))]
326static OBSERVED_ENCODE_BYTES: SyncMutex<Option<usize>> = SyncMutex::new(None);
327
328pub fn begin_observed_encode() {
330 #[cfg(feature = "std")]
331 OBSERVED_ENCODE_BYTES.with(|bytes| {
332 debug_assert!(
333 bytes.get().is_none(),
334 "observed encode measurement must not be nested"
335 );
336 bytes.set(Some(0));
337 });
338
339 #[cfg(not(feature = "std"))]
340 {
341 let mut bytes = OBSERVED_ENCODE_BYTES.lock();
342 debug_assert!(
343 bytes.is_none(),
344 "observed encode measurement must not be nested"
345 );
346 *bytes = Some(0);
347 }
348}
349
350pub fn finish_observed_encode() -> usize {
352 #[cfg(feature = "std")]
353 {
354 OBSERVED_ENCODE_BYTES.with(|bytes| bytes.replace(None).unwrap_or(0))
355 }
356
357 #[cfg(not(feature = "std"))]
358 {
359 OBSERVED_ENCODE_BYTES.lock().take().unwrap_or(0)
360 }
361}
362
363pub fn abort_observed_encode() {
365 #[cfg(feature = "std")]
366 OBSERVED_ENCODE_BYTES.with(|bytes| bytes.set(None));
367
368 #[cfg(not(feature = "std"))]
369 {
370 *OBSERVED_ENCODE_BYTES.lock() = None;
371 }
372}
373
374pub fn observed_encode_bytes() -> usize {
376 #[cfg(feature = "std")]
377 {
378 OBSERVED_ENCODE_BYTES.with(|bytes| bytes.get().unwrap_or(0))
379 }
380
381 #[cfg(not(feature = "std"))]
382 {
383 OBSERVED_ENCODE_BYTES.lock().as_ref().copied().unwrap_or(0)
384 }
385}
386
387pub fn record_observed_encode_bytes(bytes: usize) {
389 #[cfg(feature = "std")]
390 OBSERVED_ENCODE_BYTES.with(|total| {
391 if let Some(current) = total.get() {
392 total.set(Some(current.saturating_add(bytes)));
393 }
394 });
395
396 #[cfg(not(feature = "std"))]
397 {
398 let mut total = OBSERVED_ENCODE_BYTES.lock();
399 if let Some(current) = *total {
400 *total = Some(current.saturating_add(bytes));
401 }
402 }
403}
404
405pub struct ObservedWriter<W> {
408 inner: W,
409}
410
411impl<W> ObservedWriter<W> {
412 pub const fn new(inner: W) -> Self {
413 Self { inner }
414 }
415
416 pub fn into_inner(self) -> W {
417 self.inner
418 }
419
420 pub fn inner(&self) -> &W {
421 &self.inner
422 }
423
424 pub fn inner_mut(&mut self) -> &mut W {
425 &mut self.inner
426 }
427}
428
429impl<W: Writer> Writer for ObservedWriter<W> {
430 #[inline(always)]
431 fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
432 self.inner.write(bytes)?;
433 record_observed_encode_bytes(bytes.len());
434 Ok(())
435 }
436}
437
438pub trait WriteStream<E: Encode>: Debug + Send + Sync {
440 fn log(&mut self, obj: &E) -> CuResult<()>;
441 fn flush(&mut self) -> CuResult<()> {
442 Ok(())
443 }
444 fn last_log_bytes(&self) -> Option<usize> {
446 None
447 }
448}
449
450#[derive(dEncode, dDecode, Copy, Clone, Debug, PartialEq)]
452pub enum UnifiedLogType {
453 Empty, StructuredLogLine, CopperList, FrozenTasks, LastEntry, RuntimeLifecycle, }
460pub trait Metadata: Default + Debug + Clone + Encode + Decode<()> + Serialize {}
462
463impl Metadata for () {}
464
465#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
467#[cfg_attr(feature = "reflect", derive(Reflect))]
468pub struct CuMsgOrigin {
469 pub subsystem_code: u16,
470 pub instance_id: u32,
471 pub cl_id: u64,
472}
473
474pub trait CuMsgMetadataTrait {
476 fn process_time(&self) -> PartialCuTimeRange;
478
479 fn status_txt(&self) -> &CuCompactString;
481
482 fn origin(&self) -> Option<&CuMsgOrigin> {
484 None
485 }
486}
487
488pub trait ErasedCuStampedData {
490 fn payload(&self) -> Option<&dyn erased_serde::Serialize>;
491 #[cfg(feature = "reflect")]
492 fn payload_reflect(&self) -> Option<&dyn Reflect>;
493 fn tov(&self) -> Tov;
494 fn metadata(&self) -> &dyn CuMsgMetadataTrait;
495}
496
497pub trait ErasedCuStampedDataSet {
500 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData>;
501}
502
503pub trait CuPayloadRawBytes {
505 fn payload_raw_bytes(&self) -> Vec<Option<u64>>;
508}
509
510#[derive(Debug, Clone, Copy)]
515pub struct TaskOutputSpec {
516 pub task_id: &'static str,
517 pub msg_type: &'static str,
518 pub payload_type_path_fn: fn() -> &'static str,
519}
520
521impl TaskOutputSpec {
522 #[inline]
523 pub fn payload_type_path(&self) -> &'static str {
524 (self.payload_type_path_fn)()
525 }
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
529pub enum DebugFieldSemantics {
530 Time,
531 OptionalTime,
532 Duration,
533 GeodeticPosition,
534 Quantity {
535 quantity_name: String,
536 unit_symbol: String,
537 },
538}
539
540#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
541#[serde(rename_all = "snake_case")]
542pub enum DebugFieldKind {
543 Scalar,
544 Struct,
545 TupleStruct,
546 Tuple,
547 List,
548 Array,
549 Map,
550 Set,
551 Enum,
552}
553
554#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
555#[serde(rename_all = "snake_case")]
556pub enum DebugScalarKind {
557 Bool,
558 I8,
559 I16,
560 I32,
561 I64,
562 I128,
563 Isize,
564 U8,
565 U16,
566 U32,
567 U64,
568 U128,
569 Usize,
570 F32,
571 F64,
572 Char,
573 String,
574}
575
576impl DebugScalarKind {
577 pub const fn type_name(self) -> &'static str {
578 match self {
579 Self::Bool => "bool",
580 Self::I8 => "i8",
581 Self::I16 => "i16",
582 Self::I32 => "i32",
583 Self::I64 => "i64",
584 Self::I128 => "i128",
585 Self::Isize => "isize",
586 Self::U8 => "u8",
587 Self::U16 => "u16",
588 Self::U32 => "u32",
589 Self::U64 => "u64",
590 Self::U128 => "u128",
591 Self::Usize => "usize",
592 Self::F32 => "f32",
593 Self::F64 => "f64",
594 Self::Char => "char",
595 Self::String => "String",
596 }
597 }
598
599 pub const fn is_numeric(self) -> bool {
600 matches!(
601 self,
602 Self::I8
603 | Self::I16
604 | Self::I32
605 | Self::I64
606 | Self::I128
607 | Self::Isize
608 | Self::U8
609 | Self::U16
610 | Self::U32
611 | Self::U64
612 | Self::U128
613 | Self::Usize
614 | Self::F32
615 | Self::F64
616 )
617 }
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
621#[serde(rename_all = "snake_case")]
622pub enum DebugEnumVariantKind {
623 Unit,
624 Tuple,
625 Struct,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
629pub struct DebugEnumVariantDescriptor {
630 pub name: String,
631 pub kind: DebugEnumVariantKind,
632 #[serde(default, skip_serializing_if = "Vec::is_empty")]
633 pub fields: Vec<DebugFieldDescriptor>,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
637pub struct DebugFieldDescriptor {
638 pub display_path: String,
639 #[serde(
640 default,
641 skip_serializing_if = "Option::is_none",
642 deserialize_with = "deserialize_debug_binding_name"
643 )]
644 pub binding_name: Option<String>,
645 pub value_type_path: String,
646 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub scalar_kind: Option<DebugScalarKind>,
648 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub semantics: Option<DebugFieldSemantics>,
650 pub nullable: bool,
651 pub kind: DebugFieldKind,
652 #[serde(default, skip_serializing_if = "Vec::is_empty")]
653 pub children: Vec<DebugFieldDescriptor>,
654 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub map_key: Option<Box<DebugFieldDescriptor>>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub map_value: Option<Box<DebugFieldDescriptor>>,
658 #[serde(default, skip_serializing_if = "Vec::is_empty")]
659 pub enum_variants: Vec<DebugEnumVariantDescriptor>,
660}
661
662fn deserialize_debug_binding_name<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
663where
664 D: Deserializer<'de>,
665{
666 struct BindingNameVisitor;
667
668 impl<'de> Visitor<'de> for BindingNameVisitor {
669 type Value = Option<String>;
670
671 fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
672 formatter.write_str("a string, null, or an empty sequence")
673 }
674
675 fn visit_none<E>(self) -> Result<Self::Value, E>
676 where
677 E: de::Error,
678 {
679 Ok(None)
680 }
681
682 fn visit_unit<E>(self) -> Result<Self::Value, E>
683 where
684 E: de::Error,
685 {
686 Ok(None)
687 }
688
689 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
690 where
691 D: Deserializer<'de>,
692 {
693 deserialize_debug_binding_name(deserializer)
694 }
695
696 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
697 where
698 E: de::Error,
699 {
700 Ok(Some(value.to_owned()))
701 }
702
703 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
704 where
705 E: de::Error,
706 {
707 Ok(Some(value))
708 }
709
710 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
711 where
712 A: SeqAccess<'de>,
713 {
714 if seq.next_element::<de::IgnoredAny>()?.is_none() {
715 return Ok(None);
716 }
717 Err(de::Error::invalid_type(
718 de::Unexpected::Seq,
719 &"an empty sequence",
720 ))
721 }
722 }
723
724 deserializer.deserialize_any(BindingNameVisitor)
725}
726
727#[derive(Debug, Clone, PartialEq, Eq)]
728pub struct DebugScalarRegistration {
729 pub type_path: &'static str,
730 pub scalar_kind: DebugScalarKind,
731 pub semantics: DebugFieldSemantics,
732}
733
734pub trait DebugScalarType: 'static {
735 fn debug_scalar_registration() -> DebugScalarRegistration;
736}
737
738pub trait MatchingTasks {
739 fn get_all_task_ids() -> &'static [&'static str];
740
741 fn get_output_specs() -> &'static [TaskOutputSpec] {
742 &[]
743 }
744}
745
746pub trait PayloadSchemas {
755 fn get_payload_schemas() -> Vec<(&'static str, String)> {
760 Vec::new()
761 }
762}
763
764pub trait CopperListTuple:
766 bincode::Encode
767 + bincode::Decode<()>
768 + Debug
769 + Serialize
770 + ErasedCuStampedDataSet
771 + MatchingTasks
772 + Default
773{
774} impl<T> CopperListTuple for T where
778 T: bincode::Encode
779 + bincode::Decode<()>
780 + Debug
781 + Serialize
782 + ErasedCuStampedDataSet
783 + MatchingTasks
784 + Default
785{
786}
787
788pub const COMPACT_STRING_CAPACITY: usize = size_of::<String>();
792
793#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
794pub struct CuCompactString(pub CompactString);
795
796impl Encode for CuCompactString {
797 fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
798 let CuCompactString(compact_string) = self;
799 let bytes = &compact_string.as_bytes();
800 bytes.encode(encoder)
801 }
802}
803
804impl Debug for CuCompactString {
805 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
806 if self.0.is_empty() {
807 return write!(f, "CuCompactString(Empty)");
808 }
809 write!(f, "CuCompactString({})", self.0)
810 }
811}
812
813impl<Context> Decode<Context> for CuCompactString {
814 fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
815 let bytes = <Vec<u8> as Decode<D::Context>>::decode(decoder)?; let compact_string =
817 CompactString::from_utf8(bytes).map_err(|e| DecodeError::Utf8 { inner: e })?;
818 Ok(CuCompactString(compact_string))
819 }
820}
821
822impl<'de, Context> BorrowDecode<'de, Context> for CuCompactString {
823 fn borrow_decode<D: BorrowDecoder<'de>>(decoder: &mut D) -> Result<Self, DecodeError> {
824 CuCompactString::decode(decoder)
825 }
826}
827
828#[cfg(feature = "defmt")]
829impl defmt::Format for CuError {
830 fn format(&self, f: defmt::Formatter) {
831 match &self.cause {
832 Some(c) => {
833 let cause_str = c.to_string();
834 defmt::write!(
835 f,
836 "CuError {{ message: {}, cause: {} }}",
837 defmt::Display2Format(&self.message),
838 defmt::Display2Format(&cause_str),
839 )
840 }
841 None => defmt::write!(
842 f,
843 "CuError {{ message: {}, cause: None }}",
844 defmt::Display2Format(&self.message),
845 ),
846 }
847 }
848}
849
850#[cfg(feature = "defmt")]
851impl defmt::Format for CuCompactString {
852 fn format(&self, f: defmt::Formatter) {
853 if self.0.is_empty() {
854 defmt::write!(f, "CuCompactString(Empty)");
855 } else {
856 defmt::write!(f, "CuCompactString({})", defmt::Display2Format(&self.0));
857 }
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use crate::CuCompactString;
864 use bincode::{config, decode_from_slice, encode_to_vec};
865 use compact_str::CompactString;
866
867 #[test]
868 fn test_cucompactstr_encode_decode_empty() {
869 let cstr = CuCompactString(CompactString::from(""));
870 let config = config::standard();
871 let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
872 assert_eq!(encoded.len(), 1); let (decoded, _): (CuCompactString, usize) =
874 decode_from_slice(&encoded, config).expect("Decoding failed");
875 assert_eq!(cstr.0, decoded.0);
876 }
877
878 #[test]
879 fn test_cucompactstr_encode_decode_small() {
880 let cstr = CuCompactString(CompactString::from("test"));
881 let config = config::standard();
882 let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
883 assert_eq!(encoded.len(), 5); let (decoded, _): (CuCompactString, usize) =
885 decode_from_slice(&encoded, config).expect("Decoding failed");
886 assert_eq!(cstr.0, decoded.0);
887 }
888}
889
890#[cfg(all(test, feature = "std"))]
892mod std_tests {
893 use crate::{
894 CuError, DebugFieldDescriptor, DebugFieldKind, DebugFieldSemantics, DebugScalarKind,
895 with_cause,
896 };
897 use serde_json::json;
898
899 #[test]
900 fn test_cuerror_from_str() {
901 let err = CuError::from("test error");
902 assert_eq!(err.message(), "test error");
903 assert!(err.cause().is_none());
904 }
905
906 #[test]
907 fn test_cuerror_from_string() {
908 let err = CuError::from(String::from("test error"));
909 assert_eq!(err.message(), "test error");
910 assert!(err.cause().is_none());
911 }
912
913 #[test]
914 fn test_cuerror_new_index() {
915 let err = CuError::new(42);
916 assert_eq!(err.message(), "[interned:42]");
917 assert!(err.cause().is_none());
918 }
919
920 #[test]
921 fn test_cuerror_new_with_cause() {
922 let io_err = std::io::Error::other("io error");
923 let err = CuError::new_with_cause("wrapped error", io_err);
924 assert_eq!(err.message(), "wrapped error");
925 assert!(err.cause().is_some());
926 assert!(err.cause().unwrap().to_string().contains("io error"));
927 }
928
929 #[test]
930 fn test_cuerror_add_cause() {
931 let err = CuError::from("base error").add_cause("additional context");
932 assert_eq!(err.message(), "base error");
933 assert!(err.cause().is_some());
934 assert_eq!(err.cause().unwrap().to_string(), "additional context");
935 }
936
937 #[test]
938 fn test_cuerror_with_cause_method() {
939 let io_err = std::io::Error::other("io error");
940 let err = CuError::from("base error").with_cause(io_err);
941 assert_eq!(err.message(), "base error");
942 assert!(err.cause().is_some());
943 }
944
945 #[test]
946 fn test_cuerror_with_cause_free_function() {
947 let io_err = std::io::Error::other("io error");
948 let err = with_cause("wrapped", io_err);
949 assert_eq!(err.message(), "wrapped");
950 assert!(err.cause().is_some());
951 }
952
953 #[test]
954 fn test_cuerror_clone() {
955 let io_err = std::io::Error::other("io error");
956 let err = CuError::new_with_cause("test", io_err);
957 let cloned = err.clone();
958 assert_eq!(err.message(), cloned.message());
959 assert_eq!(
961 err.cause().map(|c| c.to_string()),
962 cloned.cause().map(|c| c.to_string())
963 );
964 }
965
966 #[test]
967 fn test_cuerror_serialize_deserialize_json() {
968 let io_err = std::io::Error::other("io error");
969 let err = CuError::new_with_cause("test", io_err);
970
971 let serialized = serde_json::to_string(&err).unwrap();
972 let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
973
974 assert_eq!(err.message(), deserialized.message());
975 assert!(deserialized.cause().is_some());
977 }
978
979 #[test]
980 fn test_cuerror_serialize_deserialize_no_cause() {
981 let err = CuError::from("simple error");
982
983 let serialized = serde_json::to_string(&err).unwrap();
984 let deserialized: CuError = serde_json::from_str(&serialized).unwrap();
985
986 assert_eq!(err.message(), deserialized.message());
987 assert!(deserialized.cause().is_none());
988 }
989
990 #[test]
991 fn test_cuerror_display() {
992 let err = CuError::from("test error").add_cause("some context");
993 let display = err.to_string();
994 assert!(display.contains("test error"));
995 assert!(display.contains("some context"));
996 }
997
998 #[test]
999 fn test_cuerror_debug() {
1000 let err = CuError::from("test error").add_cause("some context");
1001 let debug = format!("{:?}", err);
1002 assert!(debug.contains("test error"));
1003 assert!(debug.contains("some context"));
1004 }
1005
1006 #[test]
1007 fn debug_field_descriptor_skips_missing_binding_name_on_serialize() {
1008 let descriptor = DebugFieldDescriptor {
1009 display_path: "meta.process_time.start_ns".to_owned(),
1010 binding_name: None,
1011 value_type_path: "cu29_clock::CuTime".to_owned(),
1012 scalar_kind: Some(DebugScalarKind::U64),
1013 semantics: Some(DebugFieldSemantics::Time),
1014 nullable: true,
1015 kind: DebugFieldKind::Scalar,
1016 children: Vec::new(),
1017 map_key: None,
1018 map_value: None,
1019 enum_variants: Vec::new(),
1020 };
1021
1022 let encoded = serde_json::to_value(&descriptor).unwrap();
1023 assert!(encoded.get("binding_name").is_none());
1024 }
1025
1026 #[test]
1027 fn debug_field_descriptor_accepts_empty_array_binding_name() {
1028 let encoded = json!({
1029 "display_path": "meta.process_time.start_ns",
1030 "binding_name": [],
1031 "value_type_path": "cu29_clock::CuTime",
1032 "semantics": "Time",
1033 "nullable": true,
1034 "kind": "scalar",
1035 });
1036
1037 let descriptor: DebugFieldDescriptor = serde_json::from_value(encoded).unwrap();
1038 assert_eq!(descriptor.binding_name, None);
1039 assert_eq!(descriptor.semantics, Some(DebugFieldSemantics::Time));
1040 assert_eq!(descriptor.scalar_kind, None);
1041 assert_eq!(descriptor.kind, DebugFieldKind::Scalar);
1042 assert!(descriptor.children.is_empty());
1043 }
1044}