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