Skip to main content

ixa/triggers/
mod.rs

1//! Trigger criteria that emit user-defined events.
2//!
3//! A trigger is a way to say: when some simulation criterion is met, emit a
4//! concrete user-defined [`IxaEvent`]. Trigger-emitted events
5//! are ordinary Ixa events, so any number of subscribers can listen for them
6//! with [`Context::subscribe_to_event`](crate::Context::subscribe_to_event).
7//!
8//! A trigger criterion is not itself registered on a context. A criterion, such
9//! as [`PropertyChangeTrigger`] or [`TimeTrigger`], defines what should be
10//! monitored. A complete trigger is created only after binding that criterion
11//! to a concrete event with one of the `emit_*` methods. The value returned by
12//! `emit_with`, `emit_value`, or `emit_default` is the value passed to
13//! [`ContextTriggersExt::register_trigger`].
14//!
15//! The usual flow is:
16//!
17//! 1. Choose one of the built-in trigger criteria.
18//! 2. Bind it to the event you want emitted with [`TriggerCriterion::emit_with`],
19//!    [`TriggerCriterion::emit_value`], or [`TriggerCriterion::emit_default`].
20//! 3. Register the complete trigger with [`ContextTriggersExt::register_trigger`].
21//! 4. Subscribe to the emitted user event as usual.
22//!
23//! [`TogglingTrigger`] is a composite trigger for stateful on/off behavior. Instead of binding a
24//! single criterion to a single event, it combines an activation criterion and a deactivation
25//! criterion, each with its own emitted event (of possibly distinct types).
26//!
27//! The usual flow for a toggling trigger is:
28//!
29//! 1. Choose the activation and deactivation criteria.
30//! 2. Pair them with [`TogglingTriggerCriteria::new`].
31//! 3. Bind the pair to activation and deactivation events with one of the
32//!    `TogglingTriggerCriteria::emit_*` methods.
33//! 4. Register the complete trigger with [`ContextTriggersExt::register_trigger`].
34//! 5. Subscribe to the activation and deactivation events as usual.
35//!
36//! ## Construct an event from observation data
37//!
38//! Each trigger criterion has its own observation data type, available as the criterion's
39//! [`TriggerCriterion::Observation`] associated type. For example, [`PropertyChangeTrigger`]
40//! observations use [`PropertyChangeTriggerEvent`] containing the entity ID and the previous and
41//! current property values. [`EntityCountTrigger`], [`PropertyValueCountTrigger`], [`TimeTrigger`],
42//! and [`PeriodicTimeTrigger`] use their corresponding `*TriggerEvent` types.
43//!
44//! For events that do not need observation data, use [`TriggerCriterion::emit_value`] to emit
45//! a constant event value, or [`TriggerCriterion::emit_default`] when the event type implements
46//! [`Default`].
47//!
48//! Use [`TriggerCriterion::emit_with`] when the emitted event should contain data from the trigger
49//! observation. When the criterion is met, this observation value is passed to the event
50//! constructor (typically a closure or static constructor method) supplied to `emit_with`, and that
51//! constructor returns the concrete user-defined [`IxaEvent`] that subscribers
52//! will receive.
53//!
54//! ```rust
55//! use ixa::{Context, define_entity, define_property, IxaEvent};
56//! use ixa::entity::EntityId;
57//! use ixa::triggers::{ContextTriggersExt, PropertyChangeTrigger, TriggerCriterion};
58//!
59//! define_entity!(Person);
60//! define_property!(struct Alive(bool), Person, default_const = Alive(true));
61//!
62//! #[derive(IxaEvent)]
63//! struct FirstDeath {
64//!     person: EntityId<Person>,
65//! }
66//!
67//! let mut context = Context::new();
68//!
69//! context.register_trigger(
70//!     PropertyChangeTrigger::to(Alive(false))
71//!         .once()
72//!         .emit_with(|event| FirstDeath {
73//!             person: event.entity_id,
74//!         }),
75//! );
76//!
77//! context.subscribe_to_event(|_context, _event: FirstDeath| {
78//!     // perform cleanup tasks
79//! });
80//! ```
81//!
82
83mod entity_count;
84mod periodic_time;
85mod property_change;
86mod property_value_count;
87mod time;
88mod toggling_trigger;
89
90use std::marker::PhantomData;
91
92pub use entity_count::{EntityCountTrigger, EntityCountTriggerEvent};
93pub use periodic_time::{PeriodicTimeTrigger, PeriodicTimeTriggerEvent};
94pub use property_change::{PropertyChangeTrigger, PropertyChangeTriggerEvent};
95pub use property_value_count::{PropertyValueCountTrigger, PropertyValueCountTriggerEvent};
96pub use time::{TimeTrigger, TimeTriggerEvent};
97pub use toggling_trigger::{TogglingTrigger, TogglingTriggerCriteria};
98
99use crate::{Context, IxaEvent};
100
101/// Direction in which a count changed to reach a threshold.
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum Direction {
104    Increasing,
105    Decreasing,
106}
107
108/// Whether a trigger emits once or every time its criterion is satisfied.
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum TriggerMode {
111    Once,
112    Repeating,
113}
114
115/// A bare trigger criterion: the condition that can be monitored. This module provides a collection
116/// of types that implement this trait.
117pub trait TriggerCriterion: Sized + 'static {
118    /// The data that represents what is observed when the criterion is met.
119    /// This data is passed to the handler installed for this criterion.
120    type Observation: 'static;
121
122    /// Install the criterion's monitoring logic in `context`.
123    fn install<F>(self, context: &mut Context, on_match: F)
124    where
125        F: Fn(&mut Context, Self::Observation) + 'static;
126
127    /// Bind this criterion to a constructor for a concrete user event.
128    fn emit_with<Ev, F>(self, make_event: F) -> Trigger<Self, Ev, F>
129    where
130        Ev: IxaEvent,
131        F: Fn(Self::Observation) -> Ev + 'static,
132    {
133        Trigger {
134            criterion: self,
135            make_event,
136            _event: PhantomData,
137        }
138    }
139
140    /// Bind this criterion to a default-valued concrete user event.
141    fn emit_default<Ev>(self) -> Trigger<Self, Ev, impl Fn(Self::Observation) -> Ev>
142    where
143        Ev: IxaEvent + Default,
144    {
145        self.emit_with(|_| Ev::default())
146    }
147
148    /// Bind this criterion to a constant concrete user event value.
149    fn emit_value<Ev>(self, event: Ev) -> Trigger<Self, Ev, impl Fn(Self::Observation) -> Ev>
150    where
151        Ev: IxaEvent,
152    {
153        self.emit_with(move |_| event)
154    }
155}
156
157/// A complete installable trigger specification that can be passed to `context.register_trigger`.
158/// This is automatically implemented by the `Trigger` types returned by the `emit_*` methods
159/// on trigger criterion types. Client code should not implement this themselves.
160pub trait TriggerSpec: Sized {
161    fn install_in_context(self, context: &mut Context);
162}
163
164/// A criterion bound to a user event constructor. Values of this type are not constructed directly
165/// but rather are returned by the `emit_*` methods on trigger criterion types. These values are
166/// "complete" triggers than can be "installed" on a context with `context.register_trigger`.
167pub struct Trigger<C, Ev, F> {
168    criterion: C,
169    make_event: F,
170    _event: PhantomData<fn() -> Ev>,
171}
172
173impl<C, Ev, F> TriggerSpec for Trigger<C, Ev, F>
174where
175    C: TriggerCriterion,
176    Ev: IxaEvent,
177    F: Fn(C::Observation) -> Ev + 'static,
178{
179    fn install_in_context(self, context: &mut Context) {
180        let make_event = self.make_event;
181        self.criterion
182            .install(context, move |context, observation| {
183                context.emit_event(make_event(observation));
184            });
185    }
186}
187
188/// Extension trait for registering triggers on a [`Context`].
189pub trait ContextTriggersExt {
190    fn register_trigger<T: TriggerSpec>(&mut self, trigger: T);
191}
192
193impl ContextTriggersExt for Context {
194    fn register_trigger<T: TriggerSpec>(&mut self, trigger: T) {
195        trigger.install_in_context(self);
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    #![allow(dead_code)]
202
203    use std::cell::{Cell, RefCell};
204    use std::rc::Rc;
205
206    use super::*;
207    use crate::entity::EntityId;
208    use crate::{
209        define_entity, define_property, with, Context, ContextEntitiesExt, ExecutionPhase, IxaEvent,
210    };
211
212    define_entity!(Person);
213    define_entity!(Case);
214
215    define_property!(
216        enum InfectionStatus {
217            Susceptible,
218            Infectious,
219            Recovered,
220        },
221        Person,
222        default_const = InfectionStatus::Susceptible
223    );
224
225    define_property!(struct Alive(bool), Person, default_const = Alive(true));
226
227    define_property!(
228        enum CaseStatus {
229            Detected,
230        },
231        Case,
232        default_const = CaseStatus::Detected
233    );
234
235    #[derive(IxaEvent)]
236    struct InfectiousThresholdReached {
237        count: usize,
238        mode: TriggerMode,
239    }
240
241    #[derive(Default, IxaEvent)]
242    struct CaseThresholdReached;
243
244    #[derive(IxaEvent)]
245    struct FirstDeath {
246        person: EntityId<Person>,
247        mode: TriggerMode,
248    }
249
250    #[derive(IxaEvent)]
251    struct StopTimeReached {
252        phase: ExecutionPhase,
253    }
254
255    #[derive(IxaEvent)]
256    struct PeriodicTimeReached {
257        time: f64,
258        period: f64,
259        phase: ExecutionPhase,
260    }
261
262    #[derive(Clone, Copy)]
263    struct WrappedF64(f64);
264
265    impl From<WrappedF64> for f64 {
266        fn from(value: WrappedF64) -> Self {
267            value.0
268        }
269    }
270
271    #[test]
272    fn time_trigger_builders_accept_into_f64() {
273        let mut context = Context::new();
274        let observed = Rc::new(RefCell::new(Vec::new()));
275
276        TimeTrigger::at(WrappedF64(0.5)).install(&mut context, {
277            let observed = Rc::clone(&observed);
278            move |_context, event| {
279                observed.borrow_mut().push((event.time, event.phase));
280            }
281        });
282        TimeTrigger::at_phase(WrappedF64(0.5), ExecutionPhase::Last).install(&mut context, {
283            let observed = Rc::clone(&observed);
284            move |_context, event| {
285                observed.borrow_mut().push((event.time, event.phase));
286            }
287        });
288
289        context.execute();
290
291        assert_eq!(
292            *observed.borrow(),
293            vec![(0.5, ExecutionPhase::Normal), (0.5, ExecutionPhase::Last),]
294        );
295    }
296
297    #[test]
298    fn periodic_time_trigger_builders_accept_into_f64() {
299        let mut context = Context::new();
300        let observed = Rc::new(RefCell::new(Vec::new()));
301
302        PeriodicTimeTrigger::every(WrappedF64(1.0))
303            .start_at(WrappedF64(0.5))
304            .install(&mut context, {
305                let observed = Rc::clone(&observed);
306                move |_context, event| {
307                    observed
308                        .borrow_mut()
309                        .push(("at", event.time, event.period, event.phase));
310                }
311            });
312        PeriodicTimeTrigger::every_with_phase(WrappedF64(1.0), ExecutionPhase::Last)
313            .start_with_delay(WrappedF64(0.5))
314            .install(&mut context, {
315                let observed = Rc::clone(&observed);
316                move |_context, event| {
317                    observed
318                        .borrow_mut()
319                        .push(("delay", event.time, event.period, event.phase));
320                }
321            });
322        context.add_plan(1.5, |_| {});
323
324        context.execute();
325
326        assert_eq!(
327            *observed.borrow(),
328            vec![
329                ("at", 0.5, 1.0, ExecutionPhase::Normal),
330                ("delay", 0.5, 1.0, ExecutionPhase::Last),
331                ("at", 1.5, 1.0, ExecutionPhase::Normal),
332                ("delay", 1.5, 1.0, ExecutionPhase::Last),
333            ]
334        );
335    }
336
337    #[test]
338    fn register_property_value_count_trigger() {
339        let mut context = Context::new();
340
341        context.register_trigger(
342            PropertyValueCountTrigger::<Person, InfectionStatus>::increases_to(
343                InfectionStatus::Infectious,
344                100,
345            )
346            .emit_with(|event| InfectiousThresholdReached {
347                count: event.count,
348                mode: event.mode,
349            }),
350        );
351
352        context.subscribe_to_event(|context, _event: InfectiousThresholdReached| {
353            context.shutdown();
354        });
355    }
356
357    #[test]
358    fn register_entity_count_trigger() {
359        let mut context = Context::new();
360
361        context.register_trigger(
362            EntityCountTrigger::<Case>::increases_to(10).emit_default::<CaseThresholdReached>(),
363        );
364    }
365
366    #[test]
367    fn register_property_change_trigger() {
368        let mut context = Context::new();
369
370        context.register_trigger(
371            PropertyChangeTrigger::<Person, Alive>::to(Alive(false)).emit_with(|event| {
372                FirstDeath {
373                    person: event.entity_id,
374                    mode: event.mode,
375                }
376            }),
377        );
378    }
379
380    #[test]
381    fn register_time_trigger() {
382        let mut context = Context::new();
383
384        context.register_trigger(
385            TimeTrigger::at(50.0).emit_with(|event| StopTimeReached { phase: event.phase }),
386        );
387    }
388
389    #[test]
390    fn register_periodic_time_trigger_default_phase() {
391        let mut context = Context::new();
392
393        context.register_trigger(PeriodicTimeTrigger::every(1.0).emit_with(|event| {
394            PeriodicTimeReached {
395                time: event.time,
396                period: event.period,
397                phase: event.phase,
398            }
399        }));
400    }
401
402    #[test]
403    fn register_constant_event_value() {
404        #[derive(IxaEvent)]
405        struct ShutdownRequested;
406
407        let mut context = Context::new();
408
409        context.register_trigger(
410            TimeTrigger::at_phase(50.0, ExecutionPhase::Last)
411                .emit_value::<ShutdownRequested>(ShutdownRequested),
412        );
413    }
414
415    #[test]
416    fn time_trigger_with_phase_sets_phase() {
417        let mut context = Context::new();
418        let observed_phase = Rc::new(Cell::new(None));
419        let observed_phase_clone = Rc::clone(&observed_phase);
420
421        context.register_trigger(
422            TimeTrigger::at(1.0)
423                .with_phase(ExecutionPhase::Last)
424                .emit_with(|event| StopTimeReached { phase: event.phase }),
425        );
426        context.subscribe_to_event(move |_context, event: StopTimeReached| {
427            observed_phase_clone.set(Some(event.phase));
428        });
429
430        context.execute();
431
432        assert_eq!(observed_phase.get(), Some(ExecutionPhase::Last));
433    }
434
435    #[test]
436    fn entity_count_trigger_emits_at_threshold() {
437        let mut context = Context::new();
438        let observed_count = Rc::new(Cell::new(0));
439        let observed_count_clone = Rc::clone(&observed_count);
440
441        #[derive(IxaEvent)]
442        struct CountReached {
443            count: usize,
444        }
445
446        context.register_trigger(
447            EntityCountTrigger::<Case>::increases_to(2)
448                .emit_with(|event| CountReached { count: event.count }),
449        );
450        context.subscribe_to_event(move |_context, event: CountReached| {
451            observed_count_clone.set(event.count);
452        });
453
454        context.add_entity(Case).unwrap();
455        context.add_entity(Case).unwrap();
456        context.execute();
457
458        assert_eq!(observed_count.get(), 2);
459    }
460
461    #[test]
462    fn periodic_time_trigger_emits_on_current_time_then_periodically() {
463        let mut context = Context::new();
464        let observed = Rc::new(RefCell::new(Vec::new()));
465        let observed_clone = Rc::clone(&observed);
466
467        context.register_trigger(PeriodicTimeTrigger::every(1.0).emit_with(|event| {
468            PeriodicTimeReached {
469                time: event.time,
470                period: event.period,
471                phase: event.phase,
472            }
473        }));
474        context.subscribe_to_event(move |_context, event: PeriodicTimeReached| {
475            assert_eq!(event.period, 1.0);
476            assert_eq!(event.phase, ExecutionPhase::Normal);
477            observed_clone.borrow_mut().push(event.time);
478        });
479
480        context.add_plan(2.0, |_| {});
481        context.execute();
482
483        assert_eq!(*observed.borrow(), vec![0.0, 1.0, 2.0]);
484    }
485
486    #[test]
487    fn periodic_time_trigger_start_with_delay() {
488        let mut context = Context::new();
489        let observed = Rc::new(RefCell::new(Vec::new()));
490        let observed_clone = Rc::clone(&observed);
491
492        context.register_trigger(
493            PeriodicTimeTrigger::every(1.0)
494                .start_with_delay(0.5)
495                .emit_with(|event| PeriodicTimeReached {
496                    time: event.time,
497                    period: event.period,
498                    phase: event.phase,
499                }),
500        );
501        context.subscribe_to_event(move |_context, event: PeriodicTimeReached| {
502            observed_clone.borrow_mut().push(event.time);
503        });
504
505        context.add_plan(2.5, |_| {});
506        context.execute();
507
508        assert_eq!(*observed.borrow(), vec![0.5, 1.5, 2.5]);
509    }
510
511    #[test]
512    fn periodic_time_trigger_start_at() {
513        let mut context = Context::new();
514        let observed = Rc::new(RefCell::new(Vec::new()));
515        let observed_clone = Rc::clone(&observed);
516
517        context.register_trigger(PeriodicTimeTrigger::every(1.0).start_at(2.0).emit_with(
518            |event| PeriodicTimeReached {
519                time: event.time,
520                period: event.period,
521                phase: event.phase,
522            },
523        ));
524        context.subscribe_to_event(move |_context, event: PeriodicTimeReached| {
525            observed_clone.borrow_mut().push(event.time);
526        });
527
528        context.add_plan(4.0, |_| {});
529        context.execute();
530
531        assert_eq!(*observed.borrow(), vec![2.0, 3.0, 4.0]);
532    }
533
534    #[test]
535    fn periodic_time_trigger_uses_requested_phase() {
536        let mut context = Context::new();
537        let observed = Rc::new(RefCell::new(Vec::new()));
538
539        context.add_plan_with_phase(
540            1.0,
541            {
542                let observed = Rc::clone(&observed);
543                move |_| observed.borrow_mut().push("first")
544            },
545            ExecutionPhase::First,
546        );
547        context.add_plan_with_phase(
548            1.0,
549            {
550                let observed = Rc::clone(&observed);
551                move |_| observed.borrow_mut().push("normal")
552            },
553            ExecutionPhase::Normal,
554        );
555        context.add_plan_with_phase(
556            1.0,
557            {
558                let observed = Rc::clone(&observed);
559                move |_| observed.borrow_mut().push("last")
560            },
561            ExecutionPhase::Last,
562        );
563
564        PeriodicTimeTrigger::every_with_phase(1.0, ExecutionPhase::Last)
565            .start_at(1.0)
566            .install(&mut context, {
567                let observed = Rc::clone(&observed);
568                move |_context, event| {
569                    assert_eq!(event.phase, ExecutionPhase::Last);
570                    observed.borrow_mut().push("trigger");
571                }
572            });
573
574        context.execute();
575
576        assert_eq!(
577            *observed.borrow(),
578            vec!["first", "normal", "last", "trigger"]
579        );
580    }
581
582    #[test]
583    fn property_change_trigger_emits_matching_change() {
584        let mut context = Context::new();
585        let observed_person = Rc::new(Cell::new(None));
586        let observed_person_clone = Rc::clone(&observed_person);
587
588        #[derive(IxaEvent)]
589        struct BecameDead {
590            person: EntityId<Person>,
591        }
592
593        context.register_trigger(
594            PropertyChangeTrigger::<Person, Alive>::to(Alive(false)).emit_with(|event| {
595                BecameDead {
596                    person: event.entity_id,
597                }
598            }),
599        );
600        context.subscribe_to_event(move |_context, event: BecameDead| {
601            observed_person_clone.set(Some(event.person));
602        });
603
604        let person = context.add_entity(Person).unwrap();
605        context.set_property(person, Alive(false));
606        context.execute();
607
608        assert_eq!(observed_person.get(), Some(person));
609    }
610
611    #[test]
612    fn property_change_trigger_defaults_to_repeating() {
613        let mut context = Context::new();
614        let observed_count = Rc::new(Cell::new(0));
615        let observed_count_clone = Rc::clone(&observed_count);
616
617        #[derive(IxaEvent)]
618        struct BecameDead {
619            mode: TriggerMode,
620        }
621
622        context.register_trigger(
623            PropertyChangeTrigger::<Person, Alive>::from_to(Alive(true), Alive(false))
624                .emit_with(|event| BecameDead { mode: event.mode }),
625        );
626        context.subscribe_to_event(move |_context, event: BecameDead| {
627            assert_eq!(event.mode, TriggerMode::Repeating);
628            observed_count_clone.set(observed_count_clone.get() + 1);
629        });
630
631        let person = context.add_entity(Person).unwrap();
632        context.set_property(person, Alive(false));
633        context.set_property(person, Alive(true));
634        context.set_property(person, Alive(false));
635        context.execute();
636
637        assert_eq!(observed_count.get(), 2);
638    }
639
640    #[test]
641    fn property_value_count_trigger_defaults_to_repeating() {
642        let mut context = Context::new();
643        let observed_count = Rc::new(Cell::new(0));
644        let observed_count_clone = Rc::clone(&observed_count);
645
646        #[derive(IxaEvent)]
647        struct InfectiousThresholdReached {
648            mode: TriggerMode,
649        }
650
651        context.register_trigger(
652            PropertyValueCountTrigger::<Person, InfectionStatus>::increases_to(
653                InfectionStatus::Infectious,
654                2,
655            )
656            .emit_with(|event| InfectiousThresholdReached { mode: event.mode }),
657        );
658        context.subscribe_to_event(move |_context, event: InfectiousThresholdReached| {
659            assert_eq!(event.mode, TriggerMode::Repeating);
660            observed_count_clone.set(observed_count_clone.get() + 1);
661        });
662
663        let first = context.add_entity(Person).unwrap();
664        let second = context.add_entity(Person).unwrap();
665        context.add_plan(0.1, move |context| {
666            context.set_property(first, InfectionStatus::Infectious);
667        });
668        context.add_plan(0.2, move |context| {
669            context.set_property(second, InfectionStatus::Infectious);
670        });
671        context.add_plan(0.3, move |context| {
672            context.set_property(second, InfectionStatus::Susceptible);
673        });
674        context.add_plan(0.4, move |context| {
675            context.set_property(second, InfectionStatus::Infectious);
676        });
677        context.execute();
678
679        assert_eq!(observed_count.get(), 2);
680    }
681
682    #[test]
683    fn property_value_count_trigger_changes_to_emits_in_either_direction() {
684        let mut context = Context::new();
685        let observed_directions = Rc::new(RefCell::new(Vec::new()));
686        let observed_directions_clone = Rc::clone(&observed_directions);
687
688        #[derive(IxaEvent)]
689        struct InfectiousThresholdReached {
690            direction_filter: Option<Direction>,
691            direction: Direction,
692        }
693
694        context.register_trigger(
695            PropertyValueCountTrigger::<Person, InfectionStatus>::changes_to(
696                InfectionStatus::Infectious,
697                2,
698            )
699            .repeating()
700            .emit_with(|event| InfectiousThresholdReached {
701                direction_filter: event.direction_filter,
702                direction: event.direction,
703            }),
704        );
705        context.subscribe_to_event(move |_context, event: InfectiousThresholdReached| {
706            assert_eq!(event.direction_filter, None);
707            observed_directions_clone.borrow_mut().push(event.direction);
708        });
709
710        let first = context.add_entity(Person).unwrap();
711        let second = context.add_entity(Person).unwrap();
712        let third = context.add_entity(Person).unwrap();
713        context.add_plan(0.1, move |context| {
714            context.set_property(first, InfectionStatus::Infectious);
715        });
716        context.add_plan(0.2, move |context| {
717            context.set_property(second, InfectionStatus::Infectious);
718        });
719        context.add_plan(0.3, move |context| {
720            context.set_property(third, InfectionStatus::Infectious);
721        });
722        context.add_plan(0.4, move |context| {
723            context.set_property(second, InfectionStatus::Susceptible);
724        });
725        context.execute();
726
727        assert_eq!(
728            *observed_directions.borrow(),
729            vec![Direction::Increasing, Direction::Decreasing]
730        );
731    }
732
733    #[test]
734    fn property_value_count_trigger_changes_to_ignores_no_op_writes() {
735        let mut context = Context::new();
736        let observed_count = Rc::new(Cell::new(0));
737        let observed_count_clone = Rc::clone(&observed_count);
738
739        #[derive(IxaEvent)]
740        struct InfectiousThresholdReached;
741
742        context.register_trigger(
743            PropertyValueCountTrigger::<Person, InfectionStatus>::changes_to(
744                InfectionStatus::Infectious,
745                1,
746            )
747            .repeating()
748            .emit_value::<InfectiousThresholdReached>(InfectiousThresholdReached),
749        );
750        context.subscribe_to_event(move |_context, _event: InfectiousThresholdReached| {
751            observed_count_clone.set(observed_count_clone.get() + 1);
752        });
753
754        let person = context.add_entity(Person).unwrap();
755        context.add_plan(0.1, move |context| {
756            context.set_property(person, InfectionStatus::Infectious);
757        });
758        context.add_plan(0.2, move |context| {
759            context.set_property(person, InfectionStatus::Infectious);
760        });
761        context.execute();
762
763        assert_eq!(observed_count.get(), 1);
764    }
765
766    #[test]
767    fn property_value_count_decreases_to_tracks_entities_created_with_tracked_value() {
768        let mut context = Context::new();
769        let observed = Rc::new(RefCell::new(Vec::new()));
770        let observed_clone = Rc::clone(&observed);
771
772        #[derive(IxaEvent)]
773        struct InfectiousThresholdReached {
774            count: usize,
775            direction: Direction,
776        }
777
778        context.register_trigger(
779            PropertyValueCountTrigger::<Person, InfectionStatus>::decreases_to(
780                InfectionStatus::Infectious,
781                1,
782            )
783            .repeating()
784            .emit_with(|event| InfectiousThresholdReached {
785                count: event.count,
786                direction: event.direction,
787            }),
788        );
789        context.subscribe_to_event(move |_context, event: InfectiousThresholdReached| {
790            observed_clone
791                .borrow_mut()
792                .push((event.count, event.direction));
793        });
794
795        let first = context
796            .add_entity(with!(Person, InfectionStatus::Infectious))
797            .unwrap();
798        let _second = context
799            .add_entity(with!(Person, InfectionStatus::Infectious))
800            .unwrap();
801        context.add_plan(0.1, move |context| {
802            context.set_property(first, InfectionStatus::Susceptible);
803        });
804        context.execute();
805
806        assert_eq!(*observed.borrow(), vec![(1, Direction::Decreasing)]);
807    }
808
809    #[test]
810    #[should_panic(expected = "period must be greater than 0")]
811    fn periodic_time_trigger_zero_period_panics() {
812        let _ = PeriodicTimeTrigger::every(0.0);
813    }
814
815    #[test]
816    #[should_panic(expected = "period must be greater than 0")]
817    fn periodic_time_trigger_negative_period_panics() {
818        let _ = PeriodicTimeTrigger::every(-1.0);
819    }
820
821    #[test]
822    #[should_panic(expected = "period must be greater than 0")]
823    fn periodic_time_trigger_nan_period_panics() {
824        let _ = PeriodicTimeTrigger::every(f64::NAN);
825    }
826
827    #[test]
828    #[should_panic(expected = "period must be greater than 0")]
829    fn periodic_time_trigger_infinite_period_panics() {
830        let _ = PeriodicTimeTrigger::every(f64::INFINITY);
831    }
832
833    #[test]
834    #[should_panic(expected = "period must be greater than 0")]
835    fn periodic_time_trigger_every_with_phase_validates_period() {
836        let _ = PeriodicTimeTrigger::every_with_phase(0.0, ExecutionPhase::Last);
837    }
838
839    #[test]
840    #[should_panic(expected = "delay must be greater than or equal to 0")]
841    fn periodic_time_trigger_negative_delay_panics() {
842        let _ = PeriodicTimeTrigger::every(1.0).start_with_delay(-1.0);
843    }
844
845    #[test]
846    #[should_panic(expected = "delay must be greater than or equal to 0")]
847    fn periodic_time_trigger_nan_delay_panics() {
848        let _ = PeriodicTimeTrigger::every(1.0).start_with_delay(f64::NAN);
849    }
850
851    #[test]
852    #[should_panic(expected = "delay must be greater than or equal to 0")]
853    fn periodic_time_trigger_infinite_delay_panics() {
854        let _ = PeriodicTimeTrigger::every(1.0).start_with_delay(f64::INFINITY);
855    }
856
857    #[test]
858    #[should_panic(expected = "delay must be greater than or equal to 0")]
859    fn periodic_time_trigger_rejects_wrapped_invalid_delay() {
860        let _ = PeriodicTimeTrigger::every(1.0).start_with_delay(WrappedF64(-1.0));
861    }
862
863    #[test]
864    #[should_panic(expected = "cannot be NaN")]
865    fn periodic_time_trigger_nan_start_time_panics() {
866        let _ = PeriodicTimeTrigger::every(1.0).start_at(f64::NAN);
867    }
868
869    #[test]
870    #[should_panic(expected = "cannot be infinite")]
871    fn periodic_time_trigger_infinite_start_time_panics() {
872        let _ = PeriodicTimeTrigger::every(1.0).start_at(f64::INFINITY);
873    }
874
875    #[test]
876    #[should_panic(expected = "cannot be less than the current time")]
877    fn periodic_time_trigger_start_at_past_panics() {
878        let mut context = Context::new();
879        context.add_plan(1.0, |_| {});
880        context.execute();
881
882        context.register_trigger(
883            PeriodicTimeTrigger::every(1.0)
884                .start_at(0.5)
885                .emit_value::<CaseThresholdReached>(CaseThresholdReached),
886        );
887    }
888}