Skip to main content

des_sim/execution/runner/instance/
realtime.rs

1//! The `realtime` module provides the `RealtimeRunner`, an implementation of the `Runner` trait
2//! that synchronizes simulation progression with real-world time.
3//!
4//! This runner is suitable for scenarios where the simulation needs to interact with external
5//! systems or be visualized at a human-perceptible pace. It skips idle periods to maintain
6//! efficiency while adhering to real-time constraints.
7
8use crate::context::ExecutorStatus;
9use crate::execution::SimulationResult;
10use crate::execution::engine::Engine;
11use crate::execution::phase::MicroStepResult;
12use crate::execution::runner::Runner;
13use crate::execution::strategy::{AlwaysContinueStrategy, ContinueStrategy};
14use crate::modeling::model::Model;
15use crate::primitive::time::TickStatus;
16use std::thread;
17use std::time::{Duration as StdDuration, Instant};
18
19/// A runner that executes the simulation in synchronization with real-world time.
20///
21/// This runner controls the execution timing of each tick based on the `tick_unit_duration`.
22/// Periods without active events or source firings are skipped, allowing for efficient
23/// CPU usage while ensuring the simulation progresses at the correct wall-clock speed.
24///
25/// ### Notes
26/// - To perform periodic processing (e.g., status monitoring), register a periodic `Source`
27///   that does nothing.
28/// - If the implementations of `Model`, `Source`, and `Hook` are deterministic,
29///   this runner guarantees deterministic real-time execution.
30#[derive(Clone)]
31pub struct RealtimeRunner<CS> {
32    continue_strategy: CS,
33    /// The real-world duration allocated for a single tick.
34    tick_unit_duration: StdDuration,
35}
36
37impl<E, M: Model<E>, CS: ContinueStrategy<E, M, ()>> Runner<E, M, CS> for RealtimeRunner<CS> {
38    type Err = ();
39
40    fn run<F>(
41        &mut self,
42        engine: Engine<E, M>,
43        mut model: M,
44        mut should_stop: F,
45    ) -> SimulationResult<M, CS::Err>
46    where
47        F: FnMut(&M, ExecutorStatus, TickStatus) -> bool,
48    {
49        let mut runner_error: Option<CS::Err> = None;
50
51        // Record the start time to serve as the baseline for absolute time synchronization.
52        let start_instant = Instant::now();
53        let mut executor = engine.begin_simulation(&model);
54
55        loop {
56            let (executor_status, tick_status) = executor.peek_next_tick();
57            if should_stop(&model, executor_status, tick_status) {
58                break;
59            }
60
61            // --- Real-time Synchronization Logic ---
62            // Calculate the target elapsed time and sleep until it matches the current wall time.
63            let next_tick_value = tick_status.current().as_time_tick();
64            let target_elapsed = self.tick_unit_duration * next_tick_value as u32;
65            let now = Instant::now();
66            let expected_instant = start_instant + target_elapsed;
67            if now < expected_instant {
68                thread::sleep(expected_instant - now);
69            }
70
71            // ------------------------------
72
73            let mut active_executor = executor.begin_tick(&model);
74
75            loop {
76                // 1. Begin micro-step
77                let micro_step_handler = active_executor.begin_micro_step(&model);
78
79                // 2. Source phase
80                let mut source_phase = micro_step_handler.start_source_phase(&model);
81                while let Some(source_ready) = source_phase.take_one() {
82                    source_phase.fire_and_schedule(&model, source_ready);
83                }
84                let micro_step_handler = source_phase.complete_source_phase(&model);
85
86                // 3. Event phase
87                let mut event_phase = micro_step_handler.to_event_phase(&model);
88                while let Some(event_ready) = event_phase.take_one() {
89                    event_phase.handle_event(&mut model, event_ready);
90                }
91                let micro_step_handler = event_phase.complete_event_phase(&model);
92
93                // 4. End micro-step
94                match micro_step_handler.end_micro_step(&model) {
95                    MicroStepResult::Continue(unchecked) => {
96                        match self
97                            .continue_strategy
98                            .handle_micro_step_continue(&model, unchecked)
99                        {
100                            Ok(new_active_executor) => {
101                                active_executor = new_active_executor;
102                                continue;
103                            }
104                            Err((new_active_executor, error)) => {
105                                active_executor = new_active_executor;
106                                runner_error = Some(error);
107                                break;
108                            }
109                        }
110                    }
111                    MicroStepResult::Complete(new_active_executor, _) => {
112                        active_executor = new_active_executor;
113                        break;
114                    }
115                }
116            }
117
118            // return to executor to prepare for next loop
119            executor = active_executor.end_tick_with_jump_to_next_tick(&model);
120
121            if runner_error.is_some() {
122                break;
123            }
124        }
125
126        if let Some(error) = runner_error.take() {
127            executor.end_simulation_as_error(model, error)
128        } else {
129            executor.end_simulation_as_ok(model)
130        }
131    }
132}
133
134impl RealtimeRunner<AlwaysContinueStrategy> {
135    /// Creates a new `RealtimeRunner` with the `AlwaysContinueStrategy`.
136    ///
137    /// # Arguments
138    /// * `tick_unit_duration` - The real-world duration that corresponds to 1 tick.
139    pub fn new(tick_unit_duration: StdDuration) -> Self {
140        RealtimeRunner {
141            continue_strategy: AlwaysContinueStrategy::new(),
142            tick_unit_duration,
143        }
144    }
145}
146
147impl<CS> RealtimeRunner<CS> {
148    /// Creates a new `RealtimeRunner` with a specific `ContinueStrategy`.
149    pub fn new_with_continue_strategy(
150        tick_unit_duration: StdDuration,
151        continue_strategy: CS,
152    ) -> Self {
153        RealtimeRunner {
154            continue_strategy,
155            tick_unit_duration,
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::context::{EventContext, SourceContext, UserContext};
164    use crate::execution::strategy::LimitAbortStrategy;
165    use crate::modeling::event::{Event, EventPriority};
166    use crate::modeling::hook::Hook;
167    use crate::modeling::hook::instance::SharedHook;
168    use crate::modeling::source::Source;
169    use crate::primitive::time::{Duration, MicroStep, SimTime, TickStatus};
170    use crate::source_handler::{SourceReadyEntry, SourceView};
171    use std::sync::{Arc, Mutex};
172
173    #[derive(Debug, PartialEq, Eq, Clone, Copy)]
174    enum TestEvent {
175        A,
176    }
177
178    #[derive(Debug)]
179    struct TestModel {
180        event_count: usize,
181    }
182
183    impl Model<TestEvent> for TestModel {
184        fn handle_event(
185            &mut self,
186            _context: &mut EventContext<TestEvent, Self>,
187            _event: &Event<TestEvent>,
188        ) {
189            self.event_count += 1;
190        }
191    }
192
193    #[test]
194    fn test_realtime_runner_new() {
195        let runner_always = RealtimeRunner::new(std::time::Duration::from_millis(100));
196        assert_eq!(
197            runner_always.tick_unit_duration,
198            std::time::Duration::from_millis(100)
199        );
200
201        let strategy = AlwaysContinueStrategy::new();
202        let runner_custom = RealtimeRunner::new_with_continue_strategy(
203            std::time::Duration::from_millis(100),
204            strategy,
205        );
206        assert_eq!(
207            runner_custom.tick_unit_duration,
208            std::time::Duration::from_millis(100)
209        );
210    }
211
212    #[test]
213    fn test_realtime_runner_run_success() {
214        let model = TestModel { event_count: 0 };
215        let mut engine = Engine::new();
216
217        // Schedule an event at tick 5 to test processing during the simulation run.
218        engine.schedule_event_at(
219            SimTime::from_ticks(5),
220            EventPriority::minimum(),
221            TestEvent::A,
222        );
223
224        let mut runner = RealtimeRunner::new(std::time::Duration::from_millis(100));
225
226        // Stop condition: terminate after 10 ticks.
227        let should_stop = |_m: &TestModel, _status: ExecutorStatus, tick: TickStatus| {
228            tick.is_done_ticks(false, 10)
229        };
230
231        let result = runner.run(engine, model, should_stop);
232
233        // Verify that the simulation completed successfully and events were processed
234        assert!(result.is_ok());
235        let output = result.unwrap();
236        assert_eq!(output.model().event_count, 1);
237    }
238
239    #[test]
240    fn test_realtime_runner_run_with_strategy_error() {
241        let model = TestModel { event_count: 0 };
242        let mut engine = Engine::new();
243
244        struct TestSource;
245
246        impl Source<TestEvent, TestModel> for TestSource {
247            fn on_registered(
248                &mut self,
249                _context: &mut dyn UserContext<TestEvent, TestModel>,
250                _model: &TestModel,
251            ) -> Option<Duration> {
252                // Register an event to ensure the loop/continuation occurs within the first microstep
253                Some(Duration::zero())
254            }
255
256            fn fire(
257                &mut self,
258                context: &mut SourceContext<TestEvent, TestModel>,
259                _model: &TestModel,
260            ) -> Option<Duration> {
261                // Register events to ensure loops/continuations occur within the same microstep
262                context.schedule_event(Duration::zero(), EventPriority::minimum(), TestEvent::A);
263                Some(Duration::one())
264            }
265        }
266
267        engine.add_source("test source", TestSource);
268
269        // LimitAbortStrategy set to 0 micro-steps to trigger an error immediately.
270        let strategy = LimitAbortStrategy::new(0, 0);
271        let mut runner = RealtimeRunner::new_with_continue_strategy(
272            std::time::Duration::from_millis(100),
273            strategy,
274        );
275
276        // Stop condition with safety to prevent infinite loop (usually exits first on strategy error)
277        let mut loop_count = 0;
278        let should_stop = |_m: &TestModel, _status: ExecutorStatus, _tick: TickStatus| {
279            loop_count += 1;
280            loop_count > 10
281        };
282
283        let result = runner.run(engine, model, should_stop);
284
285        // Verify that the strategy aborted the simulation with an error
286        assert!(result.is_err());
287    }
288
289    #[test]
290    fn test_realtime_runner_run_without_strategy_error() {
291        let model = TestModel { event_count: 0 };
292        let mut engine = Engine::new();
293
294        // Event at tick 0 is processed at initialization, so 0 micro-steps is acceptable here.
295        engine.schedule_event_at(SimTime::zero(), EventPriority::minimum(), TestEvent::A);
296
297        // Input LimitAbortStrategy with microstep upper limit set to "0" and allowable number of times set to "0"
298        // This causes an error to occur immediately on the first Continue judgment
299        let strategy = LimitAbortStrategy::new(0, 0);
300        let mut runner = RealtimeRunner::new_with_continue_strategy(
301            std::time::Duration::from_millis(100),
302            strategy,
303        );
304
305        // Stop condition with safety to prevent infinite loop (usually exits first on strategy error)
306        let mut loop_count = 0;
307        let should_stop = |_m: &TestModel, _status: ExecutorStatus, _tick: TickStatus| {
308            loop_count += 1;
309            loop_count > 10
310        };
311
312        let result = runner.run(engine, model, should_stop);
313
314        // Verify that the strategy did not cause the simulation to fail due to errors.
315        assert!(result.is_ok());
316    }
317
318    // Lifecycle event definition to track call order
319    #[derive(Debug, PartialEq, Eq, Clone)]
320    enum LifecycleEvent {
321        BeforeSimulation,
322        BeforeTick(SimTime),
323        BeforeFireSource(SimTime),
324        BeforeScheduleEvent,
325        AfterScheduleEvent,
326        AfterFireSource(SimTime),
327        AfterTick(SimTime),
328        AfterSimulation,
329    }
330
331    // A dummy source that shares and records trace logs for each event for testing purposes.
332    struct TraceSource {
333        trace: Arc<Mutex<Vec<LifecycleEvent>>>,
334        initial_delay: Duration,
335        interval_delay: Option<Duration>,
336    }
337
338    impl Source<TestEvent, TestModel> for TraceSource {
339        fn on_registered(
340            &mut self,
341            _context: &mut dyn UserContext<TestEvent, TestModel>,
342            _model: &TestModel,
343        ) -> Option<Duration> {
344            self.trace
345                .lock()
346                .unwrap()
347                .push(LifecycleEvent::BeforeSimulation);
348            Some(self.initial_delay)
349        }
350
351        fn fire(
352            &mut self,
353            context: &mut SourceContext<TestEvent, TestModel>,
354            _model: &TestModel,
355        ) -> Option<Duration> {
356            let mut t = self.trace.lock().unwrap();
357            t.push(LifecycleEvent::BeforeFireSource(context.current_tick()));
358            t.push(LifecycleEvent::BeforeScheduleEvent);
359
360            context.schedule_event(Duration::zero(), EventPriority::minimum(), TestEvent::A);
361
362            t.push(LifecycleEvent::AfterScheduleEvent);
363            t.push(LifecycleEvent::AfterFireSource(context.current_tick()));
364            self.interval_delay
365        }
366    }
367
368    #[test]
369    fn test_runner_lifecycle_execution_order_scenario() {
370        let trace = Arc::new(Mutex::new(Vec::new()));
371        let model = TestModel { event_count: 0 };
372        let mut engine = Engine::new();
373
374        // Register the source that fires once at tick 1
375        engine.add_source(
376            "trace_source",
377            TraceSource {
378                trace: Arc::clone(&trace),
379                initial_delay: Duration::ticks(1),
380                interval_delay: None, // singleIgnition
381            },
382        );
383
384        let mut runner = RealtimeRunner::new(std::time::Duration::from_millis(100));
385
386        // Stopping condition: Stop before time 2 (at the peak stage)
387        let should_stop = move |_m: &TestModel, _status: ExecutorStatus, tick: TickStatus| {
388            tick.is_done_ticks(false, 2)
389        };
390
391        let result = runner.run(engine, model, should_stop);
392        assert!(result.is_ok());
393
394        // After exiting the loop, the simulation is safely finished, so we manually record it at the end.
395        trace.lock().unwrap().push(LifecycleEvent::AfterSimulation);
396
397        let final_trace = trace.lock().unwrap();
398
399        // Legal lifecycle order based on actual code flow:
400        // 1. When registering in initialize_sources (BeforeSimulation)
401        // 2. Tick start at time 0 (because there is no event, the internal MicroStep is skipped or ends immediately)
402        // 3. Start Tick at time 1 -> Enter MicroStep loop -> Fire Source (BeforeMicroStep -> BeforeEvent -> AfterEvent -> AfterMicroStep)
403        // 4. Should_stop becomes true at peak time 2 and exits the loop -> AfterSimulation
404        let expected = vec![
405            LifecycleEvent::BeforeSimulation,
406            // Iteration at time 1 (there is no event at time 0, so fire from this source does not pass)
407            LifecycleEvent::BeforeFireSource(SimTime::from_ticks(1)),
408            LifecycleEvent::BeforeScheduleEvent,
409            LifecycleEvent::AfterScheduleEvent,
410            LifecycleEvent::AfterFireSource(SimTime::from_ticks(1)),
411            LifecycleEvent::AfterSimulation,
412        ];
413
414        assert_eq!(*final_trace, expected);
415    }
416
417    #[test]
418    fn test_lifecycle_interruption_on_strategy_error() {
419        let trace = Arc::new(Mutex::new(Vec::new()));
420        let model = TestModel { event_count: 0 };
421        let mut engine = Engine::new();
422
423        // Prepare a source that will definitely fire infinitely at the first tick (time 0)
424        engine.add_source(
425            "loop_source",
426            TraceSource {
427                trace: Arc::clone(&trace),
428                initial_delay: Duration::zero(),
429                // 次のMicroStepに再度発火するよう設定
430                interval_delay: Some(Duration::zero()),
431            },
432        );
433
434        // Strategies to instantly generate upper bound errors for microsteps
435        let strategy = LimitAbortStrategy::new(0, 0);
436        let mut runner = RealtimeRunner::new_with_continue_strategy(
437            std::time::Duration::from_millis(100),
438            strategy,
439        );
440
441        let trace_for_stop = Arc::clone(&trace);
442        let should_stop = move |_m: &TestModel, _status: ExecutorStatus, tick: TickStatus| {
443            trace_for_stop
444                .lock()
445                .unwrap()
446                .push(LifecycleEvent::BeforeTick(tick.current()));
447            false
448        };
449
450        let result = runner.run(engine, model, should_stop);
451
452        // Confirmed abnormal termination due to strategy error
453        assert!(result.is_err());
454
455        let final_trace = trace.lock().unwrap();
456
457        // Even when interrupted due to an error, verify that `AfterMicroStep`, `AfterTick`, and `AfterSimulation`,
458        // which are pairs of started hooks such as `BeforeMicroStep`, detect an abnormality and correctly terminate the flow (or proceed to cleanup).
459        // *This test ensures that the life cycle will not remain in an abnormal state even if a panic/error break occurs midway through.
460        assert!(final_trace.contains(&LifecycleEvent::BeforeSimulation));
461        assert!(final_trace.contains(&LifecycleEvent::BeforeTick(SimTime::zero())));
462        assert!(!final_trace.contains(&LifecycleEvent::BeforeTick(SimTime::from_ticks(1))));
463
464        // Verify that the normal lifecycle event (such as AfterTick) after the microstep
465        // where the error occurred is not executed and the loop is safely exited.
466        let last_event = final_trace.last().unwrap();
467        assert_ne!(last_event, &LifecycleEvent::AfterTick(SimTime::zero()));
468    }
469
470    // An enum that records that the hook was called, along with detailed parameters.
471    #[derive(Debug, PartialEq, Eq, Clone)]
472    enum HookCall {
473        BeforeSimulation,
474        AfterSimulation(SimTime),
475        BeforeTick {
476            current: SimTime,
477            skipped: Duration,
478        },
479        AfterTick {
480            current: SimTime,
481            last_micro: MicroStep,
482        },
483        BeforeMicroStep {
484            current: SimTime,
485            micro: MicroStep,
486        },
487        AfterMicroStep {
488            current: SimTime,
489            micro: MicroStep,
490        },
491        BeforeSourcePhase {
492            current: SimTime,
493            micro: MicroStep,
494        },
495        AfterSourcePhase {
496            current: SimTime,
497            micro: MicroStep,
498        },
499        BeforeEventPhase {
500            current: SimTime,
501            micro: MicroStep,
502        },
503        AfterEventPhase {
504            current: SimTime,
505            micro: MicroStep,
506        },
507    }
508
509    // Hook implementation for testing
510    struct MockHook {
511        calls: Arc<Mutex<Vec<HookCall>>>,
512    }
513
514    impl<E, M: Model<E>> Hook<E, M> for MockHook {
515        fn before_simulation(&self, _model: &M) {
516            self.calls.lock().unwrap().push(HookCall::BeforeSimulation);
517        }
518        fn after_simulation(&self, _model: &M, end_tick: SimTime) {
519            self.calls
520                .lock()
521                .unwrap()
522                .push(HookCall::AfterSimulation(end_tick));
523        }
524        fn before_tick(&self, _model: &M, current_tick: SimTime, skipped_duration: Duration) {
525            self.calls.lock().unwrap().push(HookCall::BeforeTick {
526                current: current_tick,
527                skipped: skipped_duration,
528            });
529        }
530        fn after_tick(&self, _model: &M, current_tick: SimTime, last_micro_step: MicroStep) {
531            self.calls.lock().unwrap().push(HookCall::AfterTick {
532                current: current_tick,
533                last_micro: last_micro_step,
534            });
535        }
536        fn before_micro_step(
537            &self,
538            _model: &M,
539            current_tick: SimTime,
540            current_micro_step: MicroStep,
541        ) {
542            self.calls.lock().unwrap().push(HookCall::BeforeMicroStep {
543                current: current_tick,
544                micro: current_micro_step,
545            });
546        }
547        fn after_micro_step(
548            &self,
549            _model: &M,
550            current_tick: SimTime,
551            current_micro_step: MicroStep,
552        ) {
553            self.calls.lock().unwrap().push(HookCall::AfterMicroStep {
554                current: current_tick,
555                micro: current_micro_step,
556            });
557        }
558        fn on_discard_remain_micro_step(
559            &self,
560            _model: &M,
561            _current_tick: SimTime,
562            _first_discarded_micro_step: MicroStep,
563            _discarded_sources: &[SourceReadyEntry],
564            _discarded_events: &[Event<E>],
565        ) {
566        }
567        fn before_register_source(&self, _model: &M, _name: &str) {}
568        fn after_register_source(&self, _model: &M, _name: &str) {}
569        fn before_source_phase(
570            &self,
571            _model: &M,
572            current_tick: SimTime,
573            current_micro_step: MicroStep,
574        ) {
575            self.calls
576                .lock()
577                .unwrap()
578                .push(HookCall::BeforeSourcePhase {
579                    current: current_tick,
580                    micro: current_micro_step,
581                });
582        }
583        fn before_source(
584            &self,
585            _model: &M,
586            _current_tick: SimTime,
587            _current_micro_step: MicroStep,
588            _source_view: &SourceView,
589        ) {
590        }
591        fn after_source(
592            &self,
593            _model: &M,
594            _current_tick: SimTime,
595            _current_micro_step: MicroStep,
596            _source_view: &SourceView,
597            _computed_next_fire: Option<SimTime>,
598        ) {
599        }
600        fn cancel_source(
601            &self,
602            _model: &M,
603            _current_tick: SimTime,
604            _current_micro_step: MicroStep,
605            _scheduled_at: SimTime,
606            _source_view: &SourceView,
607        ) {
608        }
609        fn discard_source(
610            &self,
611            _model: &M,
612            _current_tick: SimTime,
613            _current_micro_step: MicroStep,
614            _source_view: &SourceView,
615        ) {
616        }
617        fn after_source_phase(
618            &self,
619            _model: &M,
620            current_tick: SimTime,
621            current_micro_step: MicroStep,
622        ) {
623            self.calls.lock().unwrap().push(HookCall::AfterSourcePhase {
624                current: current_tick,
625                micro: current_micro_step,
626            });
627        }
628        fn before_event_phase(
629            &self,
630            _model: &M,
631            current_tick: SimTime,
632            current_micro_step: MicroStep,
633        ) {
634            self.calls.lock().unwrap().push(HookCall::BeforeEventPhase {
635                current: current_tick,
636                micro: current_micro_step,
637            });
638        }
639        fn before_event(
640            &self,
641            _model: &M,
642            _current_tick: SimTime,
643            _current_micro_step: MicroStep,
644            _event: &Event<E>,
645        ) {
646        }
647        fn after_event(
648            &self,
649            _model: &M,
650            _current_tick: SimTime,
651            _current_micro_step: MicroStep,
652            _event: &Event<E>,
653        ) {
654        }
655        fn cancel_event(
656            &self,
657            _model: &M,
658            _current_tick: SimTime,
659            _current_micro_step: MicroStep,
660            _scheduled_at: SimTime,
661            _event: &Event<E>,
662        ) {
663        }
664        fn discard_event(
665            &self,
666            _model: &M,
667            _current_tick: SimTime,
668            _current_micro_step: MicroStep,
669            _event: &Event<E>,
670        ) {
671        }
672        fn after_event_phase(
673            &self,
674            _model: &M,
675            current_tick: SimTime,
676            current_micro_step: MicroStep,
677        ) {
678            self.calls.lock().unwrap().push(HookCall::AfterEventPhase {
679                current: current_tick,
680                micro: current_micro_step,
681            });
682        }
683    }
684
685    #[test]
686    fn test_standard_runner_hook_lifecycle_flow_with_include_zero_tick() {
687        let hook = MockHook {
688            calls: Arc::new(Mutex::new(Vec::new())),
689        };
690        let shared_hook = SharedHook::new(hook);
691
692        let model = TestModel { event_count: 0 };
693        let mut engine = Engine::new();
694
695        // Register a Hook to record with MockHook.
696        engine.add_shared_hook(shared_hook.clone());
697
698        // Schedule only one dummy event at tick 1
699        engine.schedule_event_at(
700            SimTime::from_ticks(1),
701            EventPriority::minimum(),
702            TestEvent::A,
703        );
704
705        let mut runner = RealtimeRunner::new(std::time::Duration::from_millis(100));
706
707        // Stopping condition: Ends when processing for 2 times is completed.
708        let should_stop = |_m: &TestModel, _status: ExecutorStatus, tick: TickStatus| {
709            // Since include_zero_tick=true, it ends with 0tick and 1tick.
710            tick.is_done_ticks(true, 2)
711        };
712
713        let _result = runner.run(engine, model, should_stop);
714
715        let final_calls = shared_hook.get_ref().calls.lock().unwrap();
716
717        // Expected value array fully compliant with phase nesting within `run`
718        let expected = vec![
719            HookCall::BeforeSimulation,
720            // --- process  of tick 0 ---
721            HookCall::BeforeTick {
722                current: SimTime::from_ticks(0),
723                skipped: Duration::zero(),
724            },
725            HookCall::BeforeMicroStep {
726                current: SimTime::from_ticks(0),
727                micro: MicroStep::zero(),
728            },
729            HookCall::BeforeSourcePhase {
730                current: SimTime::from_ticks(0),
731                micro: MicroStep::zero(),
732            },
733            HookCall::AfterSourcePhase {
734                current: SimTime::from_ticks(0),
735                micro: MicroStep::zero(),
736            },
737            HookCall::BeforeEventPhase {
738                current: SimTime::from_ticks(0),
739                micro: MicroStep::zero(),
740            },
741            HookCall::AfterEventPhase {
742                current: SimTime::from_ticks(0),
743                micro: MicroStep::zero(),
744            },
745            HookCall::AfterMicroStep {
746                current: SimTime::from_ticks(0),
747                micro: MicroStep::zero(),
748            },
749            HookCall::AfterTick {
750                current: SimTime::from_ticks(0),
751                last_micro: MicroStep::zero(),
752            },
753            // --- process of tick 1 ---
754            HookCall::BeforeTick {
755                current: SimTime::from_ticks(1),
756                skipped: Duration::ticks(0),
757            },
758            HookCall::BeforeMicroStep {
759                current: SimTime::from_ticks(1),
760                micro: MicroStep::zero(),
761            },
762            HookCall::BeforeSourcePhase {
763                current: SimTime::from_ticks(1),
764                micro: MicroStep::zero(),
765            },
766            HookCall::AfterSourcePhase {
767                current: SimTime::from_ticks(1),
768                micro: MicroStep::zero(),
769            },
770            HookCall::BeforeEventPhase {
771                current: SimTime::from_ticks(1),
772                micro: MicroStep::zero(),
773            },
774            // *Event A is actually processed here.
775            HookCall::AfterEventPhase {
776                current: SimTime::from_ticks(1),
777                micro: MicroStep::zero(),
778            },
779            HookCall::AfterMicroStep {
780                current: SimTime::from_ticks(1),
781                micro: MicroStep::zero(),
782            },
783            HookCall::AfterTick {
784                current: SimTime::from_ticks(1),
785                last_micro: MicroStep::zero(),
786            },
787            // Just before reaching time 2, should_stop becomes true and exits the loop.
788            HookCall::AfterSimulation(SimTime::from_ticks(1)),
789        ];
790
791        assert_eq!(*final_calls, expected);
792    }
793
794    #[test]
795    fn test_standard_runner_hook_lifecycle_flow_without_include_zero_tick() {
796        let hook = MockHook {
797            calls: Arc::new(Mutex::new(Vec::new())),
798        };
799        let shared_hook = SharedHook::new(hook);
800
801        let model = TestModel { event_count: 0 };
802        let mut engine = Engine::new();
803
804        // Register a Hook to record with MockHook.
805        engine.add_shared_hook(shared_hook.clone());
806
807        // Schedule only one dummy event at time 1
808        engine.schedule_event_at(
809            SimTime::from_ticks(1),
810            EventPriority::minimum(),
811            TestEvent::A,
812        );
813
814        let mut runner = RealtimeRunner::new(std::time::Duration::from_millis(100));
815
816        // Stopping condition: Ends when processing for 2 tick is completed.
817        let should_stop = |_m: &TestModel, _status: ExecutorStatus, tick: TickStatus| {
818            // Since include_zero_tick=false, it ends with 0 tick, 1 tick, and 2 tick.
819            tick.is_done_ticks(false, 2)
820        };
821
822        let _result = runner.run(engine, model, should_stop);
823
824        let final_calls = shared_hook.get_ref().calls.lock().unwrap();
825
826        // Expected value array fully compliant with phase nesting within `run`
827        let expected = vec![
828            HookCall::BeforeSimulation,
829            // --- process of tick 0 ---
830            HookCall::BeforeTick {
831                current: SimTime::from_ticks(0),
832                skipped: Duration::zero(),
833            },
834            HookCall::BeforeMicroStep {
835                current: SimTime::from_ticks(0),
836                micro: MicroStep::zero(),
837            },
838            HookCall::BeforeSourcePhase {
839                current: SimTime::from_ticks(0),
840                micro: MicroStep::zero(),
841            },
842            HookCall::AfterSourcePhase {
843                current: SimTime::from_ticks(0),
844                micro: MicroStep::zero(),
845            },
846            HookCall::BeforeEventPhase {
847                current: SimTime::from_ticks(0),
848                micro: MicroStep::zero(),
849            },
850            HookCall::AfterEventPhase {
851                current: SimTime::from_ticks(0),
852                micro: MicroStep::zero(),
853            },
854            HookCall::AfterMicroStep {
855                current: SimTime::from_ticks(0),
856                micro: MicroStep::zero(),
857            },
858            HookCall::AfterTick {
859                current: SimTime::from_ticks(0),
860                last_micro: MicroStep::zero(),
861            },
862            // --- process of tick 1 ---
863            HookCall::BeforeTick {
864                current: SimTime::from_ticks(1),
865                skipped: Duration::ticks(0),
866            },
867            HookCall::BeforeMicroStep {
868                current: SimTime::from_ticks(1),
869                micro: MicroStep::zero(),
870            },
871            HookCall::BeforeSourcePhase {
872                current: SimTime::from_ticks(1),
873                micro: MicroStep::zero(),
874            },
875            HookCall::AfterSourcePhase {
876                current: SimTime::from_ticks(1),
877                micro: MicroStep::zero(),
878            },
879            HookCall::BeforeEventPhase {
880                current: SimTime::from_ticks(1),
881                micro: MicroStep::zero(),
882            },
883            // *Event A is actually processed here.
884            HookCall::AfterEventPhase {
885                current: SimTime::from_ticks(1),
886                micro: MicroStep::zero(),
887            },
888            HookCall::AfterMicroStep {
889                current: SimTime::from_ticks(1),
890                micro: MicroStep::zero(),
891            },
892            HookCall::AfterTick {
893                current: SimTime::from_ticks(1),
894                last_micro: MicroStep::zero(),
895            },
896            // --- process of tick 2 ---
897            HookCall::BeforeTick {
898                current: SimTime::from_ticks(2),
899                skipped: Duration::zero(),
900            },
901            HookCall::BeforeMicroStep {
902                current: SimTime::from_ticks(2),
903                micro: MicroStep::zero(),
904            },
905            HookCall::BeforeSourcePhase {
906                current: SimTime::from_ticks(2),
907                micro: MicroStep::zero(),
908            },
909            HookCall::AfterSourcePhase {
910                current: SimTime::from_ticks(2),
911                micro: MicroStep::zero(),
912            },
913            HookCall::BeforeEventPhase {
914                current: SimTime::from_ticks(2),
915                micro: MicroStep::zero(),
916            },
917            HookCall::AfterEventPhase {
918                current: SimTime::from_ticks(2),
919                micro: MicroStep::zero(),
920            },
921            HookCall::AfterMicroStep {
922                current: SimTime::from_ticks(2),
923                micro: MicroStep::zero(),
924            },
925            HookCall::AfterTick {
926                current: SimTime::from_ticks(2),
927                last_micro: MicroStep::zero(),
928            },
929            // Just before reaching time 3, should_stop becomes true and exits the loop.
930            HookCall::AfterSimulation(SimTime::from_ticks(2)),
931        ];
932
933        assert_eq!(*final_calls, expected);
934    }
935}