ixa/
context.rs

1//! A manager for the state of a discrete-event simulation
2//!
3//! Defines a [`Context`] that is intended to provide the foundational mechanism
4//! for storing and manipulating the state of a given simulation.
5use std::any::{Any, TypeId};
6use std::cell::OnceCell;
7use std::collections::VecDeque;
8use std::fmt::{Display, Formatter};
9use std::rc::Rc;
10
11use crate::data_plugin::DataPlugin;
12use crate::entity::entity_store::EntityStore;
13use crate::entity::property::Property;
14use crate::entity::property_value_store_core::PropertyValueStoreCore;
15use crate::entity::Entity;
16use crate::execution_stats::{
17    log_execution_statistics, print_execution_statistics, ExecutionProfilingCollector,
18    ExecutionStatistics,
19};
20use crate::plan::{PlanId, Queue};
21#[cfg(feature = "progress_bar")]
22use crate::progress::update_timeline_progress;
23use crate::{get_data_plugin_count, trace, warn, HashMap, HashMapExt};
24
25/// The common callback used by multiple [`Context`] methods for future events
26type Callback = dyn FnOnce(&mut Context);
27
28/// A handler for an event type `E`
29type EventHandler<E> = dyn Fn(&mut Context, E);
30
31pub trait IxaEvent {
32    /// Called every time `context.subscribe_to_event` is called with this event
33    fn on_subscribe(_context: &mut Context) {}
34}
35
36/// An enum to indicate the phase for plans at a given time.
37///
38/// Most plans will occur as `Normal`. Plans with phase `First` are
39/// handled before all `Normal` plans, and those with phase `Last` are
40/// handled after all `Normal` plans. In all cases ties between plans at the
41/// same time and with the same phase are handled in the order of scheduling.
42///
43#[derive(PartialEq, Eq, Ord, Clone, Copy, PartialOrd, Hash, Debug)]
44pub enum ExecutionPhase {
45    First,
46    Normal,
47    Last,
48}
49
50impl Display for ExecutionPhase {
51    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
52        write!(f, "{self:?}")
53    }
54}
55
56/// A manager for the state of a discrete-event simulation
57///
58/// Provides core simulation services including
59/// * Maintaining a notion of time
60/// * Scheduling events to occur at some point in the future and executing them
61///   at that time
62/// * Holding data that can be accessed by simulation modules
63///
64/// Simulations are constructed out of a series of interacting modules that
65/// take turns manipulating the [`Context`] through a mutable reference. Modules
66/// store data in the simulation using the [`DataPlugin`] trait that allows them
67/// to retrieve data by type.
68///
69/// The future event list of the simulation is a queue of `Callback` objects -
70/// called `plans` - that will assume control of the [`Context`] at a future point
71/// in time and execute the logic in the associated `FnOnce(&mut Context)`
72/// closure. Modules can add plans to this queue through the [`Context`].
73///
74/// The simulation also has a separate callback mechanism. Callbacks
75/// fire before the next timed event (even if it is scheduled for the
76/// current time). This allows modules to schedule actions for immediate
77/// execution but outside of the current iteration of the event loop.
78///
79/// Modules can also emit 'events' that other modules can subscribe to handle by
80/// event type. This allows modules to broadcast that specific things have
81/// occurred and have other modules take turns reacting to these occurrences.
82///
83pub struct Context {
84    plan_queue: Queue<Box<Callback>, ExecutionPhase>,
85    callback_queue: VecDeque<Box<Callback>>,
86    event_handlers: HashMap<TypeId, Box<dyn Any>>,
87    pub(crate) entity_store: EntityStore,
88    data_plugins: Vec<OnceCell<Box<dyn Any>>>,
89    current_time: Option<f64>,
90    start_time: Option<f64>,
91    shutdown_requested: bool,
92    execution_profiler: ExecutionProfilingCollector,
93    pub(crate) print_execution_statistics: bool,
94}
95
96impl Context {
97    /// Create a new empty `Context`
98    #[must_use]
99    pub fn new() -> Context {
100        // Create a vector to accommodate all registered data plugins
101        let data_plugins = std::iter::repeat_with(OnceCell::new)
102            .take(get_data_plugin_count())
103            .collect();
104
105        Context {
106            plan_queue: Queue::new(),
107            callback_queue: VecDeque::new(),
108            event_handlers: HashMap::new(),
109            entity_store: EntityStore::new(),
110            data_plugins,
111            current_time: None,
112            start_time: None,
113            shutdown_requested: false,
114            execution_profiler: ExecutionProfilingCollector::new(),
115            print_execution_statistics: false,
116        }
117    }
118
119    pub(crate) fn get_property_value_store<E: Entity, P: Property<E>>(
120        &self,
121    ) -> &PropertyValueStoreCore<E, P> {
122        self.entity_store.get_property_store::<E>().get::<P>()
123    }
124    pub(crate) fn get_property_value_store_mut<E: Entity, P: Property<E>>(
125        &mut self,
126    ) -> &mut PropertyValueStoreCore<E, P> {
127        self.entity_store
128            .get_property_store_mut::<E>()
129            .get_mut::<P>()
130    }
131
132    /// Register to handle emission of events of type E
133    ///
134    /// Handlers will be called upon event emission in order of subscription as
135    /// queued `Callback`s with the appropriate event.
136    #[allow(clippy::missing_panics_doc)]
137    pub fn subscribe_to_event<E: IxaEvent + Copy + 'static>(
138        &mut self,
139        handler: impl Fn(&mut Context, E) + 'static,
140    ) {
141        let handler_vec = self
142            .event_handlers
143            .entry(TypeId::of::<E>())
144            .or_insert_with(|| Box::<Vec<Rc<EventHandler<E>>>>::default());
145        let handler_vec: &mut Vec<Rc<EventHandler<E>>> = handler_vec.downcast_mut().unwrap();
146        handler_vec.push(Rc::new(handler));
147        E::on_subscribe(self);
148    }
149
150    /// Emit an event of type E to be handled by registered receivers
151    ///
152    /// Receivers will handle events in the order that they have subscribed and
153    /// are queued as callbacks
154    #[allow(clippy::missing_panics_doc)]
155    pub fn emit_event<E: IxaEvent + Copy + 'static>(&mut self, event: E) {
156        // Destructure to obtain event handlers and plan queue
157        let Context {
158            event_handlers,
159            callback_queue,
160            ..
161        } = self;
162        if let Some(handler_vec) = event_handlers.get(&TypeId::of::<E>()) {
163            let handler_vec: &Vec<Rc<EventHandler<E>>> = handler_vec.downcast_ref().unwrap();
164            for handler in handler_vec {
165                let handler_clone = Rc::clone(handler);
166                callback_queue.push_back(Box::new(move |context| handler_clone(context, event)));
167            }
168        }
169    }
170
171    /// Add a plan to the future event list at the specified time in the normal
172    /// phase
173    ///
174    /// Returns a [`PlanId`] for the newly-added plan that can be used to cancel it
175    /// if needed.
176    /// # Panics
177    ///
178    /// Panics if time is in the past, infinite, or NaN.
179    pub fn add_plan(&mut self, time: f64, callback: impl FnOnce(&mut Context) + 'static) -> PlanId {
180        self.add_plan_with_phase(time, callback, ExecutionPhase::Normal)
181    }
182
183    /// Add a plan to the future event list at the specified time and with the
184    /// specified phase (first, normal, or last among plans at the
185    /// specified time)
186    ///
187    /// Returns a [`PlanId`] for the newly-added plan that can be used to cancel it
188    /// if needed.
189    /// # Panics
190    ///
191    /// Panics if time is in the past, infinite, or NaN.
192    pub fn add_plan_with_phase(
193        &mut self,
194        time: f64,
195        callback: impl FnOnce(&mut Context) + 'static,
196        phase: ExecutionPhase,
197    ) -> PlanId {
198        let current = self.get_current_time();
199        assert!(!time.is_nan(), "Time {time} is invalid: cannot be NaN");
200        assert!(
201            !time.is_infinite(),
202            "Time {time} is invalid: cannot be infinite"
203        );
204        assert!(
205            time >= current,
206            "Time {time} is invalid: cannot be less than the current time ({}). Consider calling set_start_time() before scheduling plans.",
207            current
208        );
209        self.plan_queue.add_plan(time, Box::new(callback), phase)
210    }
211
212    fn evaluate_periodic_and_schedule_next(
213        &mut self,
214        period: f64,
215        callback: impl Fn(&mut Context) + 'static,
216        phase: ExecutionPhase,
217    ) {
218        trace!(
219            "evaluate periodic at {} (period={})",
220            self.get_current_time(),
221            period
222        );
223        callback(self);
224        if !self.plan_queue.is_empty() {
225            let next_time = self.get_current_time() + period;
226            self.add_plan_with_phase(
227                next_time,
228                move |context| context.evaluate_periodic_and_schedule_next(period, callback, phase),
229                phase,
230            );
231        }
232    }
233
234    /// Add a plan with specified priority to the future event list, and
235    /// continuously repeat the plan at the specified period, stopping
236    /// only once there are no other plans scheduled.
237    ///
238    /// Notes:
239    /// * The first periodic plan is scheduled at time `0.0`. If `set_start_time` was
240    ///   set to a positive value, this will currently panic because the first plan
241    ///   occurs before the start time (see issue #634 for future behavior).
242    ///
243    /// # Panics
244    ///
245    /// Panics if plan period is negative, infinite, or NaN.
246    pub fn add_periodic_plan_with_phase(
247        &mut self,
248        period: f64,
249        callback: impl Fn(&mut Context) + 'static,
250        phase: ExecutionPhase,
251    ) {
252        assert!(
253            period > 0.0 && !period.is_nan() && !period.is_infinite(),
254            "Period must be greater than 0"
255        );
256
257        self.add_plan_with_phase(
258            0.0,
259            move |context| context.evaluate_periodic_and_schedule_next(period, callback, phase),
260            phase,
261        );
262    }
263
264    /// Cancel a plan that has been added to the queue
265    ///
266    /// # Panics
267    ///
268    /// This function panics if you cancel a plan which has already been
269    /// cancelled or executed.
270    pub fn cancel_plan(&mut self, plan_id: &PlanId) {
271        trace!("canceling plan {plan_id:?}");
272        let result = self.plan_queue.cancel_plan(plan_id);
273        if result.is_none() {
274            warn!("Tried to cancel nonexistent plan with ID = {plan_id:?}");
275        }
276    }
277
278    #[doc(hidden)]
279    #[allow(dead_code)]
280    pub(crate) fn remaining_plan_count(&self) -> usize {
281        self.plan_queue.remaining_plan_count()
282    }
283
284    /// Add a `Callback` to the queue to be executed before the next plan
285    pub fn queue_callback(&mut self, callback: impl FnOnce(&mut Context) + 'static) {
286        trace!("queuing callback");
287        self.callback_queue.push_back(Box::new(callback));
288    }
289
290    /// Retrieve a mutable reference to the data container associated with a
291    /// [`DataPlugin`]
292    ///
293    /// If the data container has not been already added to the [`Context`] then
294    /// this function will use the [`DataPlugin::init`] method
295    /// to construct a new data container and store it in the [`Context`].
296    ///
297    /// Returns a mutable reference to the data container
298    #[must_use]
299    #[allow(clippy::needless_pass_by_value)]
300    pub fn get_data_mut<T: DataPlugin>(&mut self, _data_plugin: T) -> &mut T::DataContainer {
301        let index = T::index_within_context();
302
303        // If the data plugin is already initialized, return a mutable reference.
304        if self.data_plugins[index].get().is_some() {
305            return self.data_plugins[index]
306                .get_mut()
307                .unwrap()
308                .downcast_mut::<T::DataContainer>()
309                .expect("TypeID does not match data plugin type");
310        }
311
312        // Initialize the data plugin if not already initialized.
313        let data = T::init(self);
314        let cell = self
315            .data_plugins
316            .get_mut(index)
317            .unwrap_or_else(|| panic!("No data plugin found with index = {index:?}. You must use the `define_data_plugin!` macro to create a data plugin."));
318        let _ = cell.set(Box::new(data));
319        cell.get_mut()
320            .unwrap()
321            .downcast_mut::<T::DataContainer>()
322            .expect("TypeID does not match data plugin type. You must use the `define_data_plugin!` macro to create a data plugin.")
323    }
324
325    /// Retrieve a reference to the data container associated with a
326    /// [`DataPlugin`]
327    ///
328    /// Returns a reference to the data container if it exists or else `None`
329    #[must_use]
330    #[allow(clippy::needless_pass_by_value)]
331    pub fn get_data<T: DataPlugin>(&self, _data_plugin: T) -> &T::DataContainer {
332        let index = T::index_within_context();
333        self.data_plugins
334            .get(index)
335            .unwrap_or_else(|| panic!("No data plugin found with index = {index:?}. You must use the `define_data_plugin!` macro to create a data plugin."))
336            .get_or_init(|| Box::new(T::init(self)))
337            .downcast_ref::<T::DataContainer>()
338            .expect("TypeID does not match data plugin type. You must use the `define_data_plugin!` macro to create a data plugin.")
339    }
340
341    /// Shutdown the simulation cleanly, abandoning all events after whatever
342    /// is currently executing.
343    pub fn shutdown(&mut self) {
344        trace!("shutdown context");
345        self.shutdown_requested = true;
346    }
347
348    /// Get the current simulation time
349    ///
350    /// Returns the current time in the simulation. The behavior depends on execution state:
351    /// * During execution: returns the time of the currently executing plan or callback
352    /// * Before execution: returns the start time (if set via [`Context::set_start_time`]), or `0.0`
353    ///
354    /// The time can be negative if a negative start time was set before execution.
355    #[must_use]
356    pub fn get_current_time(&self) -> f64 {
357        self.current_time.or(self.start_time).unwrap_or(0.0)
358    }
359
360    /// Set the start time for the simulation. Must be finite.
361    ///
362    /// * Call before `Context.execute()`.
363    /// * `start_time` must be finite (not NaN or infinite).
364    /// * May be called only once.
365    /// * If plans are already scheduled, `start_time` must be earlier than or equal to
366    ///   the earliest scheduled plan time.
367    ///
368    /// # Panics
369    ///
370    /// Panics if:
371    /// * `start_time` is NaN or infinite.
372    /// * the start time was already set.
373    /// * `Context::execute()` has been called.
374    /// * `start_time` is later than the earliest scheduled plan time.
375    pub fn set_start_time(&mut self, start_time: f64) {
376        assert!(
377            !start_time.is_nan() && !start_time.is_infinite(),
378            "Start time {start_time} must be finite"
379        );
380        assert!(
381            self.start_time.is_none(),
382            "Start time has already been set. It can only be set once."
383        );
384        assert!(
385            self.current_time.is_none(),
386            "Start time cannot be set after execution has begun."
387        );
388        if let Some(next_time) = self.plan_queue.next_time() {
389            assert!(
390                start_time <= next_time,
391                "Start time {} is later than the earliest scheduled plan time {}. Remove or reschedule existing plans first.",
392                start_time,
393                next_time
394            );
395        }
396        self.start_time = Some(start_time);
397    }
398
399    /// Get the start time that was set via `set_start_time`, or `None` if not set.
400    #[must_use]
401    pub fn get_start_time(&self) -> Option<f64> {
402        self.start_time
403    }
404
405    /// Execute the simulation until the plan and callback queues are empty
406    pub fn execute(&mut self) {
407        trace!("entering event loop");
408
409        if self.current_time.is_none() {
410            self.current_time = Some(self.start_time.unwrap_or(0.0));
411        }
412
413        // Start plan loop
414        loop {
415            #[cfg(feature = "progress_bar")]
416            if crate::progress::MAX_TIME.get().is_some() {
417                update_timeline_progress(self.get_current_time());
418            }
419
420            if self.shutdown_requested {
421                self.shutdown_requested = false;
422                break;
423            } else {
424                self.execute_single_step();
425            }
426
427            self.execution_profiler.refresh();
428        }
429
430        let stats = self.get_execution_statistics();
431        if self.print_execution_statistics {
432            print_execution_statistics(&stats);
433            #[cfg(feature = "profiling")]
434            crate::profiling::print_profiling_data();
435        } else {
436            log_execution_statistics(&stats);
437        }
438    }
439
440    /// Executes a single step of the simulation, prioritizing tasks as follows:
441    ///   1. Callbacks
442    ///   2. Plans
443    ///   3. Shutdown
444    pub fn execute_single_step(&mut self) {
445        // If there is a callback, run it.
446        if let Some(callback) = self.callback_queue.pop_front() {
447            trace!("calling callback");
448            callback(self);
449        }
450        // There aren't any callbacks, so look at the first plan.
451        else if let Some(plan) = self.plan_queue.get_next_plan() {
452            trace!("calling plan at {:.6}", plan.time);
453            self.current_time = Some(plan.time);
454            (plan.data)(self);
455        } else {
456            trace!("No callbacks or plans; exiting event loop");
457            // OK, there aren't any plans, so we're done.
458            self.shutdown_requested = true;
459        }
460    }
461
462    pub fn get_execution_statistics(&mut self) -> ExecutionStatistics {
463        #[allow(unused_mut)]
464        let mut stats = self.execution_profiler.compute_final_statistics();
465        #[cfg(feature = "profiling")]
466        {
467            stats.max_plans_in_flight = self.plan_queue.max_plans_in_flight;
468            stats.max_plan_queue_memory_in_use = self.plan_queue.max_memory_in_use;
469        }
470        stats
471    }
472}
473
474pub trait ContextBase: Sized {
475    fn subscribe_to_event<E: IxaEvent + Copy + 'static>(
476        &mut self,
477        handler: impl Fn(&mut Context, E) + 'static,
478    );
479    fn emit_event<E: IxaEvent + Copy + 'static>(&mut self, event: E);
480    fn add_plan(&mut self, time: f64, callback: impl FnOnce(&mut Context) + 'static) -> PlanId;
481    fn add_plan_with_phase(
482        &mut self,
483        time: f64,
484        callback: impl FnOnce(&mut Context) + 'static,
485        phase: ExecutionPhase,
486    ) -> PlanId;
487    fn add_periodic_plan_with_phase(
488        &mut self,
489        period: f64,
490        callback: impl Fn(&mut Context) + 'static,
491        phase: ExecutionPhase,
492    );
493    fn cancel_plan(&mut self, plan_id: &PlanId);
494    fn queue_callback(&mut self, callback: impl FnOnce(&mut Context) + 'static);
495    fn get_data_mut<T: DataPlugin>(&mut self, plugin: T) -> &mut T::DataContainer;
496    fn get_data<T: DataPlugin>(&self, plugin: T) -> &T::DataContainer;
497    fn get_current_time(&self) -> f64;
498    fn get_execution_statistics(&mut self) -> ExecutionStatistics;
499}
500impl ContextBase for Context {
501    delegate::delegate! {
502        to self {
503            fn subscribe_to_event<E: IxaEvent + Copy + 'static>(&mut self, handler: impl Fn(&mut Context, E) + 'static);
504            fn emit_event<E: IxaEvent + Copy + 'static>(&mut self, event: E);
505            fn add_plan(&mut self, time: f64, callback: impl FnOnce(&mut Context) + 'static) -> PlanId;
506            fn add_plan_with_phase(&mut self, time: f64, callback: impl FnOnce(&mut Context) + 'static, phase: ExecutionPhase) -> PlanId;
507            fn add_periodic_plan_with_phase(&mut self, period: f64, callback: impl Fn(&mut Context) + 'static, phase: ExecutionPhase);
508            fn cancel_plan(&mut self, plan_id: &PlanId);
509            fn queue_callback(&mut self, callback: impl FnOnce(&mut Context) + 'static);
510            fn get_data_mut<T: DataPlugin>(&mut self, plugin: T) -> &mut T::DataContainer;
511            fn get_data<T: DataPlugin>(&self, plugin: T) -> &T::DataContainer;
512            fn get_current_time(&self) -> f64;
513            fn get_execution_statistics(&mut self) -> ExecutionStatistics;
514        }
515    }
516}
517
518impl Default for Context {
519    fn default() -> Self {
520        Self::new()
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    // We allow defining items that are never used to test macros.
527    #![allow(dead_code)]
528    use std::cell::RefCell;
529
530    use ixa_derive::IxaEvent;
531
532    use super::*;
533    use crate::{define_data_plugin, define_entity, define_property, ContextEntitiesExt};
534
535    define_data_plugin!(ComponentA, Vec<u32>, vec![]);
536
537    define_entity!(Person);
538
539    define_property!(struct Age(u8), Person);
540
541    define_property!(
542        enum InfectionStatus {
543            Susceptible,
544            Infected,
545            Recovered,
546        },
547        Person,
548        default_const = InfectionStatus::Susceptible
549    );
550
551    define_property!(
552        struct Vaccinated(bool),
553        Person,
554        default_const = Vaccinated(false)
555    );
556
557    #[test]
558    fn empty_context() {
559        let mut context = Context::new();
560        context.execute();
561        assert_eq!(context.get_current_time(), 0.0);
562    }
563
564    #[test]
565    fn get_data() {
566        let mut context = Context::new();
567        context.get_data_mut(ComponentA).push(1);
568        assert_eq!(*context.get_data(ComponentA), vec![1],);
569    }
570
571    fn add_plan(context: &mut Context, time: f64, value: u32) -> PlanId {
572        context.add_plan(time, move |context| {
573            context.get_data_mut(ComponentA).push(value);
574        })
575    }
576
577    fn add_plan_with_phase(
578        context: &mut Context,
579        time: f64,
580        value: u32,
581        phase: ExecutionPhase,
582    ) -> PlanId {
583        context.add_plan_with_phase(
584            time,
585            move |context| {
586                context.get_data_mut(ComponentA).push(value);
587            },
588            phase,
589        )
590    }
591
592    #[test]
593    #[should_panic(expected = "Time inf is invalid")]
594    fn infinite_plan_time() {
595        let mut context = Context::new();
596        add_plan(&mut context, f64::INFINITY, 0);
597    }
598
599    #[test]
600    #[should_panic(expected = "Time NaN is invalid")]
601    fn nan_plan_time() {
602        let mut context = Context::new();
603        add_plan(&mut context, f64::NAN, 0);
604    }
605
606    #[test]
607    fn timed_plan_only() {
608        let mut context = Context::new();
609        add_plan(&mut context, 1.0, 1);
610        context.execute();
611        assert_eq!(context.get_current_time(), 1.0);
612        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
613    }
614
615    #[test]
616    fn callback_only() {
617        let mut context = Context::new();
618        context.queue_callback(|context| {
619            context.get_data_mut(ComponentA).push(1);
620        });
621        context.execute();
622        assert_eq!(context.get_current_time(), 0.0);
623        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
624    }
625
626    #[test]
627    fn callback_before_timed_plan() {
628        let mut context = Context::new();
629        context.queue_callback(|context| {
630            context.get_data_mut(ComponentA).push(1);
631        });
632        add_plan(&mut context, 1.0, 2);
633        context.execute();
634        assert_eq!(context.get_current_time(), 1.0);
635        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
636    }
637
638    #[test]
639    fn callback_adds_timed_plan() {
640        let mut context = Context::new();
641        context.queue_callback(|context| {
642            context.get_data_mut(ComponentA).push(1);
643            add_plan(context, 1.0, 2);
644            context.get_data_mut(ComponentA).push(3);
645        });
646        context.execute();
647        assert_eq!(context.get_current_time(), 1.0);
648        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 3, 2]);
649    }
650
651    #[test]
652    fn callback_adds_callback_and_timed_plan() {
653        let mut context = Context::new();
654        context.queue_callback(|context| {
655            context.get_data_mut(ComponentA).push(1);
656            add_plan(context, 1.0, 2);
657            context.queue_callback(|context| {
658                context.get_data_mut(ComponentA).push(4);
659            });
660            context.get_data_mut(ComponentA).push(3);
661        });
662        context.execute();
663        assert_eq!(context.get_current_time(), 1.0);
664        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 3, 4, 2]);
665    }
666
667    #[test]
668    fn timed_plan_adds_callback_and_timed_plan() {
669        let mut context = Context::new();
670        context.add_plan(1.0, |context| {
671            context.get_data_mut(ComponentA).push(1);
672            // We add the plan first, but the callback will fire first.
673            add_plan(context, 2.0, 3);
674            context.queue_callback(|context| {
675                context.get_data_mut(ComponentA).push(2);
676            });
677        });
678        context.execute();
679        assert_eq!(context.get_current_time(), 2.0);
680        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
681    }
682
683    #[test]
684    fn cancel_plan() {
685        let mut context = Context::new();
686        let to_cancel = add_plan(&mut context, 2.0, 1);
687        context.add_plan(1.0, move |context| {
688            context.cancel_plan(&to_cancel);
689        });
690        context.execute();
691        assert_eq!(context.get_current_time(), 1.0);
692        let test_vec: Vec<u32> = vec![];
693        assert_eq!(*context.get_data_mut(ComponentA), test_vec);
694    }
695
696    #[test]
697    fn add_plan_with_current_time() {
698        let mut context = Context::new();
699        context.add_plan(1.0, move |context| {
700            context.get_data_mut(ComponentA).push(1);
701            add_plan(context, 1.0, 2);
702            context.queue_callback(|context| {
703                context.get_data_mut(ComponentA).push(3);
704            });
705        });
706        context.execute();
707        assert_eq!(context.get_current_time(), 1.0);
708        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 3, 2]);
709    }
710
711    #[test]
712    fn plans_at_same_time_fire_in_order() {
713        let mut context = Context::new();
714        add_plan(&mut context, 1.0, 1);
715        add_plan(&mut context, 1.0, 2);
716        context.execute();
717        assert_eq!(context.get_current_time(), 1.0);
718        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
719    }
720
721    #[test]
722    fn check_plan_phase_ordering() {
723        assert!(ExecutionPhase::First < ExecutionPhase::Normal);
724        assert!(ExecutionPhase::Normal < ExecutionPhase::Last);
725    }
726
727    #[test]
728    fn plans_at_same_time_follow_phase() {
729        let mut context = Context::new();
730        add_plan(&mut context, 1.0, 1);
731        add_plan_with_phase(&mut context, 1.0, 5, ExecutionPhase::Last);
732        add_plan_with_phase(&mut context, 1.0, 3, ExecutionPhase::First);
733        add_plan(&mut context, 1.0, 2);
734        add_plan_with_phase(&mut context, 1.0, 6, ExecutionPhase::Last);
735        add_plan_with_phase(&mut context, 1.0, 4, ExecutionPhase::First);
736        context.execute();
737        assert_eq!(context.get_current_time(), 1.0);
738        assert_eq!(*context.get_data_mut(ComponentA), vec![3, 4, 1, 2, 5, 6]);
739    }
740
741    #[derive(Copy, Clone, IxaEvent)]
742    struct Event1 {
743        pub data: usize,
744    }
745
746    #[derive(Copy, Clone, IxaEvent)]
747    struct Event2 {
748        pub data: usize,
749    }
750
751    #[test]
752    fn simple_event() {
753        let mut context = Context::new();
754        let obs_data = Rc::new(RefCell::new(0));
755        let obs_data_clone = Rc::clone(&obs_data);
756
757        context.subscribe_to_event::<Event1>(move |_, event| {
758            *obs_data_clone.borrow_mut() = event.data;
759        });
760
761        context.emit_event(Event1 { data: 1 });
762        context.execute();
763        assert_eq!(*obs_data.borrow(), 1);
764    }
765
766    #[test]
767    fn multiple_events() {
768        let mut context = Context::new();
769        let obs_data = Rc::new(RefCell::new(0));
770        let obs_data_clone = Rc::clone(&obs_data);
771
772        context.subscribe_to_event::<Event1>(move |_, event| {
773            *obs_data_clone.borrow_mut() += event.data;
774        });
775
776        context.emit_event(Event1 { data: 1 });
777        context.emit_event(Event1 { data: 2 });
778        context.execute();
779
780        // Both of these should have been received.
781        assert_eq!(*obs_data.borrow(), 3);
782    }
783
784    #[test]
785    fn multiple_event_handlers() {
786        let mut context = Context::new();
787        let obs_data1 = Rc::new(RefCell::new(0));
788        let obs_data1_clone = Rc::clone(&obs_data1);
789        let obs_data2 = Rc::new(RefCell::new(0));
790        let obs_data2_clone = Rc::clone(&obs_data2);
791
792        context.subscribe_to_event::<Event1>(move |_, event| {
793            *obs_data1_clone.borrow_mut() = event.data;
794        });
795        context.subscribe_to_event::<Event1>(move |_, event| {
796            *obs_data2_clone.borrow_mut() = event.data;
797        });
798        context.emit_event(Event1 { data: 1 });
799        context.execute();
800        assert_eq!(*obs_data1.borrow(), 1);
801        assert_eq!(*obs_data2.borrow(), 1);
802    }
803
804    #[test]
805    fn multiple_event_types() {
806        let mut context = Context::new();
807        let obs_data1 = Rc::new(RefCell::new(0));
808        let obs_data1_clone = Rc::clone(&obs_data1);
809        let obs_data2 = Rc::new(RefCell::new(0));
810        let obs_data2_clone = Rc::clone(&obs_data2);
811
812        context.subscribe_to_event::<Event1>(move |_, event| {
813            *obs_data1_clone.borrow_mut() = event.data;
814        });
815        context.subscribe_to_event::<Event2>(move |_, event| {
816            *obs_data2_clone.borrow_mut() = event.data;
817        });
818        context.emit_event(Event1 { data: 1 });
819        context.emit_event(Event2 { data: 2 });
820        context.execute();
821        assert_eq!(*obs_data1.borrow(), 1);
822        assert_eq!(*obs_data2.borrow(), 2);
823    }
824
825    #[test]
826    fn subscribe_after_event() {
827        let mut context = Context::new();
828        let obs_data = Rc::new(RefCell::new(0));
829        let obs_data_clone = Rc::clone(&obs_data);
830
831        context.emit_event(Event1 { data: 1 });
832        context.subscribe_to_event::<Event1>(move |_, event| {
833            *obs_data_clone.borrow_mut() = event.data;
834        });
835
836        context.execute();
837        assert_eq!(*obs_data.borrow(), 0);
838    }
839
840    #[test]
841    fn shutdown_cancels_plans() {
842        let mut context = Context::new();
843        add_plan(&mut context, 1.0, 1);
844        context.add_plan(1.5, Context::shutdown);
845        add_plan(&mut context, 2.0, 2);
846        context.execute();
847        assert_eq!(context.get_current_time(), 1.5);
848        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
849    }
850
851    #[test]
852    fn shutdown_cancels_callbacks() {
853        let mut context = Context::new();
854        add_plan(&mut context, 1.0, 1);
855        context.add_plan(1.5, |context| {
856            // Note that we add the callback *before* we call shutdown
857            // but shutdown cancels everything.
858            context.queue_callback(|context| {
859                context.get_data_mut(ComponentA).push(3);
860            });
861            context.shutdown();
862        });
863        context.execute();
864        assert_eq!(context.get_current_time(), 1.5);
865        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
866    }
867
868    #[test]
869    fn shutdown_cancels_events() {
870        let mut context = Context::new();
871        let obs_data = Rc::new(RefCell::new(0));
872        let obs_data_clone = Rc::clone(&obs_data);
873        context.subscribe_to_event::<Event1>(move |_, event| {
874            *obs_data_clone.borrow_mut() = event.data;
875        });
876        context.emit_event(Event1 { data: 1 });
877        context.shutdown();
878        context.execute();
879        assert_eq!(*obs_data.borrow(), 0);
880    }
881
882    #[test]
883    #[allow(clippy::cast_sign_loss)]
884    #[allow(clippy::cast_possible_truncation)]
885    fn periodic_plan_self_schedules() {
886        // checks whether the person properties report schedules itself
887        // based on whether there are plans in the queue
888        let mut context = Context::new();
889        context.add_periodic_plan_with_phase(
890            1.0,
891            |context| {
892                let time = context.get_current_time();
893                context.get_data_mut(ComponentA).push(time as u32);
894            },
895            ExecutionPhase::Last,
896        );
897        context.add_plan(1.0, move |_context| {});
898        context.add_plan(1.5, move |_context| {});
899        context.execute();
900        assert_eq!(context.get_current_time(), 2.0);
901
902        assert_eq!(*context.get_data(ComponentA), vec![0, 1, 2]); // time 0.0, 1.0, and 2.0
903    }
904
905    // Tests for negative time handling
906
907    #[test]
908    fn negative_plan_time_allowed() {
909        let mut context = Context::new();
910        context.set_start_time(-1.0);
911        add_plan(&mut context, -1.0, 1);
912        context.execute();
913        assert_eq!(context.get_current_time(), -1.0);
914        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
915    }
916
917    #[test]
918    fn add_plan_get_current_time() {
919        let mut context = Context::new();
920        let current_time = context.get_current_time();
921        add_plan(&mut context, current_time, 1);
922        context.execute();
923        assert_eq!(context.get_current_time(), 0.0);
924        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
925    }
926
927    #[test]
928    fn multiple_negative_plans() {
929        let mut context = Context::new();
930        context.set_start_time(-3.0);
931        add_plan(&mut context, -3.0, 1);
932        add_plan(&mut context, -1.0, 3);
933        add_plan(&mut context, -2.0, 2);
934        context.execute();
935        assert_eq!(context.get_current_time(), -1.0);
936        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
937    }
938
939    #[test]
940    fn negative_and_positive_plans() {
941        let mut context = Context::new();
942        context.set_start_time(-1.0);
943        add_plan(&mut context, -1.0, 1);
944        add_plan(&mut context, 1.0, 3);
945        add_plan(&mut context, 0.0, 2);
946        context.execute();
947        assert_eq!(context.get_current_time(), 1.0);
948        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
949    }
950
951    #[test]
952    fn get_current_time_before_execute_defaults() {
953        let mut context = Context::new();
954        assert_eq!(context.get_current_time(), 0.0);
955
956        context.set_start_time(-2.0);
957        assert_eq!(context.get_current_time(), -2.0);
958    }
959
960    #[test]
961    fn get_current_time_initializes_to_zero_when_all_positive() {
962        let mut context = Context::new();
963        let seen_time = Rc::new(RefCell::new(f64::NAN));
964        let seen_time_clone = Rc::clone(&seen_time);
965        context.queue_callback(move |ctx| {
966            *seen_time_clone.borrow_mut() = ctx.get_current_time();
967        });
968        context.execute();
969        assert_eq!(*seen_time.borrow(), 0.0);
970    }
971
972    #[test]
973    fn get_current_time_initializes_to_zero_when_empty() {
974        let mut context = Context::new();
975        context.execute();
976        assert_eq!(context.get_current_time(), 0.0);
977    }
978
979    #[test]
980    fn get_current_time_initializes_to_zero_with_plan() {
981        let mut context = Context::new();
982        add_plan(&mut context, 0.0, 1);
983        context.execute();
984        assert_eq!(context.get_current_time(), 0.0);
985        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
986    }
987
988    #[test]
989    #[should_panic(expected = "Time -1 is invalid")]
990    fn negative_time_from_callback_panics() {
991        let mut context = Context::new();
992        context.queue_callback(|context| {
993            context.get_data_mut(ComponentA).push(1);
994            add_plan(context, -1.0, 2);
995        });
996        add_plan(&mut context, 1.0, 3);
997        context.execute();
998    }
999
1000    #[test]
1001    fn large_negative_time() {
1002        let mut context = Context::new();
1003        context.set_start_time(-1_000_000.0);
1004        add_plan(&mut context, -1_000_000.0, 1);
1005        context.execute();
1006        assert_eq!(context.get_current_time(), -1_000_000.0);
1007        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1008    }
1009
1010    #[test]
1011    fn very_small_negative_time() {
1012        let mut context = Context::new();
1013        context.set_start_time(-1e-10);
1014        add_plan(&mut context, -1e-10, 1);
1015        context.execute();
1016        assert_eq!(context.get_current_time(), -1e-10);
1017        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1018    }
1019
1020    #[test]
1021    fn negative_time_ordering_with_phases() {
1022        let mut context = Context::new();
1023        context.set_start_time(-1.0);
1024        add_plan_with_phase(&mut context, -1.0, 1, ExecutionPhase::Normal);
1025        add_plan_with_phase(&mut context, -1.0, 3, ExecutionPhase::Last);
1026        add_plan_with_phase(&mut context, -1.0, 2, ExecutionPhase::First);
1027        context.execute();
1028        assert_eq!(context.get_current_time(), -1.0);
1029        assert_eq!(*context.get_data_mut(ComponentA), vec![2, 1, 3]);
1030    }
1031
1032    #[test]
1033    #[should_panic(expected = "Time 4 is invalid")]
1034    fn cannot_schedule_plan_before_current_time() {
1035        let mut context = Context::new();
1036        add_plan(&mut context, 5.0, 1);
1037        context.add_plan(5.0, |context| {
1038            // At time 5.0, we cannot schedule a plan at time 4.0
1039            add_plan(context, 4.0, 2);
1040        });
1041        context.execute();
1042    }
1043
1044    #[test]
1045    fn get_current_time_multiple_calls_before_execute() {
1046        let mut context = Context::new();
1047        context.set_start_time(-2.0);
1048        add_plan(&mut context, -2.0, 1);
1049        context.execute();
1050        assert_eq!(context.get_current_time(), -2.0);
1051    }
1052
1053    #[test]
1054    fn negative_plan_can_add_positive_plan() {
1055        let mut context = Context::new();
1056        context.set_start_time(-1.0);
1057        add_plan(&mut context, -1.0, 1);
1058        context.add_plan(-1.0, |context| {
1059            add_plan(context, 2.0, 2);
1060        });
1061        context.execute();
1062        assert_eq!(context.get_current_time(), 2.0);
1063        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
1064    }
1065
1066    #[test]
1067    fn negative_plan_can_schedule_negative_plan() {
1068        let mut context = Context::new();
1069        context.set_start_time(-2.0);
1070        add_plan(&mut context, -2.0, 1);
1071        context.add_plan(-2.0, |context| {
1072            add_plan(context, -1.0, 2);
1073        });
1074        context.execute();
1075        assert_eq!(context.get_current_time(), -1.0);
1076        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
1077    }
1078
1079    #[test]
1080    #[should_panic(expected = "Start time has already been set. It can only be set once.")]
1081    fn set_start_time_only_once() {
1082        let mut context = Context::new();
1083        context.set_start_time(1.0);
1084        context.set_start_time(2.0);
1085    }
1086
1087    // Additional coverage around time and plans
1088
1089    #[test]
1090    #[should_panic(expected = "Start time NaN must be finite")]
1091    fn set_start_time_nan_panics() {
1092        let mut context = Context::new();
1093        context.set_start_time(f64::NAN);
1094    }
1095
1096    #[test]
1097    #[should_panic(expected = "Start time inf must be finite")]
1098    fn set_start_time_inf_panics() {
1099        let mut context = Context::new();
1100        context.set_start_time(f64::INFINITY);
1101    }
1102
1103    #[test]
1104    fn set_start_time_equal_to_earliest_plan_allowed() {
1105        let mut context = Context::new();
1106        context.set_start_time(-2.0);
1107        add_plan(&mut context, -2.0, 1);
1108        context.execute();
1109        assert_eq!(context.get_current_time(), -2.0);
1110        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1111    }
1112
1113    // Note: adding a plan earlier than current_time after setting start time
1114    // is already covered by `add_plan_less_than_current_time_panics`.
1115
1116    #[test]
1117    fn set_start_time_with_only_callbacks_keeps_time() {
1118        let mut context = Context::new();
1119        context.set_start_time(5.0);
1120        context.queue_callback(|ctx| {
1121            ctx.get_data_mut(ComponentA).push(42);
1122        });
1123        context.execute();
1124        assert_eq!(context.get_current_time(), 5.0);
1125        assert_eq!(*context.get_data_mut(ComponentA), vec![42]);
1126    }
1127
1128    #[test]
1129    fn multiple_plans_final_time_is_last() {
1130        let mut context = Context::new();
1131        add_plan(&mut context, 1.0, 1);
1132        add_plan(&mut context, 3.0, 3);
1133        add_plan(&mut context, 2.0, 2);
1134        context.execute();
1135        assert_eq!(context.get_current_time(), 3.0);
1136        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1137    }
1138
1139    #[test]
1140    fn add_plan_same_time_fifo_and_phases() {
1141        let mut context = Context::new();
1142        add_plan_with_phase(&mut context, 1.0, 3, ExecutionPhase::Last);
1143        add_plan(&mut context, 1.0, 1);
1144        add_plan_with_phase(&mut context, 1.0, 2, ExecutionPhase::First);
1145        add_plan(&mut context, 1.0, 4);
1146        context.execute();
1147        assert_eq!(context.get_current_time(), 1.0);
1148        assert_eq!(*context.get_data_mut(ComponentA), vec![2, 1, 4, 3]);
1149    }
1150
1151    #[test]
1152    #[should_panic(expected = "Time -2 is invalid")]
1153    fn add_plan_less_than_current_time_panics() {
1154        let mut context = Context::new();
1155        context.set_start_time(-1.0);
1156        add_plan(&mut context, -1.0, 1);
1157        // Attempt to schedule before current time
1158        add_plan(&mut context, -2.0, 2);
1159    }
1160
1161    #[test]
1162    #[should_panic(expected = "Period must be greater than 0")]
1163    fn add_periodic_plan_zero_period_panics() {
1164        let mut context = Context::new();
1165        context.add_periodic_plan_with_phase(0.0, |_ctx| {}, ExecutionPhase::Normal);
1166    }
1167
1168    #[test]
1169    #[should_panic(expected = "Period must be greater than 0")]
1170    fn add_periodic_plan_nan_panics() {
1171        let mut context = Context::new();
1172        context.add_periodic_plan_with_phase(f64::NAN, |_ctx| {}, ExecutionPhase::Normal);
1173    }
1174
1175    #[test]
1176    #[should_panic(expected = "Period must be greater than 0")]
1177    fn add_periodic_plan_inf_panics() {
1178        let mut context = Context::new();
1179        context.add_periodic_plan_with_phase(f64::INFINITY, |_ctx| {}, ExecutionPhase::Normal);
1180    }
1181
1182    #[test]
1183    fn shutdown_requested_reset() {
1184        // This test verifies that shutdown_requested is properly reset after
1185        // being acted upon. This allows the context to be reused after shutdown.
1186        let mut context = Context::new();
1187        let _: PersonId = context.add_entity((Age(50),)).unwrap();
1188
1189        // Schedule a plan at time 0.0 that calls shutdown
1190        context.add_plan(0.0, |ctx| {
1191            ctx.shutdown();
1192        });
1193
1194        // First execute - should run until shutdown
1195        context.execute();
1196        assert_eq!(context.get_current_time(), 0.0);
1197        assert_eq!(context.get_entity_count::<Person>(), 1);
1198
1199        // Add a new plan at time 2.0
1200        context.add_plan(2.0, |ctx| {
1201            let _: PersonId = ctx.add_entity((Age(50),)).unwrap();
1202        });
1203
1204        // Second execute - should execute the new plan
1205        // If shutdown_requested wasn't reset, this would immediately break
1206        // without executing the plan, leaving population at 1.
1207        context.execute();
1208        assert_eq!(context.get_current_time(), 2.0);
1209        assert_eq!(
1210            context.get_entity_count::<Person>(),
1211            2,
1212            "If this fails, shutdown_requested was not properly reset"
1213        );
1214    }
1215}