Skip to main content

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