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