ixa/entity/
context_extension.rs

1use std::hash::Hash;
2
3use smallvec::SmallVec;
4
5use crate::entity::entity_set::{EntitySet, EntitySetIterator, SourceSet};
6use crate::entity::events::{EntityCreatedEvent, PartialPropertyChangeEventBox};
7use crate::entity::index::{IndexCountResult, IndexSetResult, PropertyIndexType};
8use crate::entity::property::Property;
9use crate::entity::property_list::{PropertyInitializationList, PropertyList};
10use crate::entity::query::Query;
11use crate::entity::value_change_counter::StratifiedValueChangeCounter;
12use crate::entity::{Entity, EntityId, PopulationIterator};
13use crate::rand::Rng;
14use crate::random::sample_multiple_from_known_length;
15use crate::{warn, Context, ContextRandomExt, ExecutionPhase, IxaError, RngId};
16
17fn handle_periodic_value_change_count_event<E, PL, P, F>(
18    context: &mut Context,
19    period: f64,
20    counter_id: usize,
21    handler: F,
22) where
23    E: Entity,
24    PL: PropertyList<E> + Eq + Hash,
25    P: Property<E> + Eq + Hash,
26    F: Fn(&mut Context, &mut StratifiedValueChangeCounter<E, PL, P>) + 'static,
27{
28    let mut counter = {
29        let property_value_store = context.get_property_value_store_mut::<E, P>();
30        let slot = property_value_store
31            .value_change_counters
32            .get_mut(counter_id)
33            .unwrap_or_else(|| {
34                panic!(
35                    "No value change counter found for property {} with counter_id {}",
36                    P::name(),
37                    counter_id
38                )
39            });
40        std::mem::replace(
41            slot.get_mut(),
42            Box::new(StratifiedValueChangeCounter::<E, PL, P>::new()),
43        )
44    };
45
46    {
47        let counter = counter
48            .as_any_mut()
49            .downcast_mut::<StratifiedValueChangeCounter<E, PL, P>>()
50            .unwrap_or_else(|| {
51                panic!(
52                    "Value change counter for property {} and counter_id {} had unexpected type",
53                    P::name(),
54                    counter_id
55                )
56            });
57
58        handler(context, counter);
59        counter.clear();
60    }
61
62    {
63        let property_value_store = context.get_property_value_store_mut::<E, P>();
64        let slot = property_value_store
65            .value_change_counters
66            .get_mut(counter_id)
67            .unwrap_or_else(|| {
68                panic!(
69                    "No value change counter found for property {} with counter_id {}",
70                    P::name(),
71                    counter_id
72                )
73            });
74
75        // Swap back the cleared counter to retain its allocated capacity.
76        let _ = std::mem::replace(slot.get_mut(), counter);
77    }
78
79    if context.remaining_plan_count() == 0 {
80        return;
81    }
82
83    let next_time = context.get_current_time() + period;
84    context.add_plan_with_phase(
85        next_time,
86        move |context| {
87            handle_periodic_value_change_count_event::<E, PL, P, F>(
88                context, period, counter_id, handler,
89            );
90        },
91        ExecutionPhase::Last,
92    );
93}
94
95/// A trait extension for [`Context`] that exposes entity-related
96/// functionality.
97pub trait ContextEntitiesExt {
98    fn add_entity<E: Entity, PL: PropertyInitializationList<E>>(
99        &mut self,
100        property_list: PL,
101    ) -> Result<EntityId<E>, IxaError>;
102
103    /// Fetches the property value set for the given `entity_id`.
104    ///
105    /// The easiest way to call this method is by assigning it to a variable with an explicit type:
106    /// ```rust, ignore
107    /// let vaccine_status: VaccineStatus = context.get_property(entity_id);
108    /// ```
109    fn get_property<E: Entity, P: Property<E>>(&self, entity_id: EntityId<E>) -> P;
110
111    /// Sets the value of the given property. This method unconditionally emits a `PropertyChangeEvent`.
112    fn set_property<E: Entity, P: Property<E>>(
113        &mut self,
114        entity_id: EntityId<E>,
115        property_value: P,
116    );
117
118    /// Enables full indexing of property values for the property `P`.
119    ///
120    /// This method is called with the turbo-fish syntax:
121    ///     `context.index_property::<Person, Age>()`
122    ///
123    /// This method both enables the index and catches it up to the current population.
124    fn index_property<E: Entity, P: Property<E>>(&mut self);
125
126    /// Enables value-count indexing of property values for the property `P`.
127    ///
128    /// If the property already has a full index, that index is left unchanged, as it
129    /// already supports value-count queries.
130    fn index_property_counts<E: Entity, P: Property<E>>(&mut self);
131
132    /// Tracks periodic value change counts for a newly created counter.
133    ///
134    /// Also panics if `period` is not finite and strictly positive.
135    ///
136    /// Recording starts at `ExecutionPhase::First` at simulation start time. The
137    /// first report runs at simulation start time in `ExecutionPhase::Last`, then at
138    /// each subsequent `start_time + k * period`. After the handler returns, the
139    /// matched counter is cleared.
140    ///
141    /// ```rust,ignore
142    /// context.track_periodic_value_change_counts::<Person, (InfectionStatus,), Age>(
143    ///     30.0,
144    ///     |_context, counter| {
145    ///         let _ = counter;
146    ///     },
147    /// );
148    /// ```
149    fn track_periodic_value_change_counts<E, PL, P, F>(&mut self, period: f64, handler: F)
150    where
151        E: Entity,
152        PL: PropertyList<E> + Eq + Hash,
153        P: Property<E> + Eq + Hash,
154        F: Fn(&mut Context, &mut StratifiedValueChangeCounter<E, PL, P>) + 'static;
155
156    /// Checks if a property `P` is indexed.
157    ///
158    /// This method is called with the turbo-fish syntax:
159    ///     `context.index_property::<Person, Age>()`
160    ///
161    /// This method can return `true` even if `context.index_property::<P>()` has never been called. For example,
162    /// if a multi-property is indexed, all equivalent multi-properties are automatically also indexed, as they
163    /// share a single index.
164    #[cfg(test)]
165    fn is_property_indexed<E: Entity, P: Property<E>>(&self) -> bool;
166
167    /// This method gives client code direct access to the query result as an `EntitySet`.
168    /// This is especially efficient for indexed queries, as this method can reduce to wrapping
169    /// a single indexed source.
170    fn with_query_results<'a, E: Entity, Q: Query<E>>(
171        &'a self,
172        query: Q,
173        callback: &mut dyn FnMut(EntitySet<'a, E>),
174    );
175
176    /// Gives the count of distinct entity IDs satisfying the query. This is especially
177    /// efficient for indexed queries.
178    ///
179    /// Supplying a naked entity, e.g. `Person`, is equivalent to calling `get_entity_count::<Person>()`.
180    fn query_entity_count<E: Entity, Q: Query<E>>(&self, query: Q) -> usize;
181
182    /// Sample a single entity uniformly from the query results. Returns `None` if the
183    /// query's result set is empty.
184    ///
185    /// To sample from the entire population, pass the entity type directly, for example `Person`.
186    fn sample_entity<E, Q, R>(&self, rng_id: R, query: Q) -> Option<EntityId<E>>
187    where
188        E: Entity,
189        Q: Query<E>,
190        R: RngId + 'static,
191        R::RngType: Rng;
192
193    /// Count query results and sample a single entity uniformly from them.
194    ///
195    /// Returns `(count, sample)`, where `sample` is `None` iff `count == 0`.
196    /// To sample from the entire population, pass the entity type directly, for example `Person`.
197    fn count_and_sample_entity<E, Q, R>(&self, rng_id: R, query: Q) -> (usize, Option<EntityId<E>>)
198    where
199        E: Entity,
200        Q: Query<E>,
201        R: RngId + 'static,
202        R::RngType: Rng;
203
204    /// Sample up to `requested` entities uniformly from the query results. If the
205    /// query's result set has fewer than `requested` entities, the entire result
206    /// set is returned.
207    ///
208    /// To sample from the entire population, pass the entity type directly, for example `Person`.
209    fn sample_entities<E, Q, R>(&self, rng_id: R, query: Q, n: usize) -> Vec<EntityId<E>>
210    where
211        E: Entity,
212        Q: Query<E>,
213        R: RngId + 'static,
214        R::RngType: Rng;
215
216    /// Returns a total count of all created entities of type `E`.
217    fn get_entity_count<E: Entity>(&self) -> usize;
218
219    /// Returns an iterator over all created entities of type `E`.
220    fn get_entity_iterator<E: Entity>(&self) -> PopulationIterator<E>;
221
222    /// Generates an `EntitySet` representing the query results.
223    fn query<E: Entity, Q: Query<E>>(&self, query: Q) -> EntitySet<E>;
224
225    /// Generates an iterator over the results of the query.
226    fn query_result_iterator<E: Entity, Q: Query<E>>(&self, query: Q) -> EntitySetIterator<E>;
227
228    /// Determines if the given person matches this query.
229    fn match_entity<E: Entity, Q: Query<E>>(&self, entity_id: EntityId<E>, query: Q) -> bool;
230
231    /// Removes all `EntityId`s from the given vector that do not match the given query.
232    fn filter_entities<E: Entity, Q: Query<E>>(&self, entities: &mut Vec<EntityId<E>>, query: Q);
233}
234
235impl ContextEntitiesExt for Context {
236    fn add_entity<E: Entity, PL: PropertyInitializationList<E>>(
237        &mut self,
238        property_list: PL,
239    ) -> Result<EntityId<E>, IxaError> {
240        // Check that the properties in the list are distinct.
241        PL::validate()?;
242
243        // Check that all required properties are present.
244        if !PL::contains_required_properties() {
245            return Err(IxaError::MissingRequiredInitializationProperties);
246        }
247
248        // Now that we know we will succeed, we create the entity.
249        let new_entity_id = self.entity_store.new_entity_id::<E>();
250
251        // Assign the properties in the list to the new entity.
252        // This does not generate a property change event.
253        property_list.set_values_for_new_entity(
254            new_entity_id,
255            self.entity_store.get_property_store_mut::<E>(),
256        );
257
258        // Keep all enabled indexes caught up as entities are created.
259        let context_ptr: *const Context = self;
260        let property_store = self.entity_store.get_property_store_mut::<E>();
261        // SAFETY: We create a shared `&Context` for read-only property access while mutably
262        // borrowing the property store to update index internals.
263        unsafe {
264            property_store.index_unindexed_entities_for_all_properties(&*context_ptr);
265        }
266
267        // Emit an `EntityCreatedEvent<Entity>`.
268        self.emit_event(EntityCreatedEvent::<E>::new(new_entity_id));
269
270        Ok(new_entity_id)
271    }
272
273    fn get_property<E: Entity, P: Property<E>>(&self, entity_id: EntityId<E>) -> P {
274        if P::is_derived() {
275            P::compute_derived(self, entity_id)
276        } else {
277            let property_store = self.get_property_value_store::<E, P>();
278            property_store.get(entity_id)
279        }
280    }
281
282    fn set_property<E: Entity, P: Property<E>>(
283        &mut self,
284        entity_id: EntityId<E>,
285        property_value: P,
286    ) {
287        debug_assert!(!P::is_derived(), "cannot set a derived property");
288
289        // The algorithm is as follows:
290        // 1. Snapshot previous values for the main property and any dependents that need change
291        //    processing by creating `PartialPropertyChangeEvent` instances.
292        // 2. Set the new value of the main property in the property store.
293        // 3. Emit each partial event; during emission each event computes the current value,
294        //    updates its index (remove old/add new), and emits a `PropertyChangeEvent`.
295
296        // We need two passes over the dependents: one pass to compute all the old values and
297        // another to compute all the new values. We group the steps for each dependent (and, it
298        // turns out, for the main property `P` as well) into two parts:
299        //  1. Before setting the main property `P`, factored out into
300        //     `self.property_store.create_partial_property_change`
301        //  2. After setting the main property `P`, factored out into
302        //     `PartialPropertyChangeEvent::emit_in_context`
303
304        // We decided not to do the following check:
305        // ```rust
306        // let previous_value = { self.get_property_value_store::<E, P>().get(entity_id) };
307        // if property_value == previous_value {
308        //     return;
309        // }
310        // ```
311        // The reasoning is:
312        // - It should be rare that we ever set a property to its present value.
313        // - It's not a significant burden on client code to check `property_value == previous_value` on
314        //   their own if they need to.
315        // - There may be use cases for listening to "writes" that don't actually change values.
316
317        // `SmallVec` inline capacity balances stack footprint against heap allocations: a larger
318        // inline size avoids spills for more dependents, while a smaller one keeps every
319        // set_property call lighter when most properties have few dependents. A value of 5 is
320        // chosen somewhat arbitrarily.
321        let mut dependents: SmallVec<[PartialPropertyChangeEventBox; 5]> = SmallVec::new();
322
323        // Immutable: Collect the previous value to create partial property change events
324        {
325            let property_store = self.entity_store.get_property_store::<E>();
326
327            // Create the partial property change for this value.
328            if property_store.should_create_partial_property_change(P::id(), self) {
329                dependents.push(property_store.create_partial_property_change(
330                    P::id(),
331                    entity_id,
332                    self,
333                ));
334            }
335            // Now create partial property change events for each dependent.
336            for dependent_idx in P::dependents() {
337                if property_store.should_create_partial_property_change(*dependent_idx, self) {
338                    dependents.push(property_store.create_partial_property_change(
339                        *dependent_idx,
340                        entity_id,
341                        self,
342                    ));
343                }
344            }
345        }
346
347        // Update the value
348        let property_value_store = self.get_property_value_store_mut::<E, P>();
349        property_value_store.set(entity_id, property_value);
350
351        // Mutable: After updating the value, we update its dependents, removing old values and
352        // storing the new values in their respective indexes, and emit the property change event.
353        for mut dependent in dependents {
354            dependent.emit_in_context(self)
355        }
356    }
357
358    fn index_property<E: Entity, P: Property<E>>(&mut self) {
359        let property_id = P::index_id();
360        let context_ptr: *const Context = self;
361        let property_store = self.entity_store.get_property_store_mut::<E>();
362        property_store.set_property_indexed::<P>(PropertyIndexType::FullIndex);
363        // SAFETY: This only creates a shared reference to `Context` while mutably borrowing
364        // the property store to update index internals.
365        unsafe {
366            property_store.index_unindexed_entities_for_property_id(&*context_ptr, property_id);
367        }
368    }
369
370    fn index_property_counts<E: Entity, P: Property<E>>(&mut self) {
371        let property_store = self.entity_store.get_property_store_mut::<E>();
372        let current_index_type = property_store.get::<P>().index_type();
373        if current_index_type != PropertyIndexType::FullIndex {
374            property_store.set_property_indexed::<P>(PropertyIndexType::ValueCountIndex);
375        }
376    }
377
378    fn track_periodic_value_change_counts<E, PL, P, F>(&mut self, period: f64, handler: F)
379    where
380        E: Entity,
381        PL: PropertyList<E> + Eq + Hash,
382        P: Property<E> + Eq + Hash,
383        F: Fn(&mut Context, &mut StratifiedValueChangeCounter<E, PL, P>) + 'static,
384    {
385        assert!(
386            period > 0.0 && !period.is_nan() && !period.is_infinite(),
387            "Period must be greater than 0"
388        );
389        let start_time = self.get_start_time().unwrap_or(0.0);
390        self.add_plan_with_phase(
391            start_time,
392            move |context| {
393                // We create the counter at simulation start so initialization-time
394                // property writes are never recorded.
395                let counter_id = context
396                    .entity_store
397                    .get_property_store_mut::<E>()
398                    .create_value_change_counter::<PL, P>();
399
400                // We defer the first handler plan until now because it needs
401                // `counter_id`, and it must run in `ExecutionPhase::Last`.
402                context.add_plan_with_phase(
403                    context.get_current_time(),
404                    move |context| {
405                        handle_periodic_value_change_count_event::<E, PL, P, F>(
406                            context, period, counter_id, handler,
407                        );
408                    },
409                    ExecutionPhase::Last,
410                );
411            },
412            ExecutionPhase::First,
413        );
414    }
415
416    #[cfg(test)]
417    fn is_property_indexed<E: Entity, P: Property<E>>(&self) -> bool {
418        let property_store = self.entity_store.get_property_store::<E>();
419        property_store.is_property_indexed::<P>()
420    }
421
422    fn with_query_results<'a, E: Entity, Q: Query<E>>(
423        &'a self,
424        query: Q,
425        callback: &mut dyn FnMut(EntitySet<'a, E>),
426    ) {
427        // The fast path for indexed queries.
428
429        // This mirrors the indexed case in `SourceSet<'a, E>::new()` and `QueryInternal::new_query_result`.
430        // The difference is, we access the index set if we find it.
431        if let Some(multi_property_id) = query.multi_property_id() {
432            let property_store = self.entity_store.get_property_store::<E>();
433            let query_parts = query.query_parts();
434            let lookup_result = property_store
435                .get_index_set_for_query_parts(multi_property_id, query_parts.as_ref());
436            match lookup_result {
437                IndexSetResult::Set(people_set) => {
438                    callback(EntitySet::from_source(SourceSet::IndexSet(people_set)));
439                    return;
440                }
441                IndexSetResult::Empty => {
442                    callback(EntitySet::empty());
443                    return;
444                }
445                IndexSetResult::Unsupported => {}
446            }
447            // If the property is not indexed, we fall through.
448        }
449
450        // Special case a whole-population query.
451        if query.is_empty_query() {
452            warn!("Called Context::with_query_results() with an empty query. Prefer Context::get_entity_iterator::<E>() for working with the entire population.");
453            callback(EntitySet::from_source(SourceSet::PopulationRange(
454                0..self.get_entity_count::<E>(),
455            )));
456            return;
457        }
458
459        // The slow path of computing the full query set.
460        warn!("Called Context::with_query_results() with an unindexed query. It's almost always better to use Context::query_result_iterator() for unindexed queries.");
461
462        // Fall back to the query's `EntitySet`.
463        callback(self.query(query));
464    }
465
466    fn query_entity_count<E: Entity, Q: Query<E>>(&self, query: Q) -> usize {
467        // The fast path for indexed queries.
468        //
469        // This mirrors the indexed case in `SourceSet<'a, E>::new()` and `QueryInternal::new_query_result`.
470        if let Some(multi_property_id) = query.multi_property_id() {
471            let property_store = self.entity_store.get_property_store::<E>();
472            let query_parts = query.query_parts();
473            let lookup_result = property_store
474                .get_index_count_for_query_parts(multi_property_id, query_parts.as_ref());
475            match lookup_result {
476                IndexCountResult::Count(count) => return count,
477                IndexCountResult::Unsupported => {}
478            }
479            // If the property is not indexed, we fall through.
480        }
481
482        self.query_result_iterator(query).count()
483    }
484    fn sample_entity<E, Q, R>(&self, rng_id: R, query: Q) -> Option<EntityId<E>>
485    where
486        E: Entity,
487        Q: Query<E>,
488        R: RngId + 'static,
489        R::RngType: Rng,
490    {
491        if query.is_empty_query() {
492            let population = self.get_entity_count::<E>();
493            return self.sample(rng_id, move |rng| {
494                if population == 0 {
495                    warn!("Requested a sample entity from an empty population");
496                    return None;
497                }
498                let index = if population <= u32::MAX as usize {
499                    rng.random_range(0..population as u32) as usize
500                } else {
501                    rng.random_range(0..population)
502                };
503                Some(EntityId::new(index))
504            });
505        }
506
507        let query_result = self.query(query);
508        self.sample(rng_id, move |rng| query_result.sample_entity(rng))
509    }
510
511    fn count_and_sample_entity<E, Q, R>(&self, rng_id: R, query: Q) -> (usize, Option<EntityId<E>>)
512    where
513        E: Entity,
514        Q: Query<E>,
515        R: RngId + 'static,
516        R::RngType: Rng,
517    {
518        if query.is_empty_query() {
519            let population = self.get_entity_count::<E>();
520            return self.sample(rng_id, move |rng| {
521                if population == 0 {
522                    return (0, None);
523                }
524                let index = if population <= u32::MAX as usize {
525                    rng.random_range(0..population as u32) as usize
526                } else {
527                    rng.random_range(0..population)
528                };
529                (population, Some(EntityId::new(index)))
530            });
531        }
532
533        let query_result = self.query(query);
534        self.sample(rng_id, move |rng| query_result.count_and_sample_entity(rng))
535    }
536
537    fn sample_entities<E, Q, R>(&self, rng_id: R, query: Q, n: usize) -> Vec<EntityId<E>>
538    where
539        E: Entity,
540        Q: Query<E>,
541        R: RngId + 'static,
542        R::RngType: Rng,
543    {
544        if query.is_empty_query() {
545            let population = self.get_entity_count::<E>();
546            return self.sample(rng_id, move |rng| {
547                if population == 0 {
548                    warn!("Requested a sample of entities from an empty population");
549                    return vec![];
550                }
551                if n >= population {
552                    return PopulationIterator::<E>::new(population).collect();
553                }
554                sample_multiple_from_known_length(rng, PopulationIterator::<E>::new(population), n)
555            });
556        }
557
558        let query_result = self.query(query);
559        self.sample(rng_id, move |rng| query_result.sample_entities(rng, n))
560    }
561
562    fn get_entity_count<E: Entity>(&self) -> usize {
563        self.entity_store.get_entity_count::<E>()
564    }
565
566    fn get_entity_iterator<E: Entity>(&self) -> PopulationIterator<E> {
567        self.entity_store.get_entity_iterator::<E>()
568    }
569
570    fn query<E: Entity, Q: Query<E>>(&self, query: Q) -> EntitySet<E> {
571        query.new_query_result(self)
572    }
573
574    fn query_result_iterator<E: Entity, Q: Query<E>>(&self, query: Q) -> EntitySetIterator<E> {
575        query.new_query_result_iterator(self)
576    }
577
578    fn match_entity<E: Entity, Q: Query<E>>(&self, entity_id: EntityId<E>, query: Q) -> bool {
579        query.match_entity(entity_id, self)
580    }
581
582    fn filter_entities<E: Entity, Q: Query<E>>(&self, entities: &mut Vec<EntityId<E>>, query: Q) {
583        query.filter_entities(entities, self);
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use std::cell::RefCell;
590    use std::rc::Rc;
591
592    use super::*;
593    use crate::entity::query::QueryInternal;
594    use crate::hashing::IndexSet;
595    use crate::prelude::PropertyChangeEvent;
596    use crate::{
597        define_derived_property, define_entity, define_multi_property, define_property, define_rng,
598        impl_property, with,
599    };
600
601    define_entity!(Animal);
602    define_property!(struct Legs(u8), Animal, default_const = Legs(4));
603    define_rng!(EntityContextTestRng);
604
605    define_entity!(Person);
606
607    define_property!(struct Age(u8), Person);
608
609    #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
610    struct CounterValue(u8);
611    impl_property!(CounterValue, Person, default_const = CounterValue(0));
612
613    #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
614    struct CounterStratum(bool);
615    impl_property!(
616        CounterStratum,
617        Person,
618        default_const = CounterStratum(false)
619    );
620
621    define_property!(
622        enum InfectionStatus {
623            Susceptible,
624            Infected,
625            Recovered,
626        },
627        Person,
628        default_const = InfectionStatus::Susceptible
629    );
630
631    define_property!(
632        struct Vaccinated(bool),
633        Person,
634        default_const = Vaccinated(false)
635    );
636
637    define_derived_property!(
638        enum AgeGroup {
639            Child,
640            Adult,
641            Senior,
642        },
643        Person,
644        [Age],
645        |age| {
646            if age.0 <= 18 {
647                AgeGroup::Child
648            } else if age.0 <= 65 {
649                AgeGroup::Adult
650            } else {
651                AgeGroup::Senior
652            }
653        }
654    );
655
656    define_derived_property!(
657        enum RiskLevel {
658            Low,
659            Medium,
660            High,
661        },
662        Person,
663        [AgeGroup, Vaccinated, InfectionStatus],
664        |age_group, vaccinated, infection_status| {
665            match (age_group, vaccinated, infection_status) {
666                (AgeGroup::Senior, Vaccinated(false), InfectionStatus::Susceptible) => {
667                    RiskLevel::High
668                }
669                (_, Vaccinated(false), InfectionStatus::Susceptible) => RiskLevel::Medium,
670                _ => RiskLevel::Low,
671            }
672        }
673    );
674
675    // ToDo(RobertJacobsonCDC): Enable this once #691 is resolved, https://github.com/CDCgov/ixa/issues/691.
676    // define_global_property!(GlobalDummy, u8);
677    // define_derived_property!(
678    //     struct MyDerivedProperty(u8),
679    //     Person,
680    //     [Age],
681    //     [GlobalDummy],
682    //     |age, global_dummy| {
683    //         MyDerivedProperty(age.0 + global_dummy)
684    //     }
685    // );
686
687    // Derived properties in a diamond dependency relationship
688    define_property!(struct IsRunner(bool), Person, default_const = IsRunner(false));
689    define_property!(struct IsSwimmer(bool), Person, default_const = IsSwimmer(false));
690    define_derived_property!(
691        struct AdultRunner(bool),
692        Person,
693        [AgeGroup, IsRunner],
694        | age_group, is_runner | {
695            AdultRunner(
696                age_group == AgeGroup::Adult
697                && is_runner.0
698            )
699        }
700    );
701    define_derived_property!(
702        struct AdultSwimmer(bool),
703        Person,
704        [AgeGroup, IsSwimmer],
705        | age_group, is_swimmer | {
706            AdultSwimmer(
707                age_group == AgeGroup::Adult
708                && is_swimmer.0
709            )
710        }
711    );
712    define_derived_property!(
713        struct AdultAthlete(bool),
714        Person,
715        [AdultSwimmer, AdultRunner],
716        | adult_swimmer, adult_runner | {
717            AdultAthlete(
718                adult_swimmer.0 || adult_runner.0
719            )
720        }
721    );
722
723    #[test]
724    fn add_and_count_entities() {
725        let mut context = Context::new();
726
727        let _person1 = context
728            .add_entity(with!(
729                Person,
730                Age(12),
731                InfectionStatus::Susceptible,
732                Vaccinated(true)
733            ))
734            .unwrap();
735        assert_eq!(context.get_entity_count::<Person>(), 1);
736
737        let _person2 = context
738            .add_entity(with!(Person, Age(34), Vaccinated(true)))
739            .unwrap();
740        assert_eq!(context.get_entity_count::<Person>(), 2);
741
742        // Age is the only required property
743        let _person3 = context.add_entity(with!(Person, Age(120))).unwrap();
744        assert_eq!(context.get_entity_count::<Person>(), 3);
745    }
746
747    #[test]
748    fn add_entity_with_zst() {
749        let mut context = Context::new();
750        let animal = context.add_entity(Animal).unwrap();
751        assert_eq!(context.get_entity_count::<Animal>(), 1);
752        assert_eq!(context.get_property::<Animal, Legs>(animal), Legs(4));
753    }
754
755    // Helper for index tests
756    #[derive(Copy, Clone, Debug)]
757    enum IndexMode {
758        Unindexed,
759        FullIndex,
760        ValueCountIndex,
761    }
762
763    // Returns `(context, existing_value, missing_value)`
764    fn setup_context_for_index_tests(index_mode: IndexMode) -> (Context, Age, Age) {
765        let mut context = Context::new();
766        match index_mode {
767            IndexMode::Unindexed => {}
768            IndexMode::FullIndex => context.index_property::<Person, Age>(),
769            IndexMode::ValueCountIndex => context.index_property_counts::<Person, Age>(),
770        }
771
772        let existing_value = Age(12);
773        let missing_value = Age(99);
774
775        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
776        let _ = context.add_entity(with!(Person, existing_value)).unwrap();
777
778        (context, existing_value, missing_value)
779    }
780
781    #[test]
782    fn query_results_respect_index_modes() {
783        let modes = [
784            IndexMode::Unindexed,
785            IndexMode::FullIndex,
786            IndexMode::ValueCountIndex,
787        ];
788
789        for mode in modes {
790            let (context, existing_value, missing_value) = setup_context_for_index_tests(mode);
791
792            let mut existing_len = 0;
793            context.with_query_results(with!(Person, existing_value), &mut |people_set| {
794                existing_len = people_set.into_iter().count();
795            });
796            assert_eq!(existing_len, 2, "Wrong length for {mode:?}");
797
798            let mut missing_len = 0;
799            context.with_query_results(with!(Person, missing_value), &mut |people_set| {
800                missing_len = people_set.into_iter().count();
801            });
802            assert_eq!(missing_len, 0);
803
804            let existing_count = context
805                .query_result_iterator(with!(Person, existing_value))
806                .count();
807            assert_eq!(existing_count, 2);
808
809            let missing_count = context
810                .query_result_iterator(with!(Person, missing_value))
811                .count();
812            assert_eq!(missing_count, 0);
813
814            assert_eq!(context.query_entity_count(with!(Person, existing_value)), 2);
815            assert_eq!(context.query_entity_count(with!(Person, missing_value)), 0);
816        }
817    }
818
819    #[test]
820    fn add_an_entity_without_required_properties() {
821        let mut context = Context::new();
822        let result = context.add_entity(with!(
823            Person,
824            InfectionStatus::Susceptible,
825            Vaccinated(true)
826        ));
827
828        assert!(matches!(
829            result,
830            Err(crate::IxaError::MissingRequiredInitializationProperties)
831        ));
832    }
833
834    #[test]
835    fn new_entities_have_default_values() {
836        let mut context = Context::new();
837
838        // Create a person with required Age property
839        let person = context.add_entity(with!(Person, Age(25))).unwrap();
840
841        // Retrieve and check their values
842        let age: Age = context.get_property(person);
843        assert_eq!(age, Age(25));
844        let infection_status: InfectionStatus = context.get_property(person);
845        assert_eq!(infection_status, InfectionStatus::Susceptible);
846        let vaccinated: Vaccinated = context.get_property(person);
847        assert_eq!(vaccinated, Vaccinated(false));
848
849        // Change them
850        context.set_property(person, Age(26));
851        context.set_property(person, InfectionStatus::Infected);
852        context.set_property(person, Vaccinated(true));
853
854        // Retrieve and check their values
855        let age: Age = context.get_property(person);
856        assert_eq!(age, Age(26));
857        let infection_status: InfectionStatus = context.get_property(person);
858        assert_eq!(infection_status, InfectionStatus::Infected);
859        let vaccinated: Vaccinated = context.get_property(person);
860        assert_eq!(vaccinated, Vaccinated(true));
861    }
862
863    #[test]
864    fn get_and_set_property_explicit() {
865        let mut context = Context::new();
866
867        // Create a person with explicit property values
868        let person = context
869            .add_entity(with!(
870                Person,
871                Age(25),
872                InfectionStatus::Recovered,
873                Vaccinated(true)
874            ))
875            .unwrap();
876
877        // Retrieve and check their values
878        let age: Age = context.get_property(person);
879        assert_eq!(age, Age(25));
880        let infection_status: InfectionStatus = context.get_property(person);
881        assert_eq!(infection_status, InfectionStatus::Recovered);
882        let vaccinated: Vaccinated = context.get_property(person);
883        assert_eq!(vaccinated, Vaccinated(true));
884
885        // Change them
886        context.set_property(person, Age(26));
887        context.set_property(person, InfectionStatus::Infected);
888        context.set_property(person, Vaccinated(false));
889
890        // Retrieve and check their values
891        let age: Age = context.get_property(person);
892        assert_eq!(age, Age(26));
893        let infection_status: InfectionStatus = context.get_property(person);
894        assert_eq!(infection_status, InfectionStatus::Infected);
895        let vaccinated: Vaccinated = context.get_property(person);
896        assert_eq!(vaccinated, Vaccinated(false));
897    }
898
899    #[test]
900    fn count_entities() {
901        let mut context = Context::new();
902
903        assert_eq!(context.get_entity_count::<Animal>(), 0);
904        assert_eq!(context.get_entity_count::<Person>(), 0);
905
906        // Create entities of different kinds
907        for _ in 0..7 {
908            let _: PersonId = context.add_entity(with!(Person, Age(25))).unwrap();
909        }
910        for _ in 0..5 {
911            let _: AnimalId = context.add_entity(with!(Animal, Legs(2))).unwrap();
912        }
913
914        assert_eq!(context.get_entity_count::<Animal>(), 5);
915        assert_eq!(context.get_entity_count::<Person>(), 7);
916
917        let _: PersonId = context.add_entity(with!(Person, Age(30))).unwrap();
918        let _: AnimalId = context.add_entity(with!(Animal, Legs(8))).unwrap();
919
920        assert_eq!(context.get_entity_count::<Animal>(), 6);
921        assert_eq!(context.get_entity_count::<Person>(), 8);
922    }
923
924    #[test]
925    fn count_and_sample_entity_empty_query_fast_path() {
926        let mut context = Context::new();
927        context.init_random(42);
928        for age in [10u8, 20, 30] {
929            let _: PersonId = context.add_entity(with!(Person, Age(age))).unwrap();
930        }
931
932        let (count, sampled) =
933            context.count_and_sample_entity::<Person, _, _>(EntityContextTestRng, Person);
934        assert_eq!(count, 3);
935        assert!(sampled.is_some());
936    }
937
938    #[test]
939    fn count_and_sample_entity_unindexed_derived_query() {
940        let mut context = Context::new();
941        context.init_random(43);
942        for age in [10u8, 20, 30, 80] {
943            let _: PersonId = context.add_entity(with!(Person, Age(age))).unwrap();
944        }
945
946        let query = with!(Person, AgeGroup::Adult);
947        let expected_count = context.query_entity_count(query);
948        let (count, sampled) = context.count_and_sample_entity(EntityContextTestRng, query);
949        assert_eq!(count, expected_count);
950        assert_eq!(sampled.is_some(), count > 0);
951        if let Some(entity_id) = sampled {
952            assert!(context.match_entity(entity_id, query));
953        }
954    }
955
956    #[test]
957    fn get_derived_property_multiple_deps() {
958        let mut context = Context::new();
959
960        let expected_high_id: PersonId = context
961            .add_entity(with!(
962                Person,
963                Age(77),
964                Vaccinated(false),
965                InfectionStatus::Susceptible
966            ))
967            .unwrap();
968        let expected_med_id: PersonId = context
969            .add_entity(with!(
970                Person,
971                Age(30),
972                Vaccinated(false),
973                InfectionStatus::Susceptible
974            ))
975            .unwrap();
976        let expected_low_id: PersonId = context
977            .add_entity(with!(
978                Person,
979                Age(3),
980                Vaccinated(true),
981                InfectionStatus::Recovered
982            ))
983            .unwrap();
984
985        let actual_high: RiskLevel = context.get_property(expected_high_id);
986        assert_eq!(actual_high, RiskLevel::High);
987        let actual_med: RiskLevel = context.get_property(expected_med_id);
988        assert_eq!(actual_med, RiskLevel::Medium);
989        let actual_low: RiskLevel = context.get_property(expected_low_id);
990        assert_eq!(actual_low, RiskLevel::Low);
991    }
992
993    #[test]
994    fn listen_to_derived_property_change_event() {
995        let mut context = Context::new();
996
997        let expected_high_id = PersonId::new(0);
998
999        // Listen for derived property change events and record how many times it fires
1000        // For `RiskLevel`
1001        let risk_flag = Rc::new(RefCell::new(0));
1002        let risk_flag_clone = risk_flag.clone();
1003        context.subscribe_to_event(
1004            move |_context, event: PropertyChangeEvent<Person, RiskLevel>| {
1005                assert_eq!(event.entity_id, expected_high_id);
1006                assert_eq!(event.previous, RiskLevel::High);
1007                assert_eq!(event.current, RiskLevel::Medium);
1008                *risk_flag_clone.borrow_mut() += 1;
1009            },
1010        );
1011        // For `AgeGroup`
1012        let age_group_flag = Rc::new(RefCell::new(0));
1013        let age_group_flag_clone = age_group_flag.clone();
1014        context.subscribe_to_event(
1015            move |_context, event: PropertyChangeEvent<Person, AgeGroup>| {
1016                assert_eq!(event.entity_id, expected_high_id);
1017                assert_eq!(event.previous, AgeGroup::Senior);
1018                assert_eq!(event.current, AgeGroup::Adult);
1019                *age_group_flag_clone.borrow_mut() += 1;
1020            },
1021        );
1022
1023        // Should not emit change events
1024        let expected_high_id: PersonId = context
1025            .add_entity(with!(
1026                Person,
1027                Age(77),
1028                Vaccinated(false),
1029                InfectionStatus::Susceptible
1030            ))
1031            .unwrap();
1032
1033        // Should emit change events
1034        context.set_property(expected_high_id, Age(20));
1035
1036        // Execute queued event handlers
1037        context.execute();
1038        // Should have exactly one event recorded
1039        assert_eq!(*risk_flag.borrow(), 1);
1040        assert_eq!(*age_group_flag.borrow(), 1);
1041    }
1042
1043    /*
1044    ToDo(RobertJacobsonCDC): Enable this once #691 is resolved, https://github.com/CDCgov/ixa/issues/691.
1045
1046    #[test]
1047    fn get_derived_property_with_globals() {
1048        let mut context = Context::new();
1049
1050        context.set_global_property_value(GlobalDummy, 18).unwrap();
1051        let child = context.add_entity(with!(Person, Age(17))).unwrap();
1052        let adult = context.add_entity(with!(Person, Age(19))).unwrap();
1053
1054        let child_computed: MyDerivedProperty = context.get_property(child);
1055        assert_eq!(child_computed, MyDerivedProperty(17+18));
1056
1057        let adult_computed: MyDerivedProperty = context.get_property(adult);
1058        assert_eq!(adult_computed, MyDerivedProperty(19+18));
1059    }
1060    */
1061
1062    #[test]
1063    fn observe_diamond_property_change() {
1064        let mut context = Context::new();
1065        let person = context
1066            .add_entity(with!(Person, Age(17), IsSwimmer(true)))
1067            .unwrap();
1068
1069        let is_adult_athlete: AdultAthlete = context.get_property(person);
1070        assert!(!is_adult_athlete.0);
1071
1072        let flag = Rc::new(RefCell::new(0));
1073        let flag_clone = flag.clone();
1074        context.subscribe_to_event(
1075            move |_context, event: PropertyChangeEvent<Person, AdultAthlete>| {
1076                assert_eq!(event.entity_id, person);
1077                assert_eq!(event.previous, AdultAthlete(false));
1078                assert_eq!(event.current, AdultAthlete(true));
1079                *flag_clone.borrow_mut() += 1;
1080            },
1081        );
1082
1083        context.set_property(person, Age(20));
1084        // Make sure the derived property is what we expect.
1085        let is_adult_athlete: AdultAthlete = context.get_property(person);
1086        assert!(is_adult_athlete.0);
1087
1088        // Execute queued event handlers
1089        context.execute();
1090        // Should have exactly one event recorded
1091        assert_eq!(*flag.borrow(), 1);
1092    }
1093
1094    // Tests related to queries and indexing
1095
1096    define_multi_property!((InfectionStatus, Vaccinated), Person);
1097    define_multi_property!((Vaccinated, InfectionStatus), Person);
1098
1099    #[test]
1100    fn with_query_results_finds_multi_index() {
1101        use crate::rand::rngs::SmallRng;
1102        use crate::rand::seq::IndexedRandom;
1103        use crate::rand::SeedableRng;
1104
1105        let mut rng = SmallRng::seed_from_u64(42);
1106        let mut context = Context::new();
1107
1108        for _ in 0..10_000usize {
1109            let infection_status = *[
1110                InfectionStatus::Susceptible,
1111                InfectionStatus::Infected,
1112                InfectionStatus::Recovered,
1113            ]
1114            .choose(&mut rng)
1115            .unwrap();
1116            let vaccination_status: bool = rng.random_bool(0.5);
1117            let age: u8 = rng.random_range(0..100);
1118            context
1119                .add_entity(with!(
1120                    Person,
1121                    Age(age),
1122                    infection_status,
1123                    Vaccinated(vaccination_status)
1124                ))
1125                .unwrap();
1126        }
1127        context.index_property::<Person, InfectionStatusVaccinated>();
1128        // Force an index build by running a query.
1129        let _ = context.query_result_iterator(with!(
1130            Person,
1131            InfectionStatus::Susceptible,
1132            Vaccinated(true)
1133        ));
1134
1135        // Capture the set given by `with_query_results`.
1136        let mut result_entities: IndexSet<EntityId<Person>> = IndexSet::default();
1137        context.with_query_results(
1138            with!(Person, InfectionStatus::Susceptible, Vaccinated(true)),
1139            &mut |result_set| {
1140                result_entities = result_set.into_iter().collect::<IndexSet<_>>();
1141            },
1142        );
1143
1144        // Check that the order doesn't matter.
1145        assert_eq!(
1146            InfectionStatusVaccinated::index_id(),
1147            VaccinatedInfectionStatus::index_id()
1148        );
1149        assert_eq!(
1150            InfectionStatusVaccinated::index_id(),
1151            (InfectionStatus::Susceptible, Vaccinated(true))
1152                .multi_property_id()
1153                .unwrap()
1154        );
1155
1156        // Check if it matches the expected bucket.
1157        let index_id = InfectionStatusVaccinated::index_id();
1158
1159        let property_store = context.entity_store.get_property_store::<Person>();
1160        let query = (InfectionStatus::Susceptible, Vaccinated(true));
1161        let query_parts = query.query_parts();
1162        let bucket =
1163            match property_store.get_index_set_for_query_parts(index_id, query_parts.as_ref()) {
1164                IndexSetResult::Set(bucket) => bucket,
1165                other => panic!("expected indexed query bucket, found {other:?}"),
1166            };
1167
1168        let expected_entities = bucket.iter().copied().collect::<IndexSet<_>>();
1169        assert_eq!(expected_entities, result_entities);
1170    }
1171
1172    #[test]
1173    fn query_returns_entity_set_and_query_result_iterator_remains_compatible() {
1174        let mut context = Context::new();
1175        let p1 = context
1176            .add_entity(with!(
1177                Person,
1178                Age(21),
1179                InfectionStatus::Susceptible,
1180                Vaccinated(true)
1181            ))
1182            .unwrap();
1183        let _p2 = context
1184            .add_entity(with!(
1185                Person,
1186                Age(22),
1187                InfectionStatus::Susceptible,
1188                Vaccinated(false)
1189            ))
1190            .unwrap();
1191        let p3 = context
1192            .add_entity(with!(
1193                Person,
1194                Age(23),
1195                InfectionStatus::Infected,
1196                Vaccinated(true)
1197            ))
1198            .unwrap();
1199
1200        let query = with!(Person, Vaccinated(true));
1201
1202        let from_set = context
1203            .query::<Person, _>(query)
1204            .into_iter()
1205            .collect::<IndexSet<_>>();
1206        let from_iterator = context
1207            .query_result_iterator(query)
1208            .collect::<IndexSet<_>>();
1209
1210        assert_eq!(from_set, from_iterator);
1211        assert!(from_set.contains(&p1));
1212        assert!(from_set.contains(&p3));
1213        assert_eq!(from_set.len(), 2);
1214    }
1215
1216    #[test]
1217    fn set_property_correctly_maintains_index() {
1218        let mut context = Context::new();
1219        context.index_property::<Person, InfectionStatus>();
1220        context.index_property::<Person, AgeGroup>();
1221
1222        let person1 = context.add_entity(with!(Person, Age(22))).unwrap();
1223        let person2 = context.add_entity(with!(Person, Age(22))).unwrap();
1224        for _ in 0..4 {
1225            let _: PersonId = context.add_entity(with!(Person, Age(22))).unwrap();
1226        }
1227
1228        // Check non-derived property index is correctly maintained
1229        assert_eq!(
1230            context.query_entity_count(with!(Person, InfectionStatus::Susceptible)),
1231            6
1232        );
1233        assert_eq!(
1234            context.query_entity_count(with!(Person, InfectionStatus::Infected)),
1235            0
1236        );
1237        assert_eq!(
1238            context.query_entity_count(with!(Person, InfectionStatus::Recovered)),
1239            0
1240        );
1241
1242        context.set_property(person1, InfectionStatus::Infected);
1243
1244        assert_eq!(
1245            context.query_entity_count(with!(Person, InfectionStatus::Susceptible)),
1246            5
1247        );
1248        assert_eq!(
1249            context.query_entity_count(with!(Person, InfectionStatus::Infected)),
1250            1
1251        );
1252        assert_eq!(
1253            context.query_entity_count(with!(Person, InfectionStatus::Recovered)),
1254            0
1255        );
1256
1257        context.set_property(person1, InfectionStatus::Recovered);
1258
1259        assert_eq!(
1260            context.query_entity_count(with!(Person, InfectionStatus::Susceptible)),
1261            5
1262        );
1263        assert_eq!(
1264            context.query_entity_count(with!(Person, InfectionStatus::Infected)),
1265            0
1266        );
1267        assert_eq!(
1268            context.query_entity_count(with!(Person, InfectionStatus::Recovered)),
1269            1
1270        );
1271
1272        // Check derived property index is correctly maintained.
1273        assert_eq!(
1274            context.query_entity_count(with!(Person, AgeGroup::Child)),
1275            0
1276        );
1277        assert_eq!(
1278            context.query_entity_count(with!(Person, AgeGroup::Adult)),
1279            6
1280        );
1281        assert_eq!(
1282            context.query_entity_count(with!(Person, AgeGroup::Senior)),
1283            0
1284        );
1285
1286        context.set_property(person2, Age(12));
1287
1288        assert_eq!(
1289            context.query_entity_count(with!(Person, AgeGroup::Child)),
1290            1
1291        );
1292        assert_eq!(
1293            context.query_entity_count(with!(Person, AgeGroup::Adult)),
1294            5
1295        );
1296        assert_eq!(
1297            context.query_entity_count(with!(Person, AgeGroup::Senior)),
1298            0
1299        );
1300
1301        context.set_property(person1, Age(75));
1302
1303        assert_eq!(
1304            context.query_entity_count(with!(Person, AgeGroup::Child)),
1305            1
1306        );
1307        assert_eq!(
1308            context.query_entity_count(with!(Person, AgeGroup::Adult)),
1309            4
1310        );
1311        assert_eq!(
1312            context.query_entity_count(with!(Person, AgeGroup::Senior)),
1313            1
1314        );
1315
1316        context.set_property(person2, Age(77));
1317
1318        assert_eq!(
1319            context.query_entity_count(with!(Person, AgeGroup::Child)),
1320            0
1321        );
1322        assert_eq!(
1323            context.query_entity_count(with!(Person, AgeGroup::Adult)),
1324            4
1325        );
1326        assert_eq!(
1327            context.query_entity_count(with!(Person, AgeGroup::Senior)),
1328            2
1329        );
1330    }
1331
1332    #[test]
1333    fn query_unindexed_default_properties() {
1334        let mut context = Context::new();
1335
1336        // Half will have the default value.
1337        for idx in 0..10 {
1338            if idx % 2 == 0 {
1339                context.add_entity(with!(Person, Age(22))).unwrap();
1340            } else {
1341                context
1342                    .add_entity(with!(Person, Age(22), InfectionStatus::Recovered))
1343                    .unwrap();
1344            }
1345        }
1346        // The tail also has the default value
1347        for _ in 0..10 {
1348            let _: PersonId = context.add_entity(with!(Person, Age(22))).unwrap();
1349        }
1350
1351        assert_eq!(
1352            context.query_entity_count(with!(Person, InfectionStatus::Recovered)),
1353            5
1354        );
1355        assert_eq!(
1356            context.query_entity_count(with!(Person, InfectionStatus::Susceptible)),
1357            15
1358        );
1359    }
1360
1361    #[test]
1362    fn query_unindexed_derived_properties() {
1363        let mut context = Context::new();
1364
1365        for _ in 0..10 {
1366            let _: PersonId = context.add_entity(with!(Person, Age(22))).unwrap();
1367        }
1368
1369        assert_eq!(
1370            context.query_entity_count(with!(Person, AdultAthlete(false))),
1371            10
1372        );
1373    }
1374
1375    #[test]
1376    fn track_periodic_value_change_counts_uses_distinct_counters() {
1377        let mut context = Context::new();
1378
1379        context.track_periodic_value_change_counts::<Person, (CounterStratum,), CounterValue, _>(
1380            1.0,
1381            move |_context, _counter| {},
1382        );
1383
1384        context.track_periodic_value_change_counts::<Person, (CounterStratum,), CounterValue, _>(
1385            1.0,
1386            move |_context, _counter| {},
1387        );
1388
1389        let property_value_store = context.get_property_value_store::<Person, CounterValue>();
1390        assert_eq!(property_value_store.value_change_counters.len(), 0);
1391
1392        context.add_plan(0.5, Context::shutdown);
1393        context.execute();
1394
1395        let property_value_store = context.get_property_value_store::<Person, CounterValue>();
1396        assert_eq!(property_value_store.value_change_counters.len(), 2);
1397    }
1398
1399    #[test]
1400    fn value_change_counter_updates_on_true_transitions() {
1401        let mut context = Context::new();
1402        let observed = Rc::new(RefCell::new(Vec::<(usize, usize)>::new()));
1403        let observed_clone = observed.clone();
1404
1405        context.track_periodic_value_change_counts(1.0, move |_context, counter| {
1406            observed_clone.borrow_mut().push((
1407                counter.get_count((CounterStratum(true),), CounterValue(1)),
1408                counter.get_count((CounterStratum(true),), CounterValue(2)),
1409            ));
1410        });
1411
1412        let person = context
1413            .add_entity(with!(
1414                Person,
1415                Age(10),
1416                CounterValue(0),
1417                CounterStratum(true)
1418            ))
1419            .unwrap();
1420        context.add_plan(0.1, move |context| {
1421            context.set_property(person, CounterValue(1));
1422            context.set_property(person, CounterValue(1));
1423            context.set_property(person, CounterValue(2));
1424        });
1425
1426        context.execute();
1427        assert_eq!(*observed.borrow(), vec![(0, 0), (1, 1)]);
1428    }
1429
1430    #[test]
1431    fn periodic_value_change_counts_report_and_clear() {
1432        let mut context = Context::new();
1433        let person = context
1434            .add_entity(with!(
1435                Person,
1436                Age(10),
1437                CounterValue(0),
1438                CounterStratum(true)
1439            ))
1440            .unwrap();
1441
1442        let observed = Rc::new(RefCell::new(Vec::<usize>::new()));
1443        let observed_clone = observed.clone();
1444
1445        context.track_periodic_value_change_counts(1.0, move |_context, counter| {
1446            observed_clone
1447                .borrow_mut()
1448                .push(counter.get_count((CounterStratum(true),), CounterValue(1)));
1449        });
1450
1451        context.add_plan(0.5, move |context| {
1452            context.set_property(person, CounterValue(1));
1453        });
1454        context.add_plan(1.5, move |context| {
1455            context.set_property(person, CounterValue(1));
1456        });
1457
1458        context.execute();
1459        assert_eq!(*observed.borrow(), vec![0, 1, 0]);
1460    }
1461
1462    #[test]
1463    fn periodic_value_change_counts_start_time_and_phase_behavior() {
1464        let mut context = Context::new();
1465        context.set_start_time(-2.0);
1466
1467        let person = context
1468            .add_entity(with!(
1469                Person,
1470                Age(10),
1471                CounterValue(0),
1472                CounterStratum(true)
1473            ))
1474            .unwrap();
1475
1476        let observed_times = Rc::new(RefCell::new(Vec::<f64>::new()));
1477        let observed_counts = Rc::new(RefCell::new(Vec::<usize>::new()));
1478        let observed_times_clone = observed_times.clone();
1479        let observed_counts_clone = observed_counts.clone();
1480
1481        context.track_periodic_value_change_counts(1.0, move |context, counter| {
1482            observed_times_clone
1483                .borrow_mut()
1484                .push(context.get_current_time());
1485            observed_counts_clone
1486                .borrow_mut()
1487                .push(counter.get_count((CounterStratum(true),), CounterValue(1)));
1488        });
1489
1490        context.add_plan_with_phase(
1491            -2.0,
1492            move |context| {
1493                context.set_property(person, CounterValue(1));
1494            },
1495            ExecutionPhase::Normal,
1496        );
1497        context.add_plan(0.0, |_| {});
1498
1499        context.execute();
1500
1501        assert_eq!(*observed_times.borrow(), vec![-2.0, -1.0, 0.0]);
1502        assert_eq!(*observed_counts.borrow(), vec![1, 0, 0]);
1503    }
1504}