Skip to main content

cu29_runtime/
cutask.rs

1//! This module contains all the main definition of the traits you need to implement
2//! or interact with to create a Copper task.
3
4use crate::config::ComponentConfig;
5use crate::context::CuContext;
6use crate::reflect::{GetTypeRegistration, Reflect, TypePath, TypeRegistry};
7#[cfg(feature = "reflect")]
8use bevy_reflect;
9use bincode::de::{Decode, Decoder};
10use bincode::enc::{Encode, Encoder};
11use bincode::error::{DecodeError, EncodeError};
12use compact_str::{CompactString, ToCompactString};
13use core::any::{TypeId, type_name};
14use cu29_clock::{PartialCuTimeRange, Tov};
15use cu29_traits::{
16    COMPACT_STRING_CAPACITY, CuCompactString, CuError, CuMsgMetadataTrait, CuMsgOrigin, CuResult,
17    ErasedCuStampedData, Metadata,
18};
19use serde::de::DeserializeOwned;
20use serde::{Deserialize, Serialize};
21
22use alloc::format;
23use core::fmt::{Debug, Display, Formatter, Result as FmtResult};
24
25/// The state of a task.
26// Everything that is stateful in copper for zero copy constraints need to be restricted to this trait.
27#[cfg(feature = "reflect")]
28pub trait CuMsgPayload:
29    Default
30    + Debug
31    + Clone
32    + Encode
33    + Decode<()>
34    + Serialize
35    + DeserializeOwned
36    + Reflect
37    + TypePath
38    + Sized
39{
40}
41
42#[cfg(not(feature = "reflect"))]
43pub trait CuMsgPayload:
44    Default + Debug + Clone + Encode + Decode<()> + Serialize + DeserializeOwned + Reflect + Sized
45{
46}
47
48pub trait CuMsgPack {}
49
50// Also anything that follows this contract can be a payload (blanket implementation)
51#[cfg(feature = "reflect")]
52impl<T> CuMsgPayload for T where
53    T: Default
54        + Debug
55        + Clone
56        + Encode
57        + Decode<()>
58        + Serialize
59        + DeserializeOwned
60        + Reflect
61        + TypePath
62        + Sized
63{
64}
65
66#[cfg(not(feature = "reflect"))]
67impl<T> CuMsgPayload for T where
68    T: Default
69        + Debug
70        + Clone
71        + Encode
72        + Decode<()>
73        + Serialize
74        + DeserializeOwned
75        + Reflect
76        + Sized
77{
78}
79
80macro_rules! impl_cu_msg_pack {
81    ($($name:ident),+) => {
82        impl<'cl, $($name),+> CuMsgPack for ($(&CuMsg<$name>,)+)
83        where
84            $($name: CuMsgPayload),+
85        {}
86    };
87}
88
89macro_rules! impl_cu_msg_pack_up_to {
90    ($first:ident, $second:ident $(, $rest:ident)* $(,)?) => {
91        impl_cu_msg_pack!($first, $second);
92        impl_cu_msg_pack_up_to!(@accumulate ($first, $second); $($rest),*);
93    };
94    (@accumulate ($($acc:ident),+);) => {};
95    (@accumulate ($($acc:ident),+); $next:ident $(, $rest:ident)*) => {
96        impl_cu_msg_pack!($($acc),+, $next);
97        impl_cu_msg_pack_up_to!(@accumulate ($($acc),+, $next); $($rest),*);
98    };
99}
100
101impl<T: CuMsgPayload> CuMsgPack for CuMsg<T> {}
102impl<T: CuMsgPayload> CuMsgPack for &CuMsg<T> {}
103impl<T: CuMsgPayload> CuMsgPack for (&CuMsg<T>,) {}
104impl CuMsgPack for () {}
105
106// Apply the macro to generate implementations for tuple sizes up to 12.
107impl_cu_msg_pack_up_to!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
108
109// A convenience macro to get from a payload or a list of payloads to a proper CuMsg or CuMsgPack
110// declaration for your tasks used for input messages.
111#[macro_export]
112macro_rules! input_msg {
113    ($lt:lifetime, $first:ty, $($rest:ty),+) => {
114        ( & $lt CuMsg<$first>, $( & $lt CuMsg<$rest> ),+ )
115    };
116    ($ty:ty) => {
117        CuMsg<$ty>
118    };
119}
120
121// A convenience macro to get from a payload to a proper CuMsg used as output.
122#[macro_export]
123macro_rules! output_msg {
124    ($lt:lifetime, $first:ty, $($rest:ty),+) => {
125        ( CuMsg<$first>, $( CuMsg<$rest> ),+ )
126    };
127    ($first:ty, $($rest:ty),+) => {
128        ( CuMsg<$first>, $( CuMsg<$rest> ),+ )
129    };
130    ($ty:ty) => {
131        CuMsg<$ty>
132    };
133}
134
135/// Helper trait used by codegen when Copper needs to treat a task output as a
136/// single message slot without relying on config-declared output edges.
137pub trait CuSingleOutputMsg {
138    type Payload: CuMsgPayload;
139}
140
141impl<T: CuMsgPayload> CuSingleOutputMsg for CuMsg<T> {
142    type Payload = T;
143}
144
145/// CuMsgMetadata is a structure that contains metadata common to all CuStampedDataSet.
146#[derive(Debug, Clone, bincode::Encode, bincode::Decode, Serialize, Deserialize, Reflect)]
147#[reflect(opaque, from_reflect = false, no_field_bounds)]
148pub struct CuMsgMetadata {
149    /// The time range used for the processing of this message
150    pub process_time: PartialCuTimeRange,
151    /// A small string for real time feedback purposes.
152    /// This is useful for to display on the field when the tasks are operating correctly.
153    pub status_txt: CuCompactString,
154    /// Remote Copper provenance captured on receive, when available.
155    pub origin: Option<CuMsgOrigin>,
156}
157
158impl Metadata for CuMsgMetadata {}
159
160impl CuMsgMetadata {
161    pub fn set_status(&mut self, status: impl ToCompactString) {
162        self.status_txt = CuCompactString(status.to_compact_string());
163    }
164
165    pub fn set_origin(&mut self, origin: CuMsgOrigin) {
166        self.origin = Some(origin);
167    }
168
169    pub fn clear_origin(&mut self) {
170        self.origin = None;
171    }
172}
173
174impl CuMsgMetadataTrait for CuMsgMetadata {
175    fn process_time(&self) -> PartialCuTimeRange {
176        self.process_time
177    }
178
179    fn status_txt(&self) -> &CuCompactString {
180        &self.status_txt
181    }
182
183    fn origin(&self) -> Option<&CuMsgOrigin> {
184        self.origin.as_ref()
185    }
186}
187
188impl Display for CuMsgMetadata {
189    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
190        write!(
191            f,
192            "process_time start: {}, process_time end: {}",
193            self.process_time.start, self.process_time.end
194        )
195    }
196}
197
198/// CuMsg is the envelope holding the msg payload and the metadata between tasks.
199#[derive(Default, Debug, Clone, bincode::Decode, Serialize, Deserialize, Reflect)]
200#[reflect(opaque, from_reflect = false, no_field_bounds)]
201#[serde(bound(
202    serialize = "T: Serialize, M: Serialize",
203    deserialize = "T: DeserializeOwned, M: DeserializeOwned"
204))]
205pub struct CuStampedData<T, M>
206where
207    T: CuMsgPayload,
208    M: Metadata,
209{
210    /// This payload is the actual data exchanged between tasks.
211    payload: Option<T>,
212
213    /// The time of validity of the message.
214    /// It can be undefined (None), one measure point or a range of measures (TimeRange).
215    pub tov: Tov,
216
217    /// This metadata is the data that is common to all messages.
218    pub metadata: M,
219}
220
221impl<T, M> Encode for CuStampedData<T, M>
222where
223    T: CuMsgPayload,
224    M: Metadata,
225{
226    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
227        // NOTE: the `HandleContent` policy decision (TouchedOnly / None) is NOT made
228        // here. It can't be: this impl is generic over `T`, so method resolution at
229        // the `payload_should_log()` call site would always pick the trait blanket
230        // default (true) — autoref-specialization only works at concrete-type sites.
231        // The codegen-emitted per-slot encoder in cu29_derive consults the policy at
232        // the concrete payload type and routes to `encode_metadata_only` when the
233        // bytes should be skipped. This generic impl just writes the full payload.
234        match &self.payload {
235            None => {
236                0u8.encode(encoder)?;
237            }
238            Some(payload) => {
239                1u8.encode(encoder)?;
240                let encoded_start = cu29_traits::observed_encode_bytes();
241                let handle_start = crate::monitoring::current_payload_handle_bytes();
242                payload.encode(encoder)?;
243                let encoded_bytes =
244                    cu29_traits::observed_encode_bytes().saturating_sub(encoded_start);
245                let handle_bytes =
246                    crate::monitoring::current_payload_handle_bytes().saturating_sub(handle_start);
247                crate::monitoring::record_current_slot_payload_io_stats(
248                    core::mem::size_of::<T>(),
249                    encoded_bytes,
250                    handle_bytes,
251                );
252            }
253        }
254        self.tov.encode(encoder)?;
255        self.metadata.encode(encoder)?;
256        Ok(())
257    }
258}
259
260/// Write a metadata-only record for a stamped message: presence tag = 0u8 (no payload),
261/// followed by `tov` and `metadata`. Wire-compatible with the existing decode path —
262/// a reader sees `payload: None` for the frame, same as if the source had been
263/// disabled entirely, but the surrounding timestamp/status are preserved.
264///
265/// Codegen emits a call to this helper when a slot's producing task is configured with
266/// `HandleContent::None` or `HandleContent::TouchedOnly` and the handle wasn't touched.
267pub fn encode_metadata_only<T, M, E>(
268    msg: &CuStampedData<T, M>,
269    encoder: &mut E,
270) -> Result<(), EncodeError>
271where
272    T: CuMsgPayload,
273    M: Metadata,
274    E: Encoder,
275{
276    0u8.encode(encoder)?;
277    msg.tov.encode(encoder)?;
278    msg.metadata.encode(encoder)?;
279    Ok(())
280}
281
282impl Default for CuMsgMetadata {
283    fn default() -> Self {
284        CuMsgMetadata {
285            process_time: PartialCuTimeRange::default(),
286            status_txt: CuCompactString(CompactString::with_capacity(COMPACT_STRING_CAPACITY)),
287            origin: None,
288        }
289    }
290}
291
292impl<T, M> CuStampedData<T, M>
293where
294    T: CuMsgPayload,
295    M: Metadata,
296{
297    pub(crate) fn from_parts(payload: Option<T>, tov: Tov, metadata: M) -> Self {
298        CuStampedData {
299            payload,
300            tov,
301            metadata,
302        }
303    }
304
305    pub fn new(payload: Option<T>) -> Self {
306        Self::from_parts(payload, Tov::default(), M::default())
307    }
308    pub fn payload(&self) -> Option<&T> {
309        self.payload.as_ref()
310    }
311
312    pub fn set_payload(&mut self, payload: T) {
313        self.payload = Some(payload);
314    }
315
316    pub fn clear_payload(&mut self) {
317        self.payload = None;
318    }
319
320    pub fn payload_mut(&mut self) -> &mut Option<T> {
321        &mut self.payload
322    }
323}
324
325impl<T, M> ErasedCuStampedData for CuStampedData<T, M>
326where
327    T: CuMsgPayload,
328    M: CuMsgMetadataTrait + Metadata,
329{
330    fn payload(&self) -> Option<&dyn erased_serde::Serialize> {
331        self.payload
332            .as_ref()
333            .map(|p| p as &dyn erased_serde::Serialize)
334    }
335
336    #[cfg(feature = "reflect")]
337    fn payload_reflect(&self) -> Option<&dyn cu29_traits::Reflect> {
338        self.payload
339            .as_ref()
340            .map(|p| p as &dyn cu29_traits::Reflect)
341    }
342
343    fn tov(&self) -> Tov {
344        self.tov
345    }
346
347    fn metadata(&self) -> &dyn CuMsgMetadataTrait {
348        &self.metadata
349    }
350}
351
352/// This is the robotics message type for Copper with the correct Metadata type
353/// that will be used by the runtime.
354pub type CuMsg<T> = CuStampedData<T, CuMsgMetadata>;
355
356impl<T: CuMsgPayload> CuStampedData<T, CuMsgMetadata> {
357    /// Reinterprets the payload type carried by this message.
358    ///
359    /// # Safety
360    ///
361    /// The caller must guarantee that the message really contains a payload of type `U`. Failing
362    /// to do so is undefined behaviour.
363    pub unsafe fn assume_payload<U: CuMsgPayload>(&self) -> &CuMsg<U> {
364        // SAFETY: Caller guarantees that the underlying payload is of type U.
365        unsafe { &*(self as *const CuMsg<T> as *const CuMsg<U>) }
366    }
367
368    /// Mutable variant of [`assume_payload`](Self::assume_payload).
369    ///
370    /// # Safety
371    ///
372    /// The caller must guarantee that mutating the returned message is sound for the actual
373    /// payload type stored in the buffer.
374    pub unsafe fn assume_payload_mut<U: CuMsgPayload>(&mut self) -> &mut CuMsg<U> {
375        // SAFETY: Caller guarantees that the underlying payload is of type U.
376        unsafe { &mut *(self as *mut CuMsg<T> as *mut CuMsg<U>) }
377    }
378}
379
380impl<T: CuMsgPayload + 'static> CuStampedData<T, CuMsgMetadata> {
381    fn downcast_err<U: CuMsgPayload + 'static>() -> CuError {
382        CuError::from(format!(
383            "CuMsg payload mismatch: {} cannot be reinterpreted as {}",
384            type_name::<T>(),
385            type_name::<U>()
386        ))
387    }
388
389    /// Attempts to view this message as carrying payload `U`.
390    pub fn downcast_ref<U: CuMsgPayload + 'static>(&self) -> CuResult<&CuMsg<U>> {
391        if TypeId::of::<T>() == TypeId::of::<U>() {
392            // SAFETY: We just proved that T == U.
393            Ok(unsafe { self.assume_payload::<U>() })
394        } else {
395            Err(Self::downcast_err::<U>())
396        }
397    }
398
399    /// Mutable variant of [`downcast_ref`](Self::downcast_ref).
400    pub fn downcast_mut<U: CuMsgPayload + 'static>(&mut self) -> CuResult<&mut CuMsg<U>> {
401        if TypeId::of::<T>() == TypeId::of::<U>() {
402            // SAFETY: We just proved that T == U.
403            Ok(unsafe { self.assume_payload_mut::<U>() })
404        } else {
405            Err(Self::downcast_err::<U>())
406        }
407    }
408}
409
410/// The internal state of a task needs to be serializable
411/// so the framework can take a snapshot of the task graph.
412pub trait Freezable {
413    /// This method is called by the framework when it wants to save the task state.
414    /// The default implementation is to encode nothing (stateless).
415    /// If you have a state, you need to implement this method.
416    fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
417        Encode::encode(&(), encoder) // default is stateless
418    }
419
420    /// This method is called by the framework when it wants to restore the task to a specific state.
421    /// Here it is similar to Decode but the framework will give you a new instance of the task (the new method will be called)
422    fn thaw<D: Decoder>(&mut self, _decoder: &mut D) -> Result<(), DecodeError> {
423        Ok(())
424    }
425}
426
427/// Bincode Adapter for Freezable tasks
428/// This allows the use of the bincode API directly to freeze and thaw tasks.
429pub struct BincodeAdapter<'a, T: Freezable + ?Sized>(pub &'a T);
430
431impl<'a, T: Freezable + ?Sized> Encode for BincodeAdapter<'a, T> {
432    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
433        self.0.freeze(encoder)
434    }
435}
436
437/// A Src Task is a task that only produces messages. For example drivers for sensors are Src Tasks.
438/// They are in push mode from the runtime.
439/// To set the frequency of the pulls and align them to any hw, see the runtime configuration.
440/// Note: A source has the privilege to have a clock passed to it vs a frozen clock.
441pub trait CuSrcTask: Freezable + Reflect {
442    type Output<'m>: CuMsgPayload;
443    /// Resources required by the task.
444    type Resources<'r>;
445
446    /// Registers the reflected type used as this task's debug-state contract.
447    ///
448    /// The default exposes the task struct itself. Override this when the task
449    /// contains ignored, third-party, hardware, or otherwise non-inspectable
450    /// internals and should expose a purpose-built debug-state view instead.
451    fn register_debug_state_types(registry: &mut TypeRegistry)
452    where
453        Self: GetTypeRegistration + Sized,
454    {
455        registry.register::<Self>();
456    }
457
458    /// Returns the reflected type path used as this task's debug-state schema.
459    fn debug_state_type_path() -> &'static str
460    where
461        Self: TypePath + Sized,
462    {
463        Self::type_path()
464    }
465
466    /// Borrows this task's current debug-state view.
467    ///
468    /// Override this together with [`debug_state_type_path`](Self::debug_state_type_path)
469    /// when the debug state is a projected view rather than the task struct.
470    fn with_debug_state<R>(&self, f: impl FnOnce(&dyn Reflect) -> R) -> R
471    where
472        Self: Sized,
473    {
474        f(self)
475    }
476
477    /// Here you need to initialize everything your task will need for the duration of its lifetime.
478    /// The config allows you to access the configuration of the task.
479    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
480    where
481        Self: Sized;
482
483    /// Start is called between the creation of the task and the first call to pre/process.
484    fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
485        Ok(())
486    }
487
488    /// This is a method called by the runtime before "process". This is a kind of best effort,
489    /// as soon as possible call to give a chance for the task to do some work before to prepare
490    /// to make "process" as short as possible.
491    fn preprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
492        Ok(())
493    }
494
495    /// Process is the most critical execution of the task.
496    /// The goal will be to produce the output message as soon as possible.
497    /// Use preprocess to prepare the task to make this method as short as possible.
498    fn process<'o>(&mut self, ctx: &CuContext, new_msg: &mut Self::Output<'o>) -> CuResult<()>;
499
500    /// This is a method called by the runtime after "process". It is best effort a chance for
501    /// the task to update some state after process is out of the way.
502    /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
503    fn postprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
504        Ok(())
505    }
506
507    /// Called to stop the task. It signals that the *process method won't be called until start is called again.
508    fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
509        Ok(())
510    }
511}
512
513/// This is the most generic Task of copper. It is a "transform" task deriving an output from an input.
514pub trait CuTask: Freezable + Reflect {
515    type Input<'m>: CuMsgPack;
516    type Output<'m>: CuMsgPayload;
517    /// Resources required by the task.
518    type Resources<'r>;
519
520    /// Registers the reflected type used as this task's debug-state contract.
521    ///
522    /// The default exposes the task struct itself. Override this when the task
523    /// contains ignored, third-party, hardware, or otherwise non-inspectable
524    /// internals and should expose a purpose-built debug-state view instead.
525    fn register_debug_state_types(registry: &mut TypeRegistry)
526    where
527        Self: GetTypeRegistration + Sized,
528    {
529        registry.register::<Self>();
530    }
531
532    /// Returns the reflected type path used as this task's debug-state schema.
533    fn debug_state_type_path() -> &'static str
534    where
535        Self: TypePath + Sized,
536    {
537        Self::type_path()
538    }
539
540    /// Borrows this task's current debug-state view.
541    ///
542    /// Override this together with [`debug_state_type_path`](Self::debug_state_type_path)
543    /// when the debug state is a projected view rather than the task struct.
544    fn with_debug_state<R>(&self, f: impl FnOnce(&dyn Reflect) -> R) -> R
545    where
546        Self: Sized,
547    {
548        f(self)
549    }
550
551    /// Here you need to initialize everything your task will need for the duration of its lifetime.
552    /// The config allows you to access the configuration of the task.
553    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
554    where
555        Self: Sized;
556
557    /// Start is called between the creation of the task and the first call to pre/process.
558    fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
559        Ok(())
560    }
561
562    /// This is a method called by the runtime before "process". This is a kind of best effort,
563    /// as soon as possible call to give a chance for the task to do some work before to prepare
564    /// to make "process" as short as possible.
565    fn preprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
566        Ok(())
567    }
568
569    /// Process is the most critical execution of the task.
570    /// The goal will be to produce the output message as soon as possible.
571    /// Use preprocess to prepare the task to make this method as short as possible.
572    fn process<'i, 'o>(
573        &mut self,
574        _ctx: &CuContext,
575        input: &Self::Input<'i>,
576        output: &mut Self::Output<'o>,
577    ) -> CuResult<()>;
578
579    /// This is a method called by the runtime after "process". It is best effort a chance for
580    /// the task to update some state after process is out of the way.
581    /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
582    fn postprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
583        Ok(())
584    }
585
586    /// Called to stop the task. It signals that the *process method won't be called until start is called again.
587    fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
588        Ok(())
589    }
590}
591
592/// A Sink Task is a task that only consumes messages. For example drivers for actuators are Sink Tasks.
593pub trait CuSinkTask: Freezable + Reflect {
594    type Input<'m>: CuMsgPack;
595    /// Resources required by the task.
596    type Resources<'r>;
597
598    /// Registers the reflected type used as this task's debug-state contract.
599    ///
600    /// The default exposes the task struct itself. Override this when the task
601    /// contains ignored, third-party, hardware, or otherwise non-inspectable
602    /// internals and should expose a purpose-built debug-state view instead.
603    fn register_debug_state_types(registry: &mut TypeRegistry)
604    where
605        Self: GetTypeRegistration + Sized,
606    {
607        registry.register::<Self>();
608    }
609
610    /// Returns the reflected type path used as this task's debug-state schema.
611    fn debug_state_type_path() -> &'static str
612    where
613        Self: TypePath + Sized,
614    {
615        Self::type_path()
616    }
617
618    /// Borrows this task's current debug-state view.
619    ///
620    /// Override this together with [`debug_state_type_path`](Self::debug_state_type_path)
621    /// when the debug state is a projected view rather than the task struct.
622    fn with_debug_state<R>(&self, f: impl FnOnce(&dyn Reflect) -> R) -> R
623    where
624        Self: Sized,
625    {
626        f(self)
627    }
628
629    /// Here you need to initialize everything your task will need for the duration of its lifetime.
630    /// The config allows you to access the configuration of the task.
631    fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
632    where
633        Self: Sized;
634
635    /// Start is called between the creation of the task and the first call to pre/process.
636    fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
637        Ok(())
638    }
639
640    /// This is a method called by the runtime before "process". This is a kind of best effort,
641    /// as soon as possible call to give a chance for the task to do some work before to prepare
642    /// to make "process" as short as possible.
643    fn preprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
644        Ok(())
645    }
646
647    /// Process is the most critical execution of the task.
648    /// The goal will be to produce the output message as soon as possible.
649    /// Use preprocess to prepare the task to make this method as short as possible.
650    fn process<'i>(&mut self, _ctx: &CuContext, input: &Self::Input<'i>) -> CuResult<()>;
651
652    /// This is a method called by the runtime after "process". It is best effort a chance for
653    /// the task to update some state after process is out of the way.
654    /// It can be use for example to maintain statistics etc. that are not time-critical for the robot.
655    fn postprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
656        Ok(())
657    }
658
659    /// Called to stop the task. It signals that the *process method won't be called until start is called again.
660    fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
661        Ok(())
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668    use bincode::{config, decode_from_slice, encode_to_vec};
669
670    #[test]
671    fn test_cucompactstr_encode_decode() {
672        let cstr = CuCompactString(CompactString::from("hello"));
673        let config = config::standard();
674        let encoded = encode_to_vec(&cstr, config).expect("Encoding failed");
675        let (decoded, _): (CuCompactString, usize) =
676            decode_from_slice(&encoded, config).expect("Decoding failed");
677        assert_eq!(cstr.0, decoded.0);
678    }
679
680    /// Test wrapper proving that a composite payload can forward `payload_should_log`
681    /// to an inner [`CuHandle`] via an inherent method — exactly the pattern real
682    /// composite payloads like `CuImage` will use.
683    ///
684    /// Gated on the default (non-bevy_reflect) feature configuration where
685    /// `Reflect` is auto-impl'd for any `'static`, so the wrapper satisfies
686    /// `CuMsgPayload` without needing `#[derive(Reflect)]`.
687    #[cfg(not(feature = "reflect"))]
688    #[derive(Debug, Clone, bincode::Encode, bincode::Decode, Serialize, Deserialize)]
689    struct TestHandlePayload {
690        handle: crate::pool::CuHandle<Vec<u8>>,
691    }
692
693    #[cfg(not(feature = "reflect"))]
694    impl Default for TestHandlePayload {
695        fn default() -> Self {
696            Self {
697                handle: crate::pool::CuHandle::new_detached(Vec::new()),
698            }
699        }
700    }
701
702    #[cfg(not(feature = "reflect"))]
703    impl TestHandlePayload {
704        // Inherent specialization arm: real composite payloads (e.g. CuImage) provide
705        // an identical method that forwards to their inner CuHandle.
706        fn payload_should_log(&self) -> bool {
707            self.handle.payload_should_log()
708        }
709    }
710
711    /// Encoding a CuMsg whose payload wraps a CuHandle in `TouchedOnly` mode must:
712    /// * skip the payload bytes when no consumer marked the handle touched, and
713    /// * include them once `mark_touched` was called.
714    /// The wire shape stays compatible with the existing `Option<T>` decode path
715    /// (presence tag is 0u8 for skip, 1u8 + payload otherwise).
716    #[cfg(not(feature = "reflect"))]
717    #[test]
718    fn test_encode_skips_payload_for_untouched_handle() {
719        use crate::pool::{CuHandle, HandleContent};
720        let cfg = config::standard();
721
722        let untouched = TestHandlePayload {
723            handle: CuHandle::new_detached_with_mode(
724                vec![0xAA, 0xBB, 0xCC, 0xDD],
725                HandleContent::TouchedOnly,
726            ),
727        };
728        let msg_skip: CuMsg<TestHandlePayload> = CuMsg::new(Some(untouched));
729        let skip_bytes = encode_to_vec(&msg_skip, cfg).expect("encode");
730
731        let touched_payload = TestHandlePayload {
732            handle: CuHandle::new_detached_with_mode(
733                vec![0xAA, 0xBB, 0xCC, 0xDD],
734                HandleContent::TouchedOnly,
735            ),
736        };
737        touched_payload.handle.mark_touched();
738        let msg_keep: CuMsg<TestHandlePayload> = CuMsg::new(Some(touched_payload));
739        let keep_bytes = encode_to_vec(&msg_keep, cfg).expect("encode");
740
741        assert_eq!(
742            skip_bytes[0], 0u8,
743            "first byte must be the no-payload presence tag for an untouched TouchedOnly handle"
744        );
745        assert_eq!(
746            keep_bytes[0], 1u8,
747            "first byte must be the payload-present tag once the handle was touched"
748        );
749        assert!(
750            keep_bytes.len() > skip_bytes.len(),
751            "touched encoding ({} bytes) must include payload content; skip is {} bytes",
752            keep_bytes.len(),
753            skip_bytes.len()
754        );
755    }
756
757    /// `HandleContent::All` (the default for every existing source) must never drop
758    /// payload bytes — regardless of whether the handle was touched.
759    #[cfg(not(feature = "reflect"))]
760    #[test]
761    fn test_encode_keeps_payload_for_default_mode() {
762        use crate::pool::{CuHandle, HandleContent};
763        let cfg = config::standard();
764
765        let payload = TestHandlePayload {
766            handle: CuHandle::new_detached_with_mode(vec![1, 2, 3], HandleContent::All),
767        };
768        let msg: CuMsg<TestHandlePayload> = CuMsg::new(Some(payload));
769        let bytes = encode_to_vec(&msg, cfg).expect("encode");
770        assert_eq!(
771            bytes[0], 1u8,
772            "default (HandleContent::All) must keep emitting the payload"
773        );
774    }
775}