1use crate::curuntime::KeyFrame;
2use core::fmt;
3use core::marker::PhantomData;
4use cu29_traits::CopperListTuple;
5use cu29_traits::{CuError, CuResult};
6use cu29_unifiedlog::{SectionStorage, UnifiedLogWrite};
7
8#[cfg(feature = "std")]
9use crate::copperlist::CopperList;
10#[cfg(not(feature = "std"))]
11use alloc::vec::Vec;
12#[cfg(feature = "std")]
13use cu29_clock::RobotClockMock;
14#[cfg(feature = "std")]
15use std::vec::Vec;
16
17#[cfg(not(feature = "std"))]
18mod imp {
19 pub use alloc::boxed::Box;
20 pub use alloc::string::String;
21}
22
23#[cfg(feature = "std")]
24mod imp {
25 pub use crate::config::CuConfig;
26 pub use crate::simulation::SimOverride;
27 pub use cu29_clock::RobotClock;
28 pub use cu29_unifiedlog::memmap::MmapSectionStorage;
29 pub use std::sync::{Arc, Mutex};
30}
31
32use imp::*;
33
34#[cfg(feature = "std")]
36pub trait CuStdApplication:
37 CuApplication<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite>
38{
39}
40
41#[cfg(feature = "std")]
42impl<T> CuStdApplication for T where
43 T: CuApplication<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite>
44{
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct Subsystem {
50 id: Option<&'static str>,
51 code: u16,
52}
53
54impl Subsystem {
55 #[inline]
56 pub const fn new(id: Option<&'static str>, code: u16) -> Self {
57 Self { id, code }
58 }
59
60 #[inline]
61 pub const fn id(self) -> Option<&'static str> {
62 self.id
63 }
64
65 #[inline]
66 pub const fn code(self) -> u16 {
67 self.code
68 }
69}
70
71pub trait CuSubsystemMetadata {
73 fn subsystem() -> Subsystem;
75}
76
77pub trait CuApplication<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> {
87 fn get_original_config() -> String;
90
91 #[deprecated(
98 since = "1.2.0",
99 note = "use the typed transition `start()` on the handle returned by `build()`"
100 )]
101 fn start_all_tasks(&mut self) -> CuResult<()>;
102
103 #[deprecated(
111 since = "1.2.0",
112 note = "use `run_one_iteration()` on the Running handle from `build()?.start()?`"
113 )]
114 fn run_one_iteration(&mut self) -> CuResult<()>;
115
116 #[deprecated(
125 since = "1.2.0",
126 note = "use the typed transition `run_until_shutdown()` on the handle returned by `build()`"
127 )]
128 fn run(&mut self) -> CuResult<()>;
129
130 #[deprecated(
140 since = "1.2.0",
141 note = "use the typed transition `stop()` on the Running handle"
142 )]
143 fn stop_all_tasks(&mut self) -> CuResult<()>;
144
145 fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()>;
147}
148
149#[cfg(feature = "std")]
157pub trait CuSimApplication<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> {
158 type Step<'z>;
160
161 fn get_original_config() -> String;
164
165 fn mission_id() -> Option<&'static str> {
167 None
168 }
169
170 #[deprecated(
180 since = "1.2.0",
181 note = "use the typed transition `start()` on the handle returned by `build()`"
182 )]
183 fn start_all_tasks(
184 &mut self,
185 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
186 ) -> CuResult<()>;
187
188 #[deprecated(
198 since = "1.2.0",
199 note = "use `run_one_iteration()` on the Running handle from `build()?.start()?`"
200 )]
201 fn run_one_iteration(
202 &mut self,
203 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
204 ) -> CuResult<()>;
205
206 #[deprecated(
218 since = "1.2.0",
219 note = "use the typed transition `run_until_shutdown()` on the handle returned by `build()`"
220 )]
221 fn run(
222 &mut self,
223 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
224 ) -> CuResult<()>;
225
226 #[deprecated(
238 since = "1.2.0",
239 note = "use the typed transition `stop()` on the Running handle"
240 )]
241 fn stop_all_tasks(
242 &mut self,
243 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
244 ) -> CuResult<()>;
245
246 fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()>;
248}
249
250pub trait CurrentRuntimeCopperList<P: CopperListTuple> {
256 fn current_runtime_copperlist_bytes(&self) -> Option<&[u8]>;
257
258 fn set_current_runtime_copperlist_bytes(&mut self, snapshot: Option<Vec<u8>>) {
259 let _ = snapshot;
260 }
261}
262
263#[cfg(feature = "std")]
270pub trait CuRecordedReplayApplication<S: SectionStorage, L: UnifiedLogWrite<S> + 'static>:
271 CuSimApplication<S, L>
272{
273 type RecordedDataSet: CopperListTuple;
275
276 fn replay_recorded_copperlist(
286 &mut self,
287 clock_mock: &RobotClockMock,
288 copperlist: &CopperList<Self::RecordedDataSet>,
289 keyframe: Option<&KeyFrame>,
290 ) -> CuResult<()>;
291}
292
293#[cfg(feature = "std")]
299pub trait CuDistributedReplayApplication<S: SectionStorage, L: UnifiedLogWrite<S> + 'static>:
300 CuRecordedReplayApplication<S, L> + CuSubsystemMetadata
301{
302 fn build_distributed_replay(
304 clock: RobotClock,
305 unified_logger: Arc<Mutex<L>>,
306 instance_id: u32,
307 config_override: Option<CuConfig>,
308 ) -> CuResult<Self>
309 where
310 Self: Sized;
311}
312
313pub struct Initialized;
315
316pub struct Running;
318
319pub struct Stopped;
322
323pub struct Faulted;
327
328mod sealed {
329 pub trait Sealed {}
332 impl Sealed for super::Initialized {}
333 impl Sealed for super::Running {}
334 impl Sealed for super::Stopped {}
335 impl Sealed for super::Faulted {}
336}
337
338#[diagnostic::on_unimplemented(
340 message = "the application cannot be started from the `{Self}` lifecycle state",
341 label = "`start_all_tasks` and `run` require an `Initialized` or `Stopped` application",
342 note = "a `Running` application is already started and a `Faulted` application must first be cleaned up with `stop_all_tasks`"
343)]
344pub trait Startable: sealed::Sealed {}
345impl Startable for Initialized {}
346impl Startable for Stopped {}
347
348#[diagnostic::on_unimplemented(
350 message = "the application cannot be stopped from the `{Self}` lifecycle state",
351 label = "`stop_all_tasks` requires a `Running` or `Faulted` application",
352 note = "start the application first with `start_all_tasks`"
353)]
354pub trait Stoppable: sealed::Sealed {}
355impl Stoppable for Running {}
356impl Stoppable for Faulted {}
357
358pub struct CuAppLifecycle<S, L, A, State = Initialized> {
394 app: Box<A>,
395 _lifecycle: PhantomData<(S, L, State)>,
396}
397
398impl<S, L, A, State> fmt::Debug for CuAppLifecycle<S, L, A, State> {
399 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400 f.debug_struct("CuAppLifecycle")
401 .field("state", &core::any::type_name::<State>())
402 .finish_non_exhaustive()
403 }
404}
405
406#[cfg(feature = "std")]
409pub type CuStdAppLifecycle<A, State = Initialized> =
410 CuAppLifecycle<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite, A, State>;
411
412pub type TransitionResult<S, L, A, Next> =
415 Result<CuAppLifecycle<S, L, A, Next>, LifecycleError<S, L, A>>;
416
417pub struct LifecycleError<S, L, A> {
423 pub error: CuError,
425 pub app: CuAppLifecycle<S, L, A, Faulted>,
427}
428
429impl<S, L, A> fmt::Debug for LifecycleError<S, L, A> {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 f.debug_struct("LifecycleError")
432 .field("error", &self.error)
433 .finish_non_exhaustive()
434 }
435}
436
437impl<S, L, A> fmt::Display for LifecycleError<S, L, A> {
438 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439 write!(f, "lifecycle transition failed: {}", self.error)
440 }
441}
442
443impl<S, L, A> core::error::Error for LifecycleError<S, L, A> {
444 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
445 Some(&self.error)
446 }
447}
448
449impl<S, L, A> From<LifecycleError<S, L, A>> for CuError {
452 fn from(value: LifecycleError<S, L, A>) -> Self {
453 value.error
454 }
455}
456
457impl<S, L, A, State> CuAppLifecycle<S, L, A, State> {
458 fn into_state<Next>(self) -> CuAppLifecycle<S, L, A, Next> {
461 CuAppLifecycle {
462 app: self.app,
463 _lifecycle: PhantomData,
464 }
465 }
466
467 pub fn inner(&self) -> &A {
469 &self.app
470 }
471
472 pub fn into_inner(self) -> A {
475 *self.app
476 }
477}
478
479impl<S, L, A> CuAppLifecycle<S, L, A, Initialized>
480where
481 S: SectionStorage,
482 L: UnifiedLogWrite<S> + 'static,
483 A: CuApplication<S, L>,
484{
485 pub fn new(app: A) -> Self {
487 CuAppLifecycle {
488 app: Box::new(app),
489 _lifecycle: PhantomData,
490 }
491 }
492
493 pub fn inner_mut(&mut self) -> &mut A {
500 &mut self.app
501 }
502}
503
504impl<S, L, A, State> CuAppLifecycle<S, L, A, State>
505where
506 S: SectionStorage,
507 L: UnifiedLogWrite<S> + 'static,
508 A: CuApplication<S, L>,
509 State: Startable,
510{
511 #[allow(deprecated)] pub fn start(mut self) -> TransitionResult<S, L, A, Running> {
517 match self.app.start_all_tasks() {
518 Ok(()) => Ok(self.into_state()),
519 Err(error) => Err(LifecycleError {
520 error,
521 app: self.into_state(),
522 }),
523 }
524 }
525
526 #[allow(deprecated)] pub fn run_until_shutdown(mut self) -> TransitionResult<S, L, A, Stopped> {
530 match self.app.run() {
531 Ok(()) => Ok(self.into_state()),
532 Err(error) => Err(LifecycleError {
533 error,
534 app: self.into_state(),
535 }),
536 }
537 }
538}
539
540impl<S, L, A> CuAppLifecycle<S, L, A, Running>
541where
542 S: SectionStorage,
543 L: UnifiedLogWrite<S> + 'static,
544 A: CuApplication<S, L>,
545{
546 #[allow(deprecated)] pub fn run_one_iteration(&mut self) -> CuResult<()> {
551 self.app.run_one_iteration()
552 }
553}
554
555impl<S, L, A, State> CuAppLifecycle<S, L, A, State>
556where
557 S: SectionStorage,
558 L: UnifiedLogWrite<S> + 'static,
559 A: CuApplication<S, L>,
560 State: Stoppable,
561{
562 #[allow(deprecated)] pub fn stop(mut self) -> TransitionResult<S, L, A, Stopped> {
567 match self.app.stop_all_tasks() {
568 Ok(()) => Ok(self.into_state()),
569 Err(error) => Err(LifecycleError {
570 error,
571 app: self.into_state(),
572 }),
573 }
574 }
575}
576
577impl<S, L, A, State> core::ops::Deref for CuAppLifecycle<S, L, A, State> {
580 type Target = A;
581
582 fn deref(&self) -> &A {
583 &self.app
584 }
585}
586
587impl<S, L, A> core::ops::DerefMut for CuAppLifecycle<S, L, A, Initialized> {
590 fn deref_mut(&mut self) -> &mut A {
591 &mut self.app
592 }
593}
594
595#[allow(deprecated)]
601impl<S, L, A> CuApplication<S, L> for CuAppLifecycle<S, L, A, Initialized>
602where
603 S: SectionStorage,
604 L: UnifiedLogWrite<S> + 'static,
605 A: CuApplication<S, L>,
606{
607 fn get_original_config() -> String {
608 A::get_original_config()
609 }
610
611 fn start_all_tasks(&mut self) -> CuResult<()> {
612 self.app.start_all_tasks()
613 }
614
615 fn run_one_iteration(&mut self) -> CuResult<()> {
616 self.app.run_one_iteration()
617 }
618
619 fn run(&mut self) -> CuResult<()> {
620 self.app.run()
621 }
622
623 fn stop_all_tasks(&mut self) -> CuResult<()> {
624 self.app.stop_all_tasks()
625 }
626
627 fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()> {
628 self.app.restore_keyframe(freezer)
629 }
630}
631
632#[cfg(feature = "std")]
639pub struct CuSimAppLifecycle<S, L, A, State = Initialized> {
640 app: Box<A>,
641 _lifecycle: PhantomData<(S, L, State)>,
642}
643
644#[cfg(feature = "std")]
645impl<S, L, A, State> fmt::Debug for CuSimAppLifecycle<S, L, A, State> {
646 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
647 f.debug_struct("CuSimAppLifecycle")
648 .field("state", &core::any::type_name::<State>())
649 .finish_non_exhaustive()
650 }
651}
652
653#[cfg(feature = "std")]
656pub type CuStdSimAppLifecycle<A, State = Initialized> =
657 CuSimAppLifecycle<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite, A, State>;
658
659#[cfg(feature = "std")]
663pub type SimTransitionResult<S, L, A, Next> =
664 Result<CuSimAppLifecycle<S, L, A, Next>, SimLifecycleError<S, L, A>>;
665
666#[cfg(feature = "std")]
668pub struct SimLifecycleError<S, L, A> {
669 pub error: CuError,
671 pub app: CuSimAppLifecycle<S, L, A, Faulted>,
673}
674
675#[cfg(feature = "std")]
676impl<S, L, A> fmt::Debug for SimLifecycleError<S, L, A> {
677 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
678 f.debug_struct("SimLifecycleError")
679 .field("error", &self.error)
680 .finish_non_exhaustive()
681 }
682}
683
684#[cfg(feature = "std")]
685impl<S, L, A> fmt::Display for SimLifecycleError<S, L, A> {
686 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
687 write!(f, "lifecycle transition failed: {}", self.error)
688 }
689}
690
691#[cfg(feature = "std")]
692impl<S, L, A> core::error::Error for SimLifecycleError<S, L, A> {
693 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
694 Some(&self.error)
695 }
696}
697
698#[cfg(feature = "std")]
701impl<S, L, A> From<SimLifecycleError<S, L, A>> for CuError {
702 fn from(value: SimLifecycleError<S, L, A>) -> Self {
703 value.error
704 }
705}
706
707#[cfg(feature = "std")]
708impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State> {
709 fn into_state<Next>(self) -> CuSimAppLifecycle<S, L, A, Next> {
712 CuSimAppLifecycle {
713 app: self.app,
714 _lifecycle: PhantomData,
715 }
716 }
717
718 pub fn inner(&self) -> &A {
720 &self.app
721 }
722
723 pub fn into_inner(self) -> A {
726 *self.app
727 }
728}
729
730#[cfg(feature = "std")]
731impl<S, L, A> CuSimAppLifecycle<S, L, A, Initialized>
732where
733 S: SectionStorage,
734 L: UnifiedLogWrite<S> + 'static,
735 A: CuSimApplication<S, L>,
736{
737 pub fn new(app: A) -> Self {
739 CuSimAppLifecycle {
740 app: Box::new(app),
741 _lifecycle: PhantomData,
742 }
743 }
744}
745
746#[cfg(feature = "std")]
747impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State>
748where
749 S: SectionStorage,
750 L: UnifiedLogWrite<S> + 'static,
751 A: CuSimApplication<S, L>,
752 State: Startable,
753{
754 #[allow(deprecated)] pub fn start(
760 mut self,
761 sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
762 ) -> SimTransitionResult<S, L, A, Running> {
763 match self.app.start_all_tasks(sim_callback) {
764 Ok(()) => Ok(self.into_state()),
765 Err(error) => Err(SimLifecycleError {
766 error,
767 app: self.into_state(),
768 }),
769 }
770 }
771
772 #[allow(deprecated)] pub fn run_until_shutdown(
776 mut self,
777 sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
778 ) -> SimTransitionResult<S, L, A, Stopped> {
779 match self.app.run(sim_callback) {
780 Ok(()) => Ok(self.into_state()),
781 Err(error) => Err(SimLifecycleError {
782 error,
783 app: self.into_state(),
784 }),
785 }
786 }
787}
788
789#[cfg(feature = "std")]
790impl<S, L, A> CuSimAppLifecycle<S, L, A, Running>
791where
792 S: SectionStorage,
793 L: UnifiedLogWrite<S> + 'static,
794 A: CuSimApplication<S, L>,
795{
796 #[allow(deprecated)] pub fn run_one_iteration(
801 &mut self,
802 sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
803 ) -> CuResult<()> {
804 self.app.run_one_iteration(sim_callback)
805 }
806}
807
808#[cfg(feature = "std")]
809impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State>
810where
811 S: SectionStorage,
812 L: UnifiedLogWrite<S> + 'static,
813 A: CuSimApplication<S, L>,
814 State: Stoppable,
815{
816 #[allow(deprecated)] pub fn stop(
821 mut self,
822 sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
823 ) -> SimTransitionResult<S, L, A, Stopped> {
824 match self.app.stop_all_tasks(sim_callback) {
825 Ok(()) => Ok(self.into_state()),
826 Err(error) => Err(SimLifecycleError {
827 error,
828 app: self.into_state(),
829 }),
830 }
831 }
832}
833
834#[cfg(feature = "std")]
836impl<S, L, A, State> core::ops::Deref for CuSimAppLifecycle<S, L, A, State> {
837 type Target = A;
838
839 fn deref(&self) -> &A {
840 &self.app
841 }
842}
843
844#[cfg(feature = "std")]
847impl<S, L, A> core::ops::DerefMut for CuSimAppLifecycle<S, L, A, Initialized> {
848 fn deref_mut(&mut self) -> &mut A {
849 &mut self.app
850 }
851}
852
853#[cfg(feature = "std")]
857#[allow(deprecated)]
858impl<S, L, A> CuSimApplication<S, L> for CuSimAppLifecycle<S, L, A, Initialized>
859where
860 S: SectionStorage,
861 L: UnifiedLogWrite<S> + 'static,
862 A: CuSimApplication<S, L>,
863{
864 type Step<'z> = <A as CuSimApplication<S, L>>::Step<'z>;
865
866 fn get_original_config() -> String {
867 A::get_original_config()
868 }
869
870 fn mission_id() -> Option<&'static str> {
871 A::mission_id()
872 }
873
874 fn start_all_tasks(
875 &mut self,
876 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
877 ) -> CuResult<()> {
878 self.app.start_all_tasks(sim_callback)
879 }
880
881 fn run_one_iteration(
882 &mut self,
883 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
884 ) -> CuResult<()> {
885 self.app.run_one_iteration(sim_callback)
886 }
887
888 fn run(
889 &mut self,
890 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
891 ) -> CuResult<()> {
892 self.app.run(sim_callback)
893 }
894
895 fn stop_all_tasks(
896 &mut self,
897 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
898 ) -> CuResult<()> {
899 self.app.stop_all_tasks(sim_callback)
900 }
901
902 fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()> {
903 self.app.restore_keyframe(freezer)
904 }
905}
906
907#[cfg(all(test, feature = "std"))]
908mod lifecycle_tests {
909 use super::*;
910
911 #[derive(Default)]
912 struct MockApp {
913 started: u32,
914 stopped: u32,
915 iterations: u32,
916 runs: u32,
917 fail_start: bool,
918 fail_stop: bool,
919 }
920
921 #[allow(deprecated)] impl<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> CuApplication<S, L> for MockApp {
923 fn get_original_config() -> String {
924 String::new()
925 }
926
927 fn start_all_tasks(&mut self) -> CuResult<()> {
928 if self.fail_start {
929 return Err("mock start failure".into());
930 }
931 self.started += 1;
932 Ok(())
933 }
934
935 fn run_one_iteration(&mut self) -> CuResult<()> {
936 self.iterations += 1;
937 Ok(())
938 }
939
940 fn run(&mut self) -> CuResult<()> {
941 if self.fail_start {
942 return Err("mock run failure".into());
943 }
944 self.runs += 1;
945 Ok(())
946 }
947
948 fn stop_all_tasks(&mut self) -> CuResult<()> {
949 if self.fail_stop {
950 return Err("mock stop failure".into());
951 }
952 self.stopped += 1;
953 Ok(())
954 }
955
956 fn restore_keyframe(&mut self, _freezer: &KeyFrame) -> CuResult<()> {
957 Ok(())
958 }
959 }
960
961 type Lifecycle = CuStdAppLifecycle<MockApp>;
962
963 #[test]
964 fn full_cycle_with_restart() {
965 let app = Lifecycle::new(MockApp::default());
966 let mut running = app.start().unwrap();
967 running.run_one_iteration().unwrap();
968 running.run_one_iteration().unwrap();
969 let stopped = running.stop().unwrap();
970
971 let running = stopped.start().unwrap();
973 let stopped = running.stop().unwrap();
974
975 let mock = stopped.into_inner();
976 assert_eq!(mock.started, 2);
977 assert_eq!(mock.iterations, 2);
978 assert_eq!(mock.stopped, 2);
979 }
980
981 #[test]
982 fn run_transitions_to_stopped_and_can_run_again() {
983 let app = Lifecycle::new(MockApp::default());
984 let stopped = app.run_until_shutdown().unwrap();
985 let stopped = stopped.run_until_shutdown().unwrap();
986 assert_eq!(stopped.inner().runs, 2);
987 }
988
989 #[test]
990 fn failed_start_hands_back_a_faulted_app_for_cleanup() {
991 let app = Lifecycle::new(MockApp {
992 fail_start: true,
993 ..Default::default()
994 });
995 let err = app.start().unwrap_err();
996 assert!(err.error.to_string().contains("mock start failure"));
997
998 let stopped = err.app.stop().unwrap();
1000 let mock = stopped.into_inner();
1001 assert_eq!(mock.started, 0);
1002 assert_eq!(mock.stopped, 1);
1003 }
1004
1005 #[test]
1006 fn failed_stop_hands_back_a_faulted_app() {
1007 let app = Lifecycle::new(MockApp {
1008 fail_stop: true,
1009 ..Default::default()
1010 });
1011 let running = app.start().unwrap();
1012 let err = running.stop().unwrap_err();
1013 assert!(err.error.to_string().contains("mock stop failure"));
1014 }
1015
1016 #[test]
1017 fn lifecycle_error_propagates_with_question_mark() {
1018 fn drive() -> CuResult<()> {
1019 let app = Lifecycle::new(MockApp {
1020 fail_start: true,
1021 ..Default::default()
1022 });
1023 let _running = app.start()?;
1024 Ok(())
1025 }
1026 let error = drive().unwrap_err();
1027 assert!(error.to_string().contains("mock start failure"));
1028 }
1029
1030 #[derive(Default)]
1031 struct MockSimApp {
1032 started: u32,
1033 stopped: u32,
1034 iterations: u32,
1035 fail_start: bool,
1036 }
1037
1038 struct MockStep;
1039
1040 #[allow(deprecated)] impl<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> CuSimApplication<S, L> for MockSimApp {
1042 type Step<'z> = MockStep;
1043
1044 fn get_original_config() -> String {
1045 String::new()
1046 }
1047
1048 fn start_all_tasks(
1049 &mut self,
1050 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1051 ) -> CuResult<()> {
1052 if self.fail_start {
1053 return Err("mock sim start failure".into());
1054 }
1055 let _ = sim_callback(MockStep);
1056 self.started += 1;
1057 Ok(())
1058 }
1059
1060 fn run_one_iteration(
1061 &mut self,
1062 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1063 ) -> CuResult<()> {
1064 let _ = sim_callback(MockStep);
1065 self.iterations += 1;
1066 Ok(())
1067 }
1068
1069 fn run(
1070 &mut self,
1071 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1072 ) -> CuResult<()> {
1073 let _ = sim_callback(MockStep);
1074 Ok(())
1075 }
1076
1077 fn stop_all_tasks(
1078 &mut self,
1079 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1080 ) -> CuResult<()> {
1081 let _ = sim_callback(MockStep);
1082 self.stopped += 1;
1083 Ok(())
1084 }
1085
1086 fn restore_keyframe(&mut self, _freezer: &KeyFrame) -> CuResult<()> {
1087 Ok(())
1088 }
1089 }
1090
1091 type SimLifecycle = CuStdSimAppLifecycle<MockSimApp>;
1092
1093 #[test]
1094 fn sim_full_cycle_threads_the_callback() {
1095 let mut callback_calls = 0u32;
1096 let mut cb = |_step: MockStep| -> SimOverride {
1097 callback_calls += 1;
1098 SimOverride::ExecuteByRuntime
1099 };
1100
1101 let app = SimLifecycle::new(MockSimApp::default());
1102 let mut running = app.start(&mut cb).unwrap();
1103 running.run_one_iteration(&mut cb).unwrap();
1104 let stopped = running.stop(&mut cb).unwrap();
1105
1106 let mock = stopped.into_inner();
1107 assert_eq!(mock.started, 1);
1108 assert_eq!(mock.iterations, 1);
1109 assert_eq!(mock.stopped, 1);
1110 assert_eq!(callback_calls, 3);
1111 }
1112
1113 #[test]
1114 fn sim_failed_start_hands_back_a_faulted_app_for_cleanup() {
1115 let mut cb = |_step: MockStep| -> SimOverride { SimOverride::ExecuteByRuntime };
1116 let app = SimLifecycle::new(MockSimApp {
1117 fail_start: true,
1118 ..Default::default()
1119 });
1120 let err = app.start(&mut cb).unwrap_err();
1121 assert!(err.error.to_string().contains("mock sim start failure"));
1122 let stopped = err.app.stop(&mut cb).unwrap();
1123 assert_eq!(stopped.inner().stopped, 1);
1124 }
1125}