1use core::ffi::c_void;
10use nros_core::lifecycle::{
11 LifecycleState, LifecycleTransition, TransitionResult, apply_transition, can_transition,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum LifecycleError {
17 InvalidTransition {
19 from: LifecycleState,
21 transition: LifecycleTransition,
23 },
24 CallbackFailed {
26 transition: LifecycleTransition,
28 result: TransitionResult,
30 },
31 NodeFinalized,
33}
34
35pub type LifecycleCallbackFn = fn() -> TransitionResult;
41
42pub struct LifecyclePollingNode {
60 state: LifecycleState,
61 on_configure: Option<LifecycleCallbackFn>,
62 on_activate: Option<LifecycleCallbackFn>,
63 on_deactivate: Option<LifecycleCallbackFn>,
64 on_cleanup: Option<LifecycleCallbackFn>,
65 on_shutdown: Option<LifecycleCallbackFn>,
66 on_error: Option<LifecycleCallbackFn>,
67}
68
69impl LifecyclePollingNode {
70 pub const fn new() -> Self {
74 Self {
75 state: LifecycleState::Unconfigured,
76 on_configure: None,
77 on_activate: None,
78 on_deactivate: None,
79 on_cleanup: None,
80 on_shutdown: None,
81 on_error: None,
82 }
83 }
84
85 pub const fn state(&self) -> LifecycleState {
87 self.state
88 }
89
90 pub fn trigger_transition(
92 &mut self,
93 transition: LifecycleTransition,
94 ) -> Result<LifecycleState, LifecycleError> {
95 if self.state.is_terminal() {
96 return Err(LifecycleError::NodeFinalized);
97 }
98
99 if !can_transition(self.state, transition) {
100 return Err(LifecycleError::InvalidTransition {
101 from: self.state,
102 transition,
103 });
104 }
105
106 let result = self.invoke_callback(transition);
107 self.state = apply_transition(self.state, transition, result);
108
109 if result == TransitionResult::Success {
110 Ok(self.state)
111 } else {
112 Err(LifecycleError::CallbackFailed { transition, result })
113 }
114 }
115
116 pub fn configure(&mut self) -> Result<LifecycleState, LifecycleError> {
118 self.trigger_transition(LifecycleTransition::Configure)
119 }
120
121 pub fn activate(&mut self) -> Result<LifecycleState, LifecycleError> {
123 self.trigger_transition(LifecycleTransition::Activate)
124 }
125
126 pub fn deactivate(&mut self) -> Result<LifecycleState, LifecycleError> {
128 self.trigger_transition(LifecycleTransition::Deactivate)
129 }
130
131 pub fn cleanup(&mut self) -> Result<LifecycleState, LifecycleError> {
133 self.trigger_transition(LifecycleTransition::Cleanup)
134 }
135
136 pub fn shutdown(&mut self) -> Result<LifecycleState, LifecycleError> {
138 let transition = match self.state {
139 LifecycleState::Unconfigured => LifecycleTransition::ShutdownUnconfigured,
140 LifecycleState::Inactive => LifecycleTransition::ShutdownInactive,
141 LifecycleState::Active => LifecycleTransition::ShutdownActive,
142 LifecycleState::Finalized => return Err(LifecycleError::NodeFinalized),
143 LifecycleState::ErrorProcessing => {
144 return Err(LifecycleError::InvalidTransition {
145 from: self.state,
146 transition: LifecycleTransition::ShutdownUnconfigured,
147 });
148 }
149 };
150 self.trigger_transition(transition)
151 }
152
153 pub fn bring_up(&mut self) -> Result<LifecycleState, LifecycleError> {
155 self.configure()?;
156 self.activate()
157 }
158
159 pub fn register_on_configure(&mut self, cb: LifecycleCallbackFn) {
161 self.on_configure = Some(cb);
162 }
163
164 pub fn register_on_activate(&mut self, cb: LifecycleCallbackFn) {
166 self.on_activate = Some(cb);
167 }
168
169 pub fn register_on_deactivate(&mut self, cb: LifecycleCallbackFn) {
171 self.on_deactivate = Some(cb);
172 }
173
174 pub fn register_on_cleanup(&mut self, cb: LifecycleCallbackFn) {
176 self.on_cleanup = Some(cb);
177 }
178
179 pub fn register_on_shutdown(&mut self, cb: LifecycleCallbackFn) {
181 self.on_shutdown = Some(cb);
182 }
183
184 pub fn register_on_error(&mut self, cb: LifecycleCallbackFn) {
186 self.on_error = Some(cb);
187 }
188
189 fn invoke_callback(&mut self, transition: LifecycleTransition) -> TransitionResult {
190 let cb = match transition {
191 LifecycleTransition::Configure => self.on_configure,
192 LifecycleTransition::Activate => self.on_activate,
193 LifecycleTransition::Deactivate => self.on_deactivate,
194 LifecycleTransition::Cleanup => self.on_cleanup,
195 LifecycleTransition::ShutdownUnconfigured
196 | LifecycleTransition::ShutdownInactive
197 | LifecycleTransition::ShutdownActive => self.on_shutdown,
198 LifecycleTransition::ErrorRecovery => self.on_error,
199 };
200
201 match cb {
202 Some(f) => f(),
203 None => TransitionResult::Success,
204 }
205 }
206}
207
208impl Default for LifecyclePollingNode {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214pub type LifecycleCallbackFnCtx = unsafe extern "C" fn(ctx: *mut c_void) -> u8;
225
226#[cfg(feature = "lifecycle-services")]
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub(crate) struct TransitionAnnouncement {
244 pub timestamp_ns: u64,
250 pub transition: LifecycleTransition,
252 pub start_state: LifecycleState,
254 pub goal_state: LifecycleState,
256}
257
258#[cfg(feature = "lifecycle-services")]
271pub(crate) const TRANSITION_ANNOUNCEMENT_QUEUE: usize = 4;
272
273pub struct LifecyclePollingNodeCtx {
281 state: LifecycleState,
282 on_configure: Option<LifecycleCallbackFnCtx>,
283 on_activate: Option<LifecycleCallbackFnCtx>,
284 on_deactivate: Option<LifecycleCallbackFnCtx>,
285 on_cleanup: Option<LifecycleCallbackFnCtx>,
286 on_shutdown: Option<LifecycleCallbackFnCtx>,
287 on_error: Option<LifecycleCallbackFnCtx>,
288 context: *mut c_void,
289 #[cfg(feature = "lifecycle-services")]
297 announcements: heapless::Deque<TransitionAnnouncement, TRANSITION_ANNOUNCEMENT_QUEUE>,
298 #[cfg(feature = "lifecycle-services")]
301 announcements_dropped: u32,
302}
303
304impl LifecyclePollingNodeCtx {
308 pub const fn new() -> Self {
310 Self {
311 state: LifecycleState::Unconfigured,
312 on_configure: None,
313 on_activate: None,
314 on_deactivate: None,
315 on_cleanup: None,
316 on_shutdown: None,
317 on_error: None,
318 context: core::ptr::null_mut(),
319 #[cfg(feature = "lifecycle-services")]
320 announcements: heapless::Deque::new(),
321 #[cfg(feature = "lifecycle-services")]
322 announcements_dropped: 0,
323 }
324 }
325
326 pub const fn state(&self) -> LifecycleState {
328 self.state
329 }
330
331 #[cfg(all(feature = "lifecycle-services", any(has_rmw, test)))]
337 pub(crate) fn take_announcement(&mut self) -> Option<TransitionAnnouncement> {
338 self.announcements.pop_front()
339 }
340
341 #[cfg(all(feature = "lifecycle-services", any(has_rmw, test)))]
347 pub(crate) fn take_dropped_announcements(&mut self) -> u32 {
348 core::mem::replace(&mut self.announcements_dropped, 0)
349 }
350
351 #[cfg(feature = "lifecycle-services")]
353 fn record_announcement(
354 &mut self,
355 transition: LifecycleTransition,
356 start_state: LifecycleState,
357 goal_state: LifecycleState,
358 ) {
359 let record = TransitionAnnouncement {
360 timestamp_ns: nros_core::clock::Clock::steady().now().to_nanos().max(0) as u64,
361 transition,
362 start_state,
363 goal_state,
364 };
365 if self.announcements.push_back(record).is_err() {
366 self.announcements_dropped = self.announcements_dropped.saturating_add(1);
367 }
368 }
369
370 pub fn set_context(&mut self, ctx: *mut c_void) {
372 self.context = ctx;
373 }
374
375 pub fn context(&self) -> *mut c_void {
377 self.context
378 }
379
380 pub fn register(&mut self, slot: LifecycleCallbackSlot, cb: Option<LifecycleCallbackFnCtx>) {
382 match slot {
383 LifecycleCallbackSlot::Configure => self.on_configure = cb,
384 LifecycleCallbackSlot::Activate => self.on_activate = cb,
385 LifecycleCallbackSlot::Deactivate => self.on_deactivate = cb,
386 LifecycleCallbackSlot::Cleanup => self.on_cleanup = cb,
387 LifecycleCallbackSlot::Shutdown => self.on_shutdown = cb,
388 LifecycleCallbackSlot::Error => self.on_error = cb,
389 }
390 }
391
392 pub fn clear_callbacks(&mut self) {
394 self.on_configure = None;
395 self.on_activate = None;
396 self.on_deactivate = None;
397 self.on_cleanup = None;
398 self.on_shutdown = None;
399 self.on_error = None;
400 self.context = core::ptr::null_mut();
401 }
402
403 pub fn finalize(&mut self) {
405 self.state = LifecycleState::Finalized;
406 }
407
408 pub unsafe fn trigger_transition(
415 &mut self,
416 transition: LifecycleTransition,
417 ) -> Result<LifecycleState, LifecycleError> {
418 if self.state.is_terminal() {
419 return Err(LifecycleError::NodeFinalized);
420 }
421
422 if !can_transition(self.state, transition) {
423 return Err(LifecycleError::InvalidTransition {
424 from: self.state,
425 transition,
426 });
427 }
428
429 let cb = match transition {
430 LifecycleTransition::Configure => self.on_configure,
431 LifecycleTransition::Activate => self.on_activate,
432 LifecycleTransition::Deactivate => self.on_deactivate,
433 LifecycleTransition::Cleanup => self.on_cleanup,
434 LifecycleTransition::ShutdownUnconfigured
435 | LifecycleTransition::ShutdownInactive
436 | LifecycleTransition::ShutdownActive => self.on_shutdown,
437 LifecycleTransition::ErrorRecovery => self.on_error,
438 };
439
440 let result = match cb {
441 Some(f) => {
442 let raw = unsafe { f(self.context) };
443 TransitionResult::from_u8(raw).unwrap_or(TransitionResult::Error)
444 }
445 None => TransitionResult::Success,
446 };
447
448 let start_state = self.state;
449 self.state = apply_transition(self.state, transition, result);
450
451 #[cfg(feature = "lifecycle-services")]
460 self.record_announcement(transition, start_state, self.state);
461 #[cfg(not(feature = "lifecycle-services"))]
464 let _ = start_state;
465
466 if result == TransitionResult::Success {
467 Ok(self.state)
468 } else {
469 Err(LifecycleError::CallbackFailed { transition, result })
470 }
471 }
472}
473
474impl Default for LifecyclePollingNodeCtx {
475 fn default() -> Self {
476 Self::new()
477 }
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub enum LifecycleCallbackSlot {
483 Configure,
485 Activate,
487 Deactivate,
489 Cleanup,
491 Shutdown,
493 Error,
495}
496
497pub trait LifecycleCallbacks {
528 fn on_configure(&mut self) -> TransitionResult {
530 TransitionResult::Success
531 }
532 fn on_activate(&mut self) -> TransitionResult {
534 TransitionResult::Success
535 }
536 fn on_deactivate(&mut self) -> TransitionResult {
538 TransitionResult::Success
539 }
540 fn on_cleanup(&mut self) -> TransitionResult {
542 TransitionResult::Success
543 }
544 fn on_shutdown(&mut self) -> TransitionResult {
546 TransitionResult::Success
547 }
548 fn on_error(&mut self) -> TransitionResult {
550 TransitionResult::Failure
551 }
552}
553
554pub mod trampolines {
560 use super::{LifecycleCallbacks, TransitionResult};
561 use core::ffi::c_void;
562
563 macro_rules! trampoline {
564 ($name:ident, $method:ident) => {
565 pub unsafe extern "C" fn $name<T: LifecycleCallbacks>(ctx: *mut c_void) -> u8 {
570 if ctx.is_null() {
571 return TransitionResult::Error as u8;
572 }
573 let node = unsafe { &mut *(ctx as *mut T) };
577 node.$method() as u8
578 }
579 };
580 }
581
582 trampoline!(on_configure, on_configure);
583 trampoline!(on_activate, on_activate);
584 trampoline!(on_deactivate, on_deactivate);
585 trampoline!(on_cleanup, on_cleanup);
586 trampoline!(on_shutdown, on_shutdown);
587 trampoline!(on_error, on_error);
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593
594 #[cfg(feature = "lifecycle-services")]
599 #[test]
600 fn every_executed_transition_is_recorded_once() {
601 let mut sm = LifecyclePollingNodeCtx::new();
602 unsafe { sm.trigger_transition(LifecycleTransition::Configure) }.expect("configure");
604
605 let ev = sm.take_announcement().expect("configure was recorded");
606 assert_eq!(ev.transition, LifecycleTransition::Configure);
607 assert_eq!(ev.start_state, LifecycleState::Unconfigured);
608 assert_eq!(ev.goal_state, LifecycleState::Inactive);
609 assert!(
610 sm.take_announcement().is_none(),
611 "one transition, one record"
612 );
613 assert_eq!(sm.take_dropped_announcements(), 0);
614 }
615
616 #[cfg(feature = "lifecycle-services")]
617 #[test]
618 fn a_rejected_transition_records_nothing() {
619 let mut sm = LifecyclePollingNodeCtx::new();
620 assert!(unsafe { sm.trigger_transition(LifecycleTransition::Activate) }.is_err());
624 assert!(sm.take_announcement().is_none());
625 assert_eq!(sm.take_dropped_announcements(), 0);
626 }
627
628 #[cfg(feature = "lifecycle-services")]
629 #[test]
630 fn an_undrained_queue_counts_its_losses_instead_of_hiding_them() {
631 let mut sm = LifecyclePollingNodeCtx::new();
632 let runs = TRANSITION_ANNOUNCEMENT_QUEUE + 3;
636 for i in 0..runs {
637 let t = if i % 2 == 0 {
638 LifecycleTransition::Configure
639 } else {
640 LifecycleTransition::Cleanup
641 };
642 unsafe { sm.trigger_transition(t) }.expect("legal transition");
644 }
645 let mut drained = 0;
646 while sm.take_announcement().is_some() {
647 drained += 1;
648 }
649 assert_eq!(drained, TRANSITION_ANNOUNCEMENT_QUEUE);
650 assert_eq!(
651 sm.take_dropped_announcements() as usize,
652 runs - TRANSITION_ANNOUNCEMENT_QUEUE,
653 "the overflow is COUNTED — a bounded table that discards silently is \
654 the defect the zpico reply slots already answered"
655 );
656 assert_eq!(
657 sm.take_dropped_announcements(),
658 0,
659 "read-and-clear: a burst is reported once, not every spin thereafter"
660 );
661 }
662
663 struct DemoNode {
668 configured: bool,
669 }
670 impl LifecycleCallbacks for DemoNode {
671 fn on_configure(&mut self) -> TransitionResult {
672 self.configured = true;
673 TransitionResult::Success
674 }
675 fn on_error(&mut self) -> TransitionResult {
676 TransitionResult::Error
677 }
678 }
680
681 #[test]
682 fn trampoline_dispatches_to_trait_impl_and_defaults() {
683 let mut n = DemoNode { configured: false };
684 let ctx = &mut n as *mut DemoNode as *mut c_void;
685
686 let rc = unsafe { trampolines::on_configure::<DemoNode>(ctx) };
688 assert_eq!(rc, TransitionResult::Success as u8);
689 assert!(n.configured);
690
691 let rc = unsafe { trampolines::on_activate::<DemoNode>(&mut n as *mut _ as *mut c_void) };
693 assert_eq!(rc, TransitionResult::Success as u8);
694
695 let rc = unsafe { trampolines::on_error::<DemoNode>(&mut n as *mut _ as *mut c_void) };
697 assert_eq!(rc, TransitionResult::Error as u8);
698
699 let rc = unsafe { trampolines::on_configure::<DemoNode>(core::ptr::null_mut()) };
701 assert_eq!(rc, TransitionResult::Error as u8);
702 }
703
704 #[test]
705 fn ctx_state_machine_drives_trait_through_a_transition() {
706 let mut n = DemoNode { configured: false };
709 let mut sm = LifecyclePollingNodeCtx::new();
710 sm.set_context(&mut n as *mut DemoNode as *mut c_void);
711 sm.register(
712 LifecycleCallbackSlot::Configure,
713 Some(trampolines::on_configure::<DemoNode>),
714 );
715
716 let new_state =
718 unsafe { sm.trigger_transition(LifecycleTransition::Configure) }.expect("configure");
719
720 assert!(n.configured, "the trait's on_configure ran");
721 assert_eq!(new_state, LifecycleState::Inactive);
722 }
723
724 #[test]
725 fn raw_extern_c_callbacks_drive_the_ctx_state_machine() {
726 unsafe extern "C" fn cb_success(_ctx: *mut c_void) -> u8 {
731 TransitionResult::Success as u8
732 }
733 let mut sm = LifecyclePollingNodeCtx::new();
734 sm.register(LifecycleCallbackSlot::Configure, Some(cb_success));
735 sm.register(LifecycleCallbackSlot::Activate, Some(cb_success));
736
737 let s =
739 unsafe { sm.trigger_transition(LifecycleTransition::Configure) }.expect("configure");
740 assert_eq!(s, LifecycleState::Inactive);
741 let s = unsafe { sm.trigger_transition(LifecycleTransition::Activate) }.expect("activate");
742 assert_eq!(s, LifecycleState::Active);
743 }
744
745 #[test]
750 fn test_polling_node_initial_state() {
751 let node = LifecyclePollingNode::new();
752 assert_eq!(node.state(), LifecycleState::Unconfigured);
753 }
754
755 #[test]
756 fn test_polling_node_default() {
757 let node = LifecyclePollingNode::default();
758 assert_eq!(node.state(), LifecycleState::Unconfigured);
759 }
760
761 #[test]
762 fn test_polling_node_happy_path() {
763 let mut node = LifecyclePollingNode::new();
764
765 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
766 assert_eq!(node.activate().unwrap(), LifecycleState::Active);
767 assert_eq!(node.deactivate().unwrap(), LifecycleState::Inactive);
768 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
769 }
770
771 #[test]
772 fn test_polling_node_cleanup_cycle() {
773 let mut node = LifecyclePollingNode::new();
774
775 node.configure().unwrap();
776 assert_eq!(node.cleanup().unwrap(), LifecycleState::Unconfigured);
777
778 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
780 }
781
782 #[test]
783 fn test_polling_node_invalid_transition() {
784 let mut node = LifecyclePollingNode::new();
785
786 let err = node.activate().unwrap_err();
787 assert_eq!(
788 err,
789 LifecycleError::InvalidTransition {
790 from: LifecycleState::Unconfigured,
791 transition: LifecycleTransition::Activate,
792 }
793 );
794 }
795
796 #[test]
797 fn test_polling_node_finalized_rejection() {
798 let mut node = LifecyclePollingNode::new();
799 node.shutdown().unwrap();
800
801 assert_eq!(node.configure().unwrap_err(), LifecycleError::NodeFinalized);
802 assert_eq!(node.shutdown().unwrap_err(), LifecycleError::NodeFinalized);
803 }
804
805 fn on_configure_success() -> TransitionResult {
806 TransitionResult::Success
807 }
808
809 fn on_configure_failure() -> TransitionResult {
810 TransitionResult::Failure
811 }
812
813 fn on_configure_error() -> TransitionResult {
814 TransitionResult::Error
815 }
816
817 #[test]
818 fn test_polling_node_callback_success() {
819 let mut node = LifecyclePollingNode::new();
820 node.register_on_configure(on_configure_success);
821
822 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
823 }
824
825 #[test]
826 fn test_polling_node_callback_failure_rollback() {
827 let mut node = LifecyclePollingNode::new();
828 node.register_on_configure(on_configure_failure);
829
830 let err = node.configure().unwrap_err();
831 assert_eq!(
832 err,
833 LifecycleError::CallbackFailed {
834 transition: LifecycleTransition::Configure,
835 result: TransitionResult::Failure,
836 }
837 );
838 assert_eq!(node.state(), LifecycleState::Unconfigured);
840 }
841
842 #[test]
843 fn test_polling_node_callback_error() {
844 let mut node = LifecyclePollingNode::new();
845 node.register_on_configure(on_configure_error);
846
847 let err = node.configure().unwrap_err();
848 assert_eq!(
849 err,
850 LifecycleError::CallbackFailed {
851 transition: LifecycleTransition::Configure,
852 result: TransitionResult::Error,
853 }
854 );
855 assert_eq!(node.state(), LifecycleState::ErrorProcessing);
857 }
858
859 #[test]
860 fn test_polling_node_error_recovery() {
861 let mut node = LifecyclePollingNode::new();
862 node.register_on_configure(on_configure_error);
863
864 let _ = node.configure();
865 assert_eq!(node.state(), LifecycleState::ErrorProcessing);
866
867 assert!(node.shutdown().is_err());
869
870 node.register_on_error(on_configure_success);
872 let result = node.trigger_transition(LifecycleTransition::ErrorRecovery);
873 assert_eq!(result.unwrap(), LifecycleState::Unconfigured);
874 }
875
876 #[test]
877 fn test_polling_node_bring_up() {
878 let mut node = LifecyclePollingNode::new();
879 assert_eq!(node.bring_up().unwrap(), LifecycleState::Active);
880 }
881
882 #[test]
883 fn test_polling_node_bring_up_stops_on_configure_failure() {
884 let mut node = LifecyclePollingNode::new();
885 node.register_on_configure(on_configure_failure);
886
887 let err = node.bring_up().unwrap_err();
888 assert_eq!(
889 err,
890 LifecycleError::CallbackFailed {
891 transition: LifecycleTransition::Configure,
892 result: TransitionResult::Failure,
893 }
894 );
895 assert_eq!(node.state(), LifecycleState::Unconfigured);
897 }
898
899 #[test]
900 fn test_polling_node_shutdown_from_each_state() {
901 let mut node = LifecyclePollingNode::new();
903 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
904
905 let mut node = LifecyclePollingNode::new();
907 node.configure().unwrap();
908 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
909
910 let mut node = LifecyclePollingNode::new();
912 node.bring_up().unwrap();
913 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
914 }
915
916 #[test]
917 fn test_polling_node_no_callback_defaults_success() {
918 let mut node = LifecyclePollingNode::new();
920 assert_eq!(node.configure().unwrap(), LifecycleState::Inactive);
921 assert_eq!(node.activate().unwrap(), LifecycleState::Active);
922 assert_eq!(node.deactivate().unwrap(), LifecycleState::Inactive);
923 assert_eq!(node.cleanup().unwrap(), LifecycleState::Unconfigured);
924 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
925 }
926
927 fn on_shutdown_success() -> TransitionResult {
928 TransitionResult::Success
929 }
930
931 #[test]
932 fn test_polling_node_shutdown_callback_invoked() {
933 let mut node = LifecyclePollingNode::new();
934 node.register_on_shutdown(on_shutdown_success);
935
936 assert_eq!(node.shutdown().unwrap(), LifecycleState::Finalized);
938 }
939
940 unsafe extern "C" fn ctx_cb_success(_: *mut c_void) -> u8 {
945 TransitionResult::Success as u8
946 }
947 unsafe extern "C" fn ctx_cb_failure(_: *mut c_void) -> u8 {
948 TransitionResult::Failure as u8
949 }
950 unsafe extern "C" fn ctx_cb_error(_: *mut c_void) -> u8 {
951 TransitionResult::Error as u8
952 }
953
954 #[test]
955 fn test_ctx_node_happy_path() {
956 unsafe {
957 let mut node = LifecyclePollingNodeCtx::new();
958 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_success));
959 node.register(LifecycleCallbackSlot::Activate, Some(ctx_cb_success));
960 node.register(LifecycleCallbackSlot::Deactivate, Some(ctx_cb_success));
961 node.register(LifecycleCallbackSlot::Shutdown, Some(ctx_cb_success));
962
963 assert_eq!(
964 node.trigger_transition(LifecycleTransition::Configure)
965 .unwrap(),
966 LifecycleState::Inactive
967 );
968 assert_eq!(
969 node.trigger_transition(LifecycleTransition::Activate)
970 .unwrap(),
971 LifecycleState::Active
972 );
973 assert_eq!(
974 node.trigger_transition(LifecycleTransition::Deactivate)
975 .unwrap(),
976 LifecycleState::Inactive
977 );
978 assert_eq!(
979 node.trigger_transition(LifecycleTransition::ShutdownInactive)
980 .unwrap(),
981 LifecycleState::Finalized
982 );
983 }
984 }
985
986 #[test]
987 fn test_ctx_node_invalid_transition() {
988 unsafe {
989 let mut node = LifecyclePollingNodeCtx::new();
990 let err = node
991 .trigger_transition(LifecycleTransition::Activate)
992 .unwrap_err();
993 assert_eq!(
994 err,
995 LifecycleError::InvalidTransition {
996 from: LifecycleState::Unconfigured,
997 transition: LifecycleTransition::Activate,
998 }
999 );
1000 assert_eq!(node.state(), LifecycleState::Unconfigured);
1001 }
1002 }
1003
1004 #[test]
1005 fn test_ctx_node_callback_failure_rolls_back() {
1006 unsafe {
1007 let mut node = LifecyclePollingNodeCtx::new();
1008 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_failure));
1009 assert!(
1010 node.trigger_transition(LifecycleTransition::Configure)
1011 .is_err()
1012 );
1013 assert_eq!(node.state(), LifecycleState::Unconfigured);
1014 }
1015 }
1016
1017 #[test]
1018 fn test_ctx_node_callback_error_enters_error_processing() {
1019 unsafe {
1020 let mut node = LifecyclePollingNodeCtx::new();
1021 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_error));
1022 assert!(
1023 node.trigger_transition(LifecycleTransition::Configure)
1024 .is_err()
1025 );
1026 assert_eq!(node.state(), LifecycleState::ErrorProcessing);
1027 }
1028 }
1029
1030 #[test]
1031 fn test_ctx_node_finalized_rejects() {
1032 unsafe {
1033 let mut node = LifecyclePollingNodeCtx::new();
1034 node.finalize();
1035 let err = node
1036 .trigger_transition(LifecycleTransition::Configure)
1037 .unwrap_err();
1038 assert_eq!(err, LifecycleError::NodeFinalized);
1039 }
1040 }
1041
1042 #[test]
1043 fn test_ctx_node_context_passed() {
1044 use core::sync::atomic::{AtomicU32, Ordering};
1045 static SEEN: AtomicU32 = AtomicU32::new(0);
1046 unsafe extern "C" fn cb_record(ctx: *mut c_void) -> u8 {
1047 SEEN.store(ctx as usize as u32, Ordering::Relaxed);
1048 TransitionResult::Success as u8
1049 }
1050
1051 unsafe {
1052 let mut node = LifecyclePollingNodeCtx::new();
1053 node.set_context(0xBEEFu32 as usize as *mut c_void);
1054 node.register(LifecycleCallbackSlot::Configure, Some(cb_record));
1055 let _ = node.trigger_transition(LifecycleTransition::Configure);
1056 assert_eq!(SEEN.load(Ordering::Relaxed), 0xBEEF);
1057 }
1058 }
1059
1060 #[test]
1061 fn test_ctx_node_clear_callbacks_resets() {
1062 unsafe {
1063 let mut node = LifecyclePollingNodeCtx::new();
1064 node.set_context(core::ptr::dangling_mut::<c_void>());
1065 node.register(LifecycleCallbackSlot::Configure, Some(ctx_cb_success));
1066 node.clear_callbacks();
1067 assert!(node.context().is_null());
1068 assert_eq!(
1070 node.trigger_transition(LifecycleTransition::Configure)
1071 .unwrap(),
1072 LifecycleState::Inactive
1073 );
1074 }
1075 }
1076}