Skip to main content

des_sim/execution/runner/instance/
standard.rs

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