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(
278 &mut self,
279 clock_mock: &RobotClockMock,
280 copperlist: &CopperList<Self::RecordedDataSet>,
281 keyframe: Option<&KeyFrame>,
282 ) -> CuResult<()>;
283}
284
285#[cfg(feature = "std")]
291pub trait CuDistributedReplayApplication<S: SectionStorage, L: UnifiedLogWrite<S> + 'static>:
292 CuRecordedReplayApplication<S, L> + CuSubsystemMetadata
293{
294 fn build_distributed_replay(
296 clock: RobotClock,
297 unified_logger: Arc<Mutex<L>>,
298 instance_id: u32,
299 config_override: Option<CuConfig>,
300 ) -> CuResult<Self>
301 where
302 Self: Sized;
303}
304
305pub struct Initialized;
307
308pub struct Running;
310
311pub struct Stopped;
314
315pub struct Faulted;
319
320mod sealed {
321 pub trait Sealed {}
324 impl Sealed for super::Initialized {}
325 impl Sealed for super::Running {}
326 impl Sealed for super::Stopped {}
327 impl Sealed for super::Faulted {}
328}
329
330#[diagnostic::on_unimplemented(
332 message = "the application cannot be started from the `{Self}` lifecycle state",
333 label = "`start_all_tasks` and `run` require an `Initialized` or `Stopped` application",
334 note = "a `Running` application is already started and a `Faulted` application must first be cleaned up with `stop_all_tasks`"
335)]
336pub trait Startable: sealed::Sealed {}
337impl Startable for Initialized {}
338impl Startable for Stopped {}
339
340#[diagnostic::on_unimplemented(
342 message = "the application cannot be stopped from the `{Self}` lifecycle state",
343 label = "`stop_all_tasks` requires a `Running` or `Faulted` application",
344 note = "start the application first with `start_all_tasks`"
345)]
346pub trait Stoppable: sealed::Sealed {}
347impl Stoppable for Running {}
348impl Stoppable for Faulted {}
349
350pub struct CuAppLifecycle<S, L, A, State = Initialized> {
386 app: Box<A>,
387 _lifecycle: PhantomData<(S, L, State)>,
388}
389
390impl<S, L, A, State> fmt::Debug for CuAppLifecycle<S, L, A, State> {
391 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392 f.debug_struct("CuAppLifecycle")
393 .field("state", &core::any::type_name::<State>())
394 .finish_non_exhaustive()
395 }
396}
397
398#[cfg(feature = "std")]
401pub type CuStdAppLifecycle<A, State = Initialized> =
402 CuAppLifecycle<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite, A, State>;
403
404pub type TransitionResult<S, L, A, Next> =
407 Result<CuAppLifecycle<S, L, A, Next>, LifecycleError<S, L, A>>;
408
409pub struct LifecycleError<S, L, A> {
415 pub error: CuError,
417 pub app: CuAppLifecycle<S, L, A, Faulted>,
419}
420
421impl<S, L, A> fmt::Debug for LifecycleError<S, L, A> {
422 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423 f.debug_struct("LifecycleError")
424 .field("error", &self.error)
425 .finish_non_exhaustive()
426 }
427}
428
429impl<S, L, A> fmt::Display for LifecycleError<S, L, A> {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 write!(f, "lifecycle transition failed: {}", self.error)
432 }
433}
434
435impl<S, L, A> core::error::Error for LifecycleError<S, L, A> {
436 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
437 Some(&self.error)
438 }
439}
440
441impl<S, L, A> From<LifecycleError<S, L, A>> for CuError {
444 fn from(value: LifecycleError<S, L, A>) -> Self {
445 value.error
446 }
447}
448
449impl<S, L, A, State> CuAppLifecycle<S, L, A, State> {
450 fn into_state<Next>(self) -> CuAppLifecycle<S, L, A, Next> {
453 CuAppLifecycle {
454 app: self.app,
455 _lifecycle: PhantomData,
456 }
457 }
458
459 pub fn inner(&self) -> &A {
461 &self.app
462 }
463
464 pub fn into_inner(self) -> A {
467 *self.app
468 }
469}
470
471impl<S, L, A> CuAppLifecycle<S, L, A, Initialized>
472where
473 S: SectionStorage,
474 L: UnifiedLogWrite<S> + 'static,
475 A: CuApplication<S, L>,
476{
477 pub fn new(app: A) -> Self {
479 CuAppLifecycle {
480 app: Box::new(app),
481 _lifecycle: PhantomData,
482 }
483 }
484
485 pub fn inner_mut(&mut self) -> &mut A {
492 &mut self.app
493 }
494}
495
496impl<S, L, A, State> CuAppLifecycle<S, L, A, State>
497where
498 S: SectionStorage,
499 L: UnifiedLogWrite<S> + 'static,
500 A: CuApplication<S, L>,
501 State: Startable,
502{
503 #[allow(deprecated)] pub fn start(mut self) -> TransitionResult<S, L, A, Running> {
509 match self.app.start_all_tasks() {
510 Ok(()) => Ok(self.into_state()),
511 Err(error) => Err(LifecycleError {
512 error,
513 app: self.into_state(),
514 }),
515 }
516 }
517
518 #[allow(deprecated)] pub fn run_until_shutdown(mut self) -> TransitionResult<S, L, A, Stopped> {
522 match self.app.run() {
523 Ok(()) => Ok(self.into_state()),
524 Err(error) => Err(LifecycleError {
525 error,
526 app: self.into_state(),
527 }),
528 }
529 }
530}
531
532impl<S, L, A> CuAppLifecycle<S, L, A, Running>
533where
534 S: SectionStorage,
535 L: UnifiedLogWrite<S> + 'static,
536 A: CuApplication<S, L>,
537{
538 #[allow(deprecated)] pub fn run_one_iteration(&mut self) -> CuResult<()> {
543 self.app.run_one_iteration()
544 }
545}
546
547impl<S, L, A, State> CuAppLifecycle<S, L, A, State>
548where
549 S: SectionStorage,
550 L: UnifiedLogWrite<S> + 'static,
551 A: CuApplication<S, L>,
552 State: Stoppable,
553{
554 #[allow(deprecated)] pub fn stop(mut self) -> TransitionResult<S, L, A, Stopped> {
559 match self.app.stop_all_tasks() {
560 Ok(()) => Ok(self.into_state()),
561 Err(error) => Err(LifecycleError {
562 error,
563 app: self.into_state(),
564 }),
565 }
566 }
567}
568
569impl<S, L, A, State> core::ops::Deref for CuAppLifecycle<S, L, A, State> {
572 type Target = A;
573
574 fn deref(&self) -> &A {
575 &self.app
576 }
577}
578
579impl<S, L, A> core::ops::DerefMut for CuAppLifecycle<S, L, A, Initialized> {
582 fn deref_mut(&mut self) -> &mut A {
583 &mut self.app
584 }
585}
586
587#[allow(deprecated)]
593impl<S, L, A> CuApplication<S, L> for CuAppLifecycle<S, L, A, Initialized>
594where
595 S: SectionStorage,
596 L: UnifiedLogWrite<S> + 'static,
597 A: CuApplication<S, L>,
598{
599 fn get_original_config() -> String {
600 A::get_original_config()
601 }
602
603 fn start_all_tasks(&mut self) -> CuResult<()> {
604 self.app.start_all_tasks()
605 }
606
607 fn run_one_iteration(&mut self) -> CuResult<()> {
608 self.app.run_one_iteration()
609 }
610
611 fn run(&mut self) -> CuResult<()> {
612 self.app.run()
613 }
614
615 fn stop_all_tasks(&mut self) -> CuResult<()> {
616 self.app.stop_all_tasks()
617 }
618
619 fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()> {
620 self.app.restore_keyframe(freezer)
621 }
622}
623
624#[cfg(feature = "std")]
631pub struct CuSimAppLifecycle<S, L, A, State = Initialized> {
632 app: Box<A>,
633 _lifecycle: PhantomData<(S, L, State)>,
634}
635
636#[cfg(feature = "std")]
637impl<S, L, A, State> fmt::Debug for CuSimAppLifecycle<S, L, A, State> {
638 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
639 f.debug_struct("CuSimAppLifecycle")
640 .field("state", &core::any::type_name::<State>())
641 .finish_non_exhaustive()
642 }
643}
644
645#[cfg(feature = "std")]
648pub type CuStdSimAppLifecycle<A, State = Initialized> =
649 CuSimAppLifecycle<MmapSectionStorage, cu29_unifiedlog::UnifiedLoggerWrite, A, State>;
650
651#[cfg(feature = "std")]
655pub type SimTransitionResult<S, L, A, Next> =
656 Result<CuSimAppLifecycle<S, L, A, Next>, SimLifecycleError<S, L, A>>;
657
658#[cfg(feature = "std")]
660pub struct SimLifecycleError<S, L, A> {
661 pub error: CuError,
663 pub app: CuSimAppLifecycle<S, L, A, Faulted>,
665}
666
667#[cfg(feature = "std")]
668impl<S, L, A> fmt::Debug for SimLifecycleError<S, L, A> {
669 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670 f.debug_struct("SimLifecycleError")
671 .field("error", &self.error)
672 .finish_non_exhaustive()
673 }
674}
675
676#[cfg(feature = "std")]
677impl<S, L, A> fmt::Display for SimLifecycleError<S, L, A> {
678 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
679 write!(f, "lifecycle transition failed: {}", self.error)
680 }
681}
682
683#[cfg(feature = "std")]
684impl<S, L, A> core::error::Error for SimLifecycleError<S, L, A> {
685 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
686 Some(&self.error)
687 }
688}
689
690#[cfg(feature = "std")]
693impl<S, L, A> From<SimLifecycleError<S, L, A>> for CuError {
694 fn from(value: SimLifecycleError<S, L, A>) -> Self {
695 value.error
696 }
697}
698
699#[cfg(feature = "std")]
700impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State> {
701 fn into_state<Next>(self) -> CuSimAppLifecycle<S, L, A, Next> {
704 CuSimAppLifecycle {
705 app: self.app,
706 _lifecycle: PhantomData,
707 }
708 }
709
710 pub fn inner(&self) -> &A {
712 &self.app
713 }
714
715 pub fn into_inner(self) -> A {
718 *self.app
719 }
720}
721
722#[cfg(feature = "std")]
723impl<S, L, A> CuSimAppLifecycle<S, L, A, Initialized>
724where
725 S: SectionStorage,
726 L: UnifiedLogWrite<S> + 'static,
727 A: CuSimApplication<S, L>,
728{
729 pub fn new(app: A) -> Self {
731 CuSimAppLifecycle {
732 app: Box::new(app),
733 _lifecycle: PhantomData,
734 }
735 }
736}
737
738#[cfg(feature = "std")]
739impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State>
740where
741 S: SectionStorage,
742 L: UnifiedLogWrite<S> + 'static,
743 A: CuSimApplication<S, L>,
744 State: Startable,
745{
746 #[allow(deprecated)] pub fn start(
752 mut self,
753 sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
754 ) -> SimTransitionResult<S, L, A, Running> {
755 match self.app.start_all_tasks(sim_callback) {
756 Ok(()) => Ok(self.into_state()),
757 Err(error) => Err(SimLifecycleError {
758 error,
759 app: self.into_state(),
760 }),
761 }
762 }
763
764 #[allow(deprecated)] pub fn run_until_shutdown(
768 mut self,
769 sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
770 ) -> SimTransitionResult<S, L, A, Stopped> {
771 match self.app.run(sim_callback) {
772 Ok(()) => Ok(self.into_state()),
773 Err(error) => Err(SimLifecycleError {
774 error,
775 app: self.into_state(),
776 }),
777 }
778 }
779}
780
781#[cfg(feature = "std")]
782impl<S, L, A> CuSimAppLifecycle<S, L, A, Running>
783where
784 S: SectionStorage,
785 L: UnifiedLogWrite<S> + 'static,
786 A: CuSimApplication<S, L>,
787{
788 #[allow(deprecated)] pub fn run_one_iteration(
793 &mut self,
794 sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
795 ) -> CuResult<()> {
796 self.app.run_one_iteration(sim_callback)
797 }
798}
799
800#[cfg(feature = "std")]
801impl<S, L, A, State> CuSimAppLifecycle<S, L, A, State>
802where
803 S: SectionStorage,
804 L: UnifiedLogWrite<S> + 'static,
805 A: CuSimApplication<S, L>,
806 State: Stoppable,
807{
808 #[allow(deprecated)] pub fn stop(
813 mut self,
814 sim_callback: &mut impl for<'z> FnMut(<A as CuSimApplication<S, L>>::Step<'z>) -> SimOverride,
815 ) -> SimTransitionResult<S, L, A, Stopped> {
816 match self.app.stop_all_tasks(sim_callback) {
817 Ok(()) => Ok(self.into_state()),
818 Err(error) => Err(SimLifecycleError {
819 error,
820 app: self.into_state(),
821 }),
822 }
823 }
824}
825
826#[cfg(feature = "std")]
828impl<S, L, A, State> core::ops::Deref for CuSimAppLifecycle<S, L, A, State> {
829 type Target = A;
830
831 fn deref(&self) -> &A {
832 &self.app
833 }
834}
835
836#[cfg(feature = "std")]
839impl<S, L, A> core::ops::DerefMut for CuSimAppLifecycle<S, L, A, Initialized> {
840 fn deref_mut(&mut self) -> &mut A {
841 &mut self.app
842 }
843}
844
845#[cfg(feature = "std")]
849#[allow(deprecated)]
850impl<S, L, A> CuSimApplication<S, L> for CuSimAppLifecycle<S, L, A, Initialized>
851where
852 S: SectionStorage,
853 L: UnifiedLogWrite<S> + 'static,
854 A: CuSimApplication<S, L>,
855{
856 type Step<'z> = <A as CuSimApplication<S, L>>::Step<'z>;
857
858 fn get_original_config() -> String {
859 A::get_original_config()
860 }
861
862 fn mission_id() -> Option<&'static str> {
863 A::mission_id()
864 }
865
866 fn start_all_tasks(
867 &mut self,
868 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
869 ) -> CuResult<()> {
870 self.app.start_all_tasks(sim_callback)
871 }
872
873 fn run_one_iteration(
874 &mut self,
875 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
876 ) -> CuResult<()> {
877 self.app.run_one_iteration(sim_callback)
878 }
879
880 fn run(
881 &mut self,
882 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
883 ) -> CuResult<()> {
884 self.app.run(sim_callback)
885 }
886
887 fn stop_all_tasks(
888 &mut self,
889 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
890 ) -> CuResult<()> {
891 self.app.stop_all_tasks(sim_callback)
892 }
893
894 fn restore_keyframe(&mut self, freezer: &KeyFrame) -> CuResult<()> {
895 self.app.restore_keyframe(freezer)
896 }
897}
898
899#[cfg(all(test, feature = "std"))]
900mod lifecycle_tests {
901 use super::*;
902
903 #[derive(Default)]
904 struct MockApp {
905 started: u32,
906 stopped: u32,
907 iterations: u32,
908 runs: u32,
909 fail_start: bool,
910 fail_stop: bool,
911 }
912
913 #[allow(deprecated)] impl<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> CuApplication<S, L> for MockApp {
915 fn get_original_config() -> String {
916 String::new()
917 }
918
919 fn start_all_tasks(&mut self) -> CuResult<()> {
920 if self.fail_start {
921 return Err("mock start failure".into());
922 }
923 self.started += 1;
924 Ok(())
925 }
926
927 fn run_one_iteration(&mut self) -> CuResult<()> {
928 self.iterations += 1;
929 Ok(())
930 }
931
932 fn run(&mut self) -> CuResult<()> {
933 if self.fail_start {
934 return Err("mock run failure".into());
935 }
936 self.runs += 1;
937 Ok(())
938 }
939
940 fn stop_all_tasks(&mut self) -> CuResult<()> {
941 if self.fail_stop {
942 return Err("mock stop failure".into());
943 }
944 self.stopped += 1;
945 Ok(())
946 }
947
948 fn restore_keyframe(&mut self, _freezer: &KeyFrame) -> CuResult<()> {
949 Ok(())
950 }
951 }
952
953 type Lifecycle = CuStdAppLifecycle<MockApp>;
954
955 #[test]
956 fn full_cycle_with_restart() {
957 let app = Lifecycle::new(MockApp::default());
958 let mut running = app.start().unwrap();
959 running.run_one_iteration().unwrap();
960 running.run_one_iteration().unwrap();
961 let stopped = running.stop().unwrap();
962
963 let running = stopped.start().unwrap();
965 let stopped = running.stop().unwrap();
966
967 let mock = stopped.into_inner();
968 assert_eq!(mock.started, 2);
969 assert_eq!(mock.iterations, 2);
970 assert_eq!(mock.stopped, 2);
971 }
972
973 #[test]
974 fn run_transitions_to_stopped_and_can_run_again() {
975 let app = Lifecycle::new(MockApp::default());
976 let stopped = app.run_until_shutdown().unwrap();
977 let stopped = stopped.run_until_shutdown().unwrap();
978 assert_eq!(stopped.inner().runs, 2);
979 }
980
981 #[test]
982 fn failed_start_hands_back_a_faulted_app_for_cleanup() {
983 let app = Lifecycle::new(MockApp {
984 fail_start: true,
985 ..Default::default()
986 });
987 let err = app.start().unwrap_err();
988 assert!(err.error.to_string().contains("mock start failure"));
989
990 let stopped = err.app.stop().unwrap();
992 let mock = stopped.into_inner();
993 assert_eq!(mock.started, 0);
994 assert_eq!(mock.stopped, 1);
995 }
996
997 #[test]
998 fn failed_stop_hands_back_a_faulted_app() {
999 let app = Lifecycle::new(MockApp {
1000 fail_stop: true,
1001 ..Default::default()
1002 });
1003 let running = app.start().unwrap();
1004 let err = running.stop().unwrap_err();
1005 assert!(err.error.to_string().contains("mock stop failure"));
1006 }
1007
1008 #[test]
1009 fn lifecycle_error_propagates_with_question_mark() {
1010 fn drive() -> CuResult<()> {
1011 let app = Lifecycle::new(MockApp {
1012 fail_start: true,
1013 ..Default::default()
1014 });
1015 let _running = app.start()?;
1016 Ok(())
1017 }
1018 let error = drive().unwrap_err();
1019 assert!(error.to_string().contains("mock start failure"));
1020 }
1021
1022 #[derive(Default)]
1023 struct MockSimApp {
1024 started: u32,
1025 stopped: u32,
1026 iterations: u32,
1027 fail_start: bool,
1028 }
1029
1030 struct MockStep;
1031
1032 #[allow(deprecated)] impl<S: SectionStorage, L: UnifiedLogWrite<S> + 'static> CuSimApplication<S, L> for MockSimApp {
1034 type Step<'z> = MockStep;
1035
1036 fn get_original_config() -> String {
1037 String::new()
1038 }
1039
1040 fn start_all_tasks(
1041 &mut self,
1042 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1043 ) -> CuResult<()> {
1044 if self.fail_start {
1045 return Err("mock sim start failure".into());
1046 }
1047 let _ = sim_callback(MockStep);
1048 self.started += 1;
1049 Ok(())
1050 }
1051
1052 fn run_one_iteration(
1053 &mut self,
1054 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1055 ) -> CuResult<()> {
1056 let _ = sim_callback(MockStep);
1057 self.iterations += 1;
1058 Ok(())
1059 }
1060
1061 fn run(
1062 &mut self,
1063 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1064 ) -> CuResult<()> {
1065 let _ = sim_callback(MockStep);
1066 Ok(())
1067 }
1068
1069 fn stop_all_tasks(
1070 &mut self,
1071 sim_callback: &mut impl for<'z> FnMut(Self::Step<'z>) -> SimOverride,
1072 ) -> CuResult<()> {
1073 let _ = sim_callback(MockStep);
1074 self.stopped += 1;
1075 Ok(())
1076 }
1077
1078 fn restore_keyframe(&mut self, _freezer: &KeyFrame) -> CuResult<()> {
1079 Ok(())
1080 }
1081 }
1082
1083 type SimLifecycle = CuStdSimAppLifecycle<MockSimApp>;
1084
1085 #[test]
1086 fn sim_full_cycle_threads_the_callback() {
1087 let mut callback_calls = 0u32;
1088 let mut cb = |_step: MockStep| -> SimOverride {
1089 callback_calls += 1;
1090 SimOverride::ExecuteByRuntime
1091 };
1092
1093 let app = SimLifecycle::new(MockSimApp::default());
1094 let mut running = app.start(&mut cb).unwrap();
1095 running.run_one_iteration(&mut cb).unwrap();
1096 let stopped = running.stop(&mut cb).unwrap();
1097
1098 let mock = stopped.into_inner();
1099 assert_eq!(mock.started, 1);
1100 assert_eq!(mock.iterations, 1);
1101 assert_eq!(mock.stopped, 1);
1102 assert_eq!(callback_calls, 3);
1103 }
1104
1105 #[test]
1106 fn sim_failed_start_hands_back_a_faulted_app_for_cleanup() {
1107 let mut cb = |_step: MockStep| -> SimOverride { SimOverride::ExecuteByRuntime };
1108 let app = SimLifecycle::new(MockSimApp {
1109 fail_start: true,
1110 ..Default::default()
1111 });
1112 let err = app.start(&mut cb).unwrap_err();
1113 assert!(err.error.to_string().contains("mock sim start failure"));
1114 let stopped = err.app.stop(&mut cb).unwrap();
1115 assert_eq!(stopped.inner().stopped, 1);
1116 }
1117}