1mod 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum Direction {
104 Increasing,
105 Decreasing,
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum TriggerMode {
111 Once,
112 Repeating,
113}
114
115pub trait TriggerCriterion: Sized + 'static {
118 type Observation: 'static;
121
122 fn install<F>(self, context: &mut Context, on_match: F)
124 where
125 F: Fn(&mut Context, Self::Observation) + 'static;
126
127 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 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 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
157pub trait TriggerSpec: Sized {
161 fn install_in_context(self, context: &mut Context);
162}
163
164pub 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
188pub 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}