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`](crate::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`](crate::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    #[test]
263    fn register_property_value_count_trigger() {
264        let mut context = Context::new();
265
266        context.register_trigger(
267            PropertyValueCountTrigger::<Person, InfectionStatus>::increases_to(
268                InfectionStatus::Infectious,
269                100,
270            )
271            .emit_with(|event| InfectiousThresholdReached {
272                count: event.count,
273                mode: event.mode,
274            }),
275        );
276
277        context.subscribe_to_event(|context, _event: InfectiousThresholdReached| {
278            context.shutdown();
279        });
280    }
281
282    #[test]
283    fn register_entity_count_trigger() {
284        let mut context = Context::new();
285
286        context.register_trigger(
287            EntityCountTrigger::<Case>::increases_to(10).emit_default::<CaseThresholdReached>(),
288        );
289    }
290
291    #[test]
292    fn register_property_change_trigger() {
293        let mut context = Context::new();
294
295        context.register_trigger(
296            PropertyChangeTrigger::<Person, Alive>::to(Alive(false)).emit_with(|event| {
297                FirstDeath {
298                    person: event.entity_id,
299                    mode: event.mode,
300                }
301            }),
302        );
303    }
304
305    #[test]
306    fn register_time_trigger() {
307        let mut context = Context::new();
308
309        context.register_trigger(
310            TimeTrigger::at(50.0).emit_with(|event| StopTimeReached { phase: event.phase }),
311        );
312    }
313
314    #[test]
315    fn register_periodic_time_trigger_default_phase() {
316        let mut context = Context::new();
317
318        context.register_trigger(PeriodicTimeTrigger::every(1.0).emit_with(|event| {
319            PeriodicTimeReached {
320                time: event.time,
321                period: event.period,
322                phase: event.phase,
323            }
324        }));
325    }
326
327    #[test]
328    fn register_constant_event_value() {
329        #[derive(IxaEvent)]
330        struct ShutdownRequested;
331
332        let mut context = Context::new();
333
334        context.register_trigger(
335            TimeTrigger::at_phase(50.0, ExecutionPhase::Last)
336                .emit_value::<ShutdownRequested>(ShutdownRequested),
337        );
338    }
339
340    #[test]
341    fn time_trigger_with_phase_sets_phase() {
342        let mut context = Context::new();
343        let observed_phase = Rc::new(Cell::new(None));
344        let observed_phase_clone = Rc::clone(&observed_phase);
345
346        context.register_trigger(
347            TimeTrigger::at(1.0)
348                .with_phase(ExecutionPhase::Last)
349                .emit_with(|event| StopTimeReached { phase: event.phase }),
350        );
351        context.subscribe_to_event(move |_context, event: StopTimeReached| {
352            observed_phase_clone.set(Some(event.phase));
353        });
354
355        context.execute();
356
357        assert_eq!(observed_phase.get(), Some(ExecutionPhase::Last));
358    }
359
360    #[test]
361    fn entity_count_trigger_emits_at_threshold() {
362        let mut context = Context::new();
363        let observed_count = Rc::new(Cell::new(0));
364        let observed_count_clone = Rc::clone(&observed_count);
365
366        #[derive(IxaEvent)]
367        struct CountReached {
368            count: usize,
369        }
370
371        context.register_trigger(
372            EntityCountTrigger::<Case>::increases_to(2)
373                .emit_with(|event| CountReached { count: event.count }),
374        );
375        context.subscribe_to_event(move |_context, event: CountReached| {
376            observed_count_clone.set(event.count);
377        });
378
379        context.add_entity(Case).unwrap();
380        context.add_entity(Case).unwrap();
381        context.execute();
382
383        assert_eq!(observed_count.get(), 2);
384    }
385
386    #[test]
387    fn periodic_time_trigger_emits_on_current_time_then_periodically() {
388        let mut context = Context::new();
389        let observed = Rc::new(RefCell::new(Vec::new()));
390        let observed_clone = Rc::clone(&observed);
391
392        context.register_trigger(PeriodicTimeTrigger::every(1.0).emit_with(|event| {
393            PeriodicTimeReached {
394                time: event.time,
395                period: event.period,
396                phase: event.phase,
397            }
398        }));
399        context.subscribe_to_event(move |_context, event: PeriodicTimeReached| {
400            assert_eq!(event.period, 1.0);
401            assert_eq!(event.phase, ExecutionPhase::Normal);
402            observed_clone.borrow_mut().push(event.time);
403        });
404
405        context.add_plan(2.0, |_| {});
406        context.execute();
407
408        assert_eq!(*observed.borrow(), vec![0.0, 1.0, 2.0]);
409    }
410
411    #[test]
412    fn periodic_time_trigger_start_with_delay() {
413        let mut context = Context::new();
414        let observed = Rc::new(RefCell::new(Vec::new()));
415        let observed_clone = Rc::clone(&observed);
416
417        context.register_trigger(
418            PeriodicTimeTrigger::every(1.0)
419                .start_with_delay(0.5)
420                .emit_with(|event| PeriodicTimeReached {
421                    time: event.time,
422                    period: event.period,
423                    phase: event.phase,
424                }),
425        );
426        context.subscribe_to_event(move |_context, event: PeriodicTimeReached| {
427            observed_clone.borrow_mut().push(event.time);
428        });
429
430        context.add_plan(2.5, |_| {});
431        context.execute();
432
433        assert_eq!(*observed.borrow(), vec![0.5, 1.5, 2.5]);
434    }
435
436    #[test]
437    fn periodic_time_trigger_start_at() {
438        let mut context = Context::new();
439        let observed = Rc::new(RefCell::new(Vec::new()));
440        let observed_clone = Rc::clone(&observed);
441
442        context.register_trigger(PeriodicTimeTrigger::every(1.0).start_at(2.0).emit_with(
443            |event| PeriodicTimeReached {
444                time: event.time,
445                period: event.period,
446                phase: event.phase,
447            },
448        ));
449        context.subscribe_to_event(move |_context, event: PeriodicTimeReached| {
450            observed_clone.borrow_mut().push(event.time);
451        });
452
453        context.add_plan(4.0, |_| {});
454        context.execute();
455
456        assert_eq!(*observed.borrow(), vec![2.0, 3.0, 4.0]);
457    }
458
459    #[test]
460    fn periodic_time_trigger_uses_requested_phase() {
461        let mut context = Context::new();
462        let observed = Rc::new(RefCell::new(Vec::new()));
463
464        context.add_plan_with_phase(
465            1.0,
466            {
467                let observed = Rc::clone(&observed);
468                move |_| observed.borrow_mut().push("first")
469            },
470            ExecutionPhase::First,
471        );
472        context.add_plan_with_phase(
473            1.0,
474            {
475                let observed = Rc::clone(&observed);
476                move |_| observed.borrow_mut().push("normal")
477            },
478            ExecutionPhase::Normal,
479        );
480        context.add_plan_with_phase(
481            1.0,
482            {
483                let observed = Rc::clone(&observed);
484                move |_| observed.borrow_mut().push("last")
485            },
486            ExecutionPhase::Last,
487        );
488
489        PeriodicTimeTrigger::every_with_phase(1.0, ExecutionPhase::Last)
490            .start_at(1.0)
491            .install(&mut context, {
492                let observed = Rc::clone(&observed);
493                move |_context, event| {
494                    assert_eq!(event.phase, ExecutionPhase::Last);
495                    observed.borrow_mut().push("trigger");
496                }
497            });
498
499        context.execute();
500
501        assert_eq!(
502            *observed.borrow(),
503            vec!["first", "normal", "last", "trigger"]
504        );
505    }
506
507    #[test]
508    fn property_change_trigger_emits_matching_change() {
509        let mut context = Context::new();
510        let observed_person = Rc::new(Cell::new(None));
511        let observed_person_clone = Rc::clone(&observed_person);
512
513        #[derive(IxaEvent)]
514        struct BecameDead {
515            person: EntityId<Person>,
516        }
517
518        context.register_trigger(
519            PropertyChangeTrigger::<Person, Alive>::to(Alive(false)).emit_with(|event| {
520                BecameDead {
521                    person: event.entity_id,
522                }
523            }),
524        );
525        context.subscribe_to_event(move |_context, event: BecameDead| {
526            observed_person_clone.set(Some(event.person));
527        });
528
529        let person = context.add_entity(Person).unwrap();
530        context.set_property(person, Alive(false));
531        context.execute();
532
533        assert_eq!(observed_person.get(), Some(person));
534    }
535
536    #[test]
537    fn property_change_trigger_defaults_to_repeating() {
538        let mut context = Context::new();
539        let observed_count = Rc::new(Cell::new(0));
540        let observed_count_clone = Rc::clone(&observed_count);
541
542        #[derive(IxaEvent)]
543        struct BecameDead {
544            mode: TriggerMode,
545        }
546
547        context.register_trigger(
548            PropertyChangeTrigger::<Person, Alive>::from_to(Alive(true), Alive(false))
549                .emit_with(|event| BecameDead { mode: event.mode }),
550        );
551        context.subscribe_to_event(move |_context, event: BecameDead| {
552            assert_eq!(event.mode, TriggerMode::Repeating);
553            observed_count_clone.set(observed_count_clone.get() + 1);
554        });
555
556        let person = context.add_entity(Person).unwrap();
557        context.set_property(person, Alive(false));
558        context.set_property(person, Alive(true));
559        context.set_property(person, Alive(false));
560        context.execute();
561
562        assert_eq!(observed_count.get(), 2);
563    }
564
565    #[test]
566    fn property_value_count_trigger_defaults_to_repeating() {
567        let mut context = Context::new();
568        let observed_count = Rc::new(Cell::new(0));
569        let observed_count_clone = Rc::clone(&observed_count);
570
571        #[derive(IxaEvent)]
572        struct InfectiousThresholdReached {
573            mode: TriggerMode,
574        }
575
576        context.register_trigger(
577            PropertyValueCountTrigger::<Person, InfectionStatus>::increases_to(
578                InfectionStatus::Infectious,
579                2,
580            )
581            .emit_with(|event| InfectiousThresholdReached { mode: event.mode }),
582        );
583        context.subscribe_to_event(move |_context, event: InfectiousThresholdReached| {
584            assert_eq!(event.mode, TriggerMode::Repeating);
585            observed_count_clone.set(observed_count_clone.get() + 1);
586        });
587
588        let first = context.add_entity(Person).unwrap();
589        let second = context.add_entity(Person).unwrap();
590        context.add_plan(0.1, move |context| {
591            context.set_property(first, InfectionStatus::Infectious);
592        });
593        context.add_plan(0.2, move |context| {
594            context.set_property(second, InfectionStatus::Infectious);
595        });
596        context.add_plan(0.3, move |context| {
597            context.set_property(second, InfectionStatus::Susceptible);
598        });
599        context.add_plan(0.4, move |context| {
600            context.set_property(second, InfectionStatus::Infectious);
601        });
602        context.execute();
603
604        assert_eq!(observed_count.get(), 2);
605    }
606
607    #[test]
608    fn property_value_count_trigger_changes_to_emits_in_either_direction() {
609        let mut context = Context::new();
610        let observed_directions = Rc::new(RefCell::new(Vec::new()));
611        let observed_directions_clone = Rc::clone(&observed_directions);
612
613        #[derive(IxaEvent)]
614        struct InfectiousThresholdReached {
615            direction_filter: Option<Direction>,
616            direction: Direction,
617        }
618
619        context.register_trigger(
620            PropertyValueCountTrigger::<Person, InfectionStatus>::changes_to(
621                InfectionStatus::Infectious,
622                2,
623            )
624            .repeating()
625            .emit_with(|event| InfectiousThresholdReached {
626                direction_filter: event.direction_filter,
627                direction: event.direction,
628            }),
629        );
630        context.subscribe_to_event(move |_context, event: InfectiousThresholdReached| {
631            assert_eq!(event.direction_filter, None);
632            observed_directions_clone.borrow_mut().push(event.direction);
633        });
634
635        let first = context.add_entity(Person).unwrap();
636        let second = context.add_entity(Person).unwrap();
637        let third = context.add_entity(Person).unwrap();
638        context.add_plan(0.1, move |context| {
639            context.set_property(first, InfectionStatus::Infectious);
640        });
641        context.add_plan(0.2, move |context| {
642            context.set_property(second, InfectionStatus::Infectious);
643        });
644        context.add_plan(0.3, move |context| {
645            context.set_property(third, InfectionStatus::Infectious);
646        });
647        context.add_plan(0.4, move |context| {
648            context.set_property(second, InfectionStatus::Susceptible);
649        });
650        context.execute();
651
652        assert_eq!(
653            *observed_directions.borrow(),
654            vec![Direction::Increasing, Direction::Decreasing]
655        );
656    }
657
658    #[test]
659    fn property_value_count_trigger_changes_to_ignores_no_op_writes() {
660        let mut context = Context::new();
661        let observed_count = Rc::new(Cell::new(0));
662        let observed_count_clone = Rc::clone(&observed_count);
663
664        #[derive(IxaEvent)]
665        struct InfectiousThresholdReached;
666
667        context.register_trigger(
668            PropertyValueCountTrigger::<Person, InfectionStatus>::changes_to(
669                InfectionStatus::Infectious,
670                1,
671            )
672            .repeating()
673            .emit_value::<InfectiousThresholdReached>(InfectiousThresholdReached),
674        );
675        context.subscribe_to_event(move |_context, _event: InfectiousThresholdReached| {
676            observed_count_clone.set(observed_count_clone.get() + 1);
677        });
678
679        let person = context.add_entity(Person).unwrap();
680        context.add_plan(0.1, move |context| {
681            context.set_property(person, InfectionStatus::Infectious);
682        });
683        context.add_plan(0.2, move |context| {
684            context.set_property(person, InfectionStatus::Infectious);
685        });
686        context.execute();
687
688        assert_eq!(observed_count.get(), 1);
689    }
690
691    #[test]
692    fn property_value_count_decreases_to_tracks_entities_created_with_tracked_value() {
693        let mut context = Context::new();
694        let observed = Rc::new(RefCell::new(Vec::new()));
695        let observed_clone = Rc::clone(&observed);
696
697        #[derive(IxaEvent)]
698        struct InfectiousThresholdReached {
699            count: usize,
700            direction: Direction,
701        }
702
703        context.register_trigger(
704            PropertyValueCountTrigger::<Person, InfectionStatus>::decreases_to(
705                InfectionStatus::Infectious,
706                1,
707            )
708            .repeating()
709            .emit_with(|event| InfectiousThresholdReached {
710                count: event.count,
711                direction: event.direction,
712            }),
713        );
714        context.subscribe_to_event(move |_context, event: InfectiousThresholdReached| {
715            observed_clone
716                .borrow_mut()
717                .push((event.count, event.direction));
718        });
719
720        let first = context
721            .add_entity(with!(Person, InfectionStatus::Infectious))
722            .unwrap();
723        let _second = context
724            .add_entity(with!(Person, InfectionStatus::Infectious))
725            .unwrap();
726        context.add_plan(0.1, move |context| {
727            context.set_property(first, InfectionStatus::Susceptible);
728        });
729        context.execute();
730
731        assert_eq!(*observed.borrow(), vec![(1, Direction::Decreasing)]);
732    }
733
734    #[test]
735    #[should_panic(expected = "period must be greater than 0")]
736    fn periodic_time_trigger_zero_period_panics() {
737        let _ = PeriodicTimeTrigger::every(0.0);
738    }
739
740    #[test]
741    #[should_panic(expected = "period must be greater than 0")]
742    fn periodic_time_trigger_negative_period_panics() {
743        let _ = PeriodicTimeTrigger::every(-1.0);
744    }
745
746    #[test]
747    #[should_panic(expected = "period must be greater than 0")]
748    fn periodic_time_trigger_nan_period_panics() {
749        let _ = PeriodicTimeTrigger::every(f64::NAN);
750    }
751
752    #[test]
753    #[should_panic(expected = "period must be greater than 0")]
754    fn periodic_time_trigger_infinite_period_panics() {
755        let _ = PeriodicTimeTrigger::every(f64::INFINITY);
756    }
757
758    #[test]
759    #[should_panic(expected = "period must be greater than 0")]
760    fn periodic_time_trigger_every_with_phase_validates_period() {
761        let _ = PeriodicTimeTrigger::every_with_phase(0.0, ExecutionPhase::Last);
762    }
763
764    #[test]
765    #[should_panic(expected = "delay must be greater than or equal to 0")]
766    fn periodic_time_trigger_negative_delay_panics() {
767        let _ = PeriodicTimeTrigger::every(1.0).start_with_delay(-1.0);
768    }
769
770    #[test]
771    #[should_panic(expected = "delay must be greater than or equal to 0")]
772    fn periodic_time_trigger_nan_delay_panics() {
773        let _ = PeriodicTimeTrigger::every(1.0).start_with_delay(f64::NAN);
774    }
775
776    #[test]
777    #[should_panic(expected = "delay must be greater than or equal to 0")]
778    fn periodic_time_trigger_infinite_delay_panics() {
779        let _ = PeriodicTimeTrigger::every(1.0).start_with_delay(f64::INFINITY);
780    }
781
782    #[test]
783    #[should_panic(expected = "cannot be NaN")]
784    fn periodic_time_trigger_nan_start_time_panics() {
785        let _ = PeriodicTimeTrigger::every(1.0).start_at(f64::NAN);
786    }
787
788    #[test]
789    #[should_panic(expected = "cannot be infinite")]
790    fn periodic_time_trigger_infinite_start_time_panics() {
791        let _ = PeriodicTimeTrigger::every(1.0).start_at(f64::INFINITY);
792    }
793
794    #[test]
795    #[should_panic(expected = "cannot be less than the current time")]
796    fn periodic_time_trigger_start_at_past_panics() {
797        let mut context = Context::new();
798        context.add_plan(1.0, |_| {});
799        context.execute();
800
801        context.register_trigger(
802            PeriodicTimeTrigger::every(1.0)
803                .start_at(0.5)
804                .emit_value::<CaseThresholdReached>(CaseThresholdReached),
805        );
806    }
807}