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