1use std::{
23 cmp::Ordering,
24 fmt::{Debug, Display},
25 num::NonZeroU64,
26 rc::Rc,
27 sync::Arc,
28};
29
30use nautilus_core::{
31 DurationNanos, UUID4, UnixNanos,
32 correctness::{FAILED, check_valid_string_utf8},
33};
34#[cfg(feature = "python")]
35use pyo3::{Py, PyAny, Python};
36use ustr::Ustr;
37
38#[must_use]
40pub fn create_valid_interval(interval_ns: DurationNanos) -> NonZeroU64 {
41 NonZeroU64::new(interval_ns.as_u64()).unwrap_or(NonZeroU64::MIN)
42}
43
44#[repr(C)]
45#[derive(Clone, Debug, PartialEq, Eq)]
46#[cfg_attr(
47 feature = "python",
48 pyo3::pyclass(module = "nautilus_trader.common", from_py_object)
49)]
50#[cfg_attr(
51 feature = "python",
52 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
53)]
54pub struct TimeEvent {
59 pub name: Ustr,
61 pub event_id: UUID4,
63 pub ts_event: UnixNanos,
65 pub ts_init: UnixNanos,
67}
68
69impl TimeEvent {
70 #[must_use]
72 pub const fn new(name: Ustr, event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos) -> Self {
73 Self {
74 name,
75 event_id,
76 ts_event,
77 ts_init,
78 }
79 }
80}
81
82impl Display for TimeEvent {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 write!(
85 f,
86 "{}(name={}, event_id={}, ts_event={}, ts_init={})",
87 stringify!(TimeEvent),
88 self.name,
89 self.event_id,
90 self.ts_event,
91 self.ts_init
92 )
93 }
94}
95
96#[repr(transparent)] #[derive(Clone, Debug, PartialEq, Eq)]
103pub struct ScheduledTimeEvent(
104 pub TimeEvent,
106);
107
108impl ScheduledTimeEvent {
109 #[must_use]
111 pub const fn new(event: TimeEvent) -> Self {
112 Self(event)
113 }
114
115 #[must_use]
117 pub fn into_inner(self) -> TimeEvent {
118 self.0
119 }
120}
121
122impl PartialOrd for ScheduledTimeEvent {
123 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
124 Some(self.cmp(other))
125 }
126}
127
128impl Ord for ScheduledTimeEvent {
129 fn cmp(&self, other: &Self) -> Ordering {
130 cmp_time_events(&other.0, &self.0)
132 }
133}
134
135#[cfg(feature = "python")]
136pub struct PythonTimeEventCallback {
138 callback: Py<PyAny>,
139}
140
141#[cfg(feature = "python")]
142impl PythonTimeEventCallback {
143 #[must_use]
145 pub const fn new(callback: Py<PyAny>) -> Self {
146 Self { callback }
147 }
148
149 pub fn call(&self, event: TimeEvent) {
153 Python::attach(|py| {
154 if let Err(e) = self.callback.call1(py, (event,)) {
155 log::error!("Python time event callback raised exception: {e}");
156 }
157 });
158 }
159}
160
161#[cfg(feature = "python")]
162impl Debug for PythonTimeEventCallback {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 f.debug_struct(stringify!(PythonTimeEventCallback))
165 .finish_non_exhaustive()
166 }
167}
168
169#[derive(Clone)]
170pub enum TimeEventCallback {
197 #[cfg(feature = "python")]
199 Python(Arc<PythonTimeEventCallback>),
200 Rust(Arc<dyn Fn(TimeEvent) + Send + Sync>),
202 RustLocal(Rc<dyn Fn(TimeEvent)>),
204}
205
206impl Debug for TimeEventCallback {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 match self {
209 #[cfg(feature = "python")]
210 Self::Python(_) => f.write_str("Python callback"),
211 Self::Rust(_) => f.write_str("Rust callback (thread-safe)"),
212 Self::RustLocal(_) => f.write_str("Rust callback (local)"),
213 }
214 }
215}
216
217impl TimeEventCallback {
218 #[must_use]
223 pub const fn is_local(&self) -> bool {
224 matches!(self, Self::RustLocal(_))
225 }
226
227 pub fn call(&self, event: TimeEvent) {
235 match self {
236 #[cfg(feature = "python")]
237 Self::Python(callback) => callback.call(event),
238 Self::Rust(callback) => callback(event),
239 Self::RustLocal(callback) => callback(event),
240 }
241 }
242}
243
244impl<F> From<F> for TimeEventCallback
245where
246 F: Fn(TimeEvent) + Send + Sync + 'static,
247{
248 fn from(value: F) -> Self {
249 Self::Rust(Arc::new(value))
250 }
251}
252
253impl From<Arc<dyn Fn(TimeEvent) + Send + Sync>> for TimeEventCallback {
254 fn from(value: Arc<dyn Fn(TimeEvent) + Send + Sync>) -> Self {
255 Self::Rust(value)
256 }
257}
258
259impl From<Rc<dyn Fn(TimeEvent)>> for TimeEventCallback {
260 fn from(value: Rc<dyn Fn(TimeEvent)>) -> Self {
261 Self::RustLocal(value)
262 }
263}
264
265#[cfg(feature = "python")]
266impl From<Py<PyAny>> for TimeEventCallback {
267 fn from(value: Py<PyAny>) -> Self {
268 Self::from_python_time_event(value)
269 }
270}
271
272#[cfg(feature = "python")]
273impl TimeEventCallback {
274 #[must_use]
276 pub fn from_python_time_event(callback: Py<PyAny>) -> Self {
277 Self::Python(Arc::new(PythonTimeEventCallback::new(callback)))
278 }
279}
280
281#[repr(C)]
282#[derive(Clone, Debug)]
283pub struct TimeEventHandler {
287 pub event: TimeEvent,
289 pub callback: TimeEventCallback,
291}
292
293impl TimeEventHandler {
294 #[must_use]
296 pub const fn new(event: TimeEvent, callback: TimeEventCallback) -> Self {
297 Self { event, callback }
298 }
299
300 pub fn run(self) {
306 let Self { event, callback } = self;
307 crate::msgbus::dispatch_tap_time_event(&event);
308 callback.call(event);
309 }
310}
311
312impl PartialOrd for TimeEventHandler {
313 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
314 Some(self.cmp(other))
315 }
316}
317
318impl PartialEq for TimeEventHandler {
319 fn eq(&self, other: &Self) -> bool {
320 self.cmp(other).is_eq()
321 }
322}
323
324impl Eq for TimeEventHandler {}
325
326impl Ord for TimeEventHandler {
327 fn cmp(&self, other: &Self) -> Ordering {
328 cmp_time_events(&self.event, &other.event)
329 }
330}
331
332fn cmp_time_events(left: &TimeEvent, right: &TimeEvent) -> Ordering {
333 left.ts_event
334 .cmp(&right.ts_event)
335 .then_with(|| left.name.cmp(&right.name))
336 .then_with(|| left.ts_init.cmp(&right.ts_init))
337 .then_with(|| left.event_id.as_str().cmp(right.event_id.as_str()))
338}
339
340pub(crate) trait Timer {
341 fn is_expired(&self) -> bool;
342 fn cancel(&mut self);
343}
344
345#[derive(Clone, Debug)]
350pub struct TestTimer {
351 pub name: Ustr,
353 pub interval_ns: NonZeroU64,
355 pub start_time_ns: UnixNanos,
357 pub stop_time_ns: Option<UnixNanos>,
359 pub fire_immediately: bool,
361 next_time_ns: UnixNanos,
362 is_expired: bool,
363}
364
365impl TestTimer {
366 #[must_use]
375 pub fn new(
376 name: Ustr,
377 interval_ns: NonZeroU64,
378 start_time_ns: UnixNanos,
379 stop_time_ns: Option<UnixNanos>,
380 fire_immediately: bool,
381 ) -> Self {
382 check_valid_string_utf8(name, stringify!(name)).expect(FAILED);
383
384 let next_time_ns = if fire_immediately {
385 start_time_ns
386 } else {
387 start_time_ns + DurationNanos::new(interval_ns.get())
388 };
389
390 Self {
391 name,
392 interval_ns,
393 start_time_ns,
394 stop_time_ns,
395 fire_immediately,
396 next_time_ns,
397 is_expired: false,
398 }
399 }
400
401 #[must_use]
403 pub const fn next_time_ns(&self) -> UnixNanos {
404 self.next_time_ns
405 }
406
407 #[must_use]
409 pub const fn is_expired(&self) -> bool {
410 self.is_expired
411 }
412
413 pub fn advance(&mut self, to_time_ns: UnixNanos) -> impl Iterator<Item = TimeEvent> + '_ {
418 let advances = if self.next_time_ns <= to_time_ns {
420 ((to_time_ns - self.next_time_ns).as_u64() / self.interval_ns.get()).saturating_add(1)
421 } else {
422 0
423 };
424
425 self.take(advances as usize).map(|(event, _)| event)
426 }
427
428 pub const fn cancel(&mut self) {
430 self.is_expired = true;
431 }
432}
433
434impl Timer for TestTimer {
435 fn is_expired(&self) -> bool {
436 Self::is_expired(self)
437 }
438
439 fn cancel(&mut self) {
440 Self::cancel(self);
441 }
442}
443
444impl Iterator for TestTimer {
445 type Item = (TimeEvent, UnixNanos);
446
447 fn next(&mut self) -> Option<Self::Item> {
448 if self.is_expired {
449 return None;
450 }
451
452 if let Some(stop_time_ns) = self.stop_time_ns
454 && self.next_time_ns > stop_time_ns
455 {
456 self.is_expired = true;
457 return None;
458 }
459
460 let event_time_ns = self.next_time_ns;
461
462 let item = (
463 TimeEvent {
464 name: self.name,
465 event_id: UUID4::new(),
466 ts_event: event_time_ns,
467 ts_init: event_time_ns,
468 },
469 event_time_ns,
470 );
471
472 if let Some(following_time_ns) =
473 event_time_ns.checked_add(DurationNanos::new(self.interval_ns.get()))
474 {
475 self.next_time_ns = following_time_ns;
476 } else {
477 self.is_expired = true;
478 }
479
480 if self.stop_time_ns == Some(event_time_ns) {
481 self.is_expired = true;
482 }
483
484 Some(item)
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use std::{cell::RefCell, collections::BinaryHeap, num::NonZeroU64, rc::Rc};
491
492 use nautilus_core::{DurationNanos, UUID4, UnixNanos};
493 #[cfg(feature = "python")]
494 use pyo3::{
495 Bound, PyResult, Python,
496 types::{
497 PyAnyMethods, PyCFunction, PyDict, PyList, PyListMethods, PyTuple, PyTupleMethods,
498 PyTypeMethods,
499 },
500 };
501 use rstest::*;
502 use ustr::Ustr;
503
504 use super::{
505 ScheduledTimeEvent, TestTimer, TimeEvent, TimeEventCallback, TimeEventHandler,
506 create_valid_interval,
507 };
508 use crate::msgbus::{
509 BusTap, Endpoint, MStr, MessagingSwitchboard, Topic, clear_bus_tap, set_bus_tap,
510 };
511
512 #[rstest]
513 #[case(0, 1)]
514 #[case(1, 1)]
515 #[case(25, 25)]
516 fn test_create_valid_interval(#[case] interval_ns: u64, #[case] expected: u64) {
517 assert_eq!(
518 create_valid_interval(DurationNanos::new(interval_ns)).get(),
519 expected
520 );
521 }
522
523 #[rstest]
524 fn test_test_timer_advance_within_next_time_ns() {
525 let mut timer = TestTimer::new(
526 Ustr::from("TEST_TIMER"),
527 NonZeroU64::new(5).unwrap(),
528 UnixNanos::default(),
529 None,
530 false,
531 );
532 let _: Vec<TimeEvent> = timer.advance(UnixNanos::from(1)).collect();
533 let _: Vec<TimeEvent> = timer.advance(UnixNanos::from(2)).collect();
534 let _: Vec<TimeEvent> = timer.advance(UnixNanos::from(3)).collect();
535 assert_eq!(timer.advance(UnixNanos::from(4)).count(), 0);
536 assert_eq!(timer.next_time_ns, 5);
537 assert!(!timer.is_expired);
538 }
539
540 #[rstest]
541 fn test_test_timer_advance_up_to_next_time_ns() {
542 let mut timer = TestTimer::new(
543 Ustr::from("TEST_TIMER"),
544 NonZeroU64::new(1).unwrap(),
545 UnixNanos::default(),
546 None,
547 false,
548 );
549 assert_eq!(timer.advance(UnixNanos::from(1)).count(), 1);
550 assert!(!timer.is_expired);
551 }
552
553 #[rstest]
554 fn test_test_timer_advance_up_to_next_time_ns_with_stop_time() {
555 let mut timer = TestTimer::new(
556 Ustr::from("TEST_TIMER"),
557 NonZeroU64::new(1).unwrap(),
558 UnixNanos::default(),
559 Some(UnixNanos::from(2)),
560 false,
561 );
562 assert_eq!(timer.advance(UnixNanos::from(2)).count(), 2);
563 assert!(timer.is_expired);
564 }
565
566 #[rstest]
567 fn test_test_timer_advance_beyond_next_time_ns() {
568 let mut timer = TestTimer::new(
569 Ustr::from("TEST_TIMER"),
570 NonZeroU64::new(1).unwrap(),
571 UnixNanos::default(),
572 Some(UnixNanos::from(5)),
573 false,
574 );
575 assert_eq!(timer.advance(UnixNanos::from(5)).count(), 5);
576 assert!(timer.is_expired);
577 }
578
579 #[rstest]
580 fn test_test_timer_advance_beyond_stop_time() {
581 let mut timer = TestTimer::new(
582 Ustr::from("TEST_TIMER"),
583 NonZeroU64::new(1).unwrap(),
584 UnixNanos::default(),
585 Some(UnixNanos::from(5)),
586 false,
587 );
588 assert_eq!(timer.advance(UnixNanos::from(10)).count(), 5);
589 assert!(timer.is_expired);
590 }
591
592 #[rstest]
593 fn test_test_timer_advance_exact_boundary() {
594 let mut timer = TestTimer::new(
595 Ustr::from("TEST_TIMER"),
596 NonZeroU64::new(5).unwrap(),
597 UnixNanos::from(0),
598 None,
599 false,
600 );
601 assert_eq!(
602 timer.advance(UnixNanos::from(5)).count(),
603 1,
604 "Expected one event at the 5 ns boundary"
605 );
606 assert_eq!(
607 timer.advance(UnixNanos::from(10)).count(),
608 1,
609 "Expected one event at the 10 ns boundary"
610 );
611 }
612
613 #[rstest]
614 fn test_test_timer_fire_immediately_true() {
615 let mut timer = TestTimer::new(
616 Ustr::from("TEST_TIMER"),
617 NonZeroU64::new(5).unwrap(),
618 UnixNanos::from(10),
619 None,
620 true, );
622
623 assert_eq!(timer.next_time_ns(), UnixNanos::from(10));
625
626 let events: Vec<TimeEvent> = timer.advance(UnixNanos::from(10)).collect();
628 assert_eq!(events.len(), 1);
629 assert_eq!(events[0].ts_event, UnixNanos::from(10));
630
631 assert_eq!(timer.next_time_ns(), UnixNanos::from(15));
633 }
634
635 #[rstest]
636 fn test_test_timer_fire_immediately_false() {
637 let mut timer = TestTimer::new(
638 Ustr::from("TEST_TIMER"),
639 NonZeroU64::new(5).unwrap(),
640 UnixNanos::from(10),
641 None,
642 false, );
644
645 assert_eq!(timer.next_time_ns(), UnixNanos::from(15));
647
648 assert_eq!(timer.advance(UnixNanos::from(10)).count(), 0);
650
651 let events: Vec<TimeEvent> = timer.advance(UnixNanos::from(15)).collect();
653 assert_eq!(events.len(), 1);
654 assert_eq!(events[0].ts_event, UnixNanos::from(15));
655 }
656
657 #[rstest]
658 fn test_time_event_handler_ordering_uses_tie_breakers() {
659 let callback = TimeEventCallback::from(|_: TimeEvent| {});
660
661 let later_name = TimeEventHandler::new(
662 TimeEvent::new(
663 Ustr::from("TIME_BAR_ESM4-2-MINUTE-ASK-INTERNAL"),
664 UUID4::from("00000000-0000-4000-8000-000000000003"),
665 100.into(),
666 100.into(),
667 ),
668 callback.clone(),
669 );
670 let earlier_name = TimeEventHandler::new(
671 TimeEvent::new(
672 Ustr::from("SPREAD_QUOTE_ESM4"),
673 UUID4::from("00000000-0000-4000-8000-000000000002"),
674 100.into(),
675 100.into(),
676 ),
677 callback.clone(),
678 );
679 let later_init = TimeEventHandler::new(
680 TimeEvent::new(
681 Ustr::from("SPREAD_QUOTE_ESM4"),
682 UUID4::from("00000000-0000-4000-8000-000000000004"),
683 100.into(),
684 101.into(),
685 ),
686 callback.clone(),
687 );
688 let later_id = TimeEventHandler::new(
689 TimeEvent::new(
690 Ustr::from("SPREAD_QUOTE_ESM4"),
691 UUID4::from("00000000-0000-4000-8000-000000000005"),
692 100.into(),
693 100.into(),
694 ),
695 callback,
696 );
697
698 assert!(earlier_name < later_name);
699 assert!(earlier_name < later_init);
700 assert!(earlier_name < later_id);
701 assert_ne!(earlier_name, later_id);
702 }
703
704 #[rstest]
705 fn test_scheduled_time_event_ordering_laws() {
706 let base = ScheduledTimeEvent::new(TimeEvent::new(
707 Ustr::from("ALPHA"),
708 UUID4::from("00000000-0000-4000-8000-000000000001"),
709 100.into(),
710 10.into(),
711 ));
712 let variants = [
713 base.clone(),
714 ScheduledTimeEvent::new(TimeEvent::new(
715 Ustr::from("BETA"),
716 base.0.event_id,
717 base.0.ts_event,
718 base.0.ts_init,
719 )),
720 ScheduledTimeEvent::new(TimeEvent::new(
721 base.0.name,
722 UUID4::from("00000000-0000-4000-8000-000000000002"),
723 base.0.ts_event,
724 base.0.ts_init,
725 )),
726 ScheduledTimeEvent::new(TimeEvent::new(
727 base.0.name,
728 base.0.event_id,
729 101.into(),
730 base.0.ts_init,
731 )),
732 ScheduledTimeEvent::new(TimeEvent::new(
733 base.0.name,
734 base.0.event_id,
735 base.0.ts_event,
736 11.into(),
737 )),
738 ];
739
740 for a in &variants {
741 for b in &variants {
742 assert_eq!(a == b, a.cmp(b).is_eq());
743 assert_eq!(a.partial_cmp(b), Some(a.cmp(b)));
744 assert_eq!(a.cmp(b), b.cmp(a).reverse());
745 }
746 }
747 }
748
749 #[rstest]
750 fn test_scheduled_time_event_heap_ordering() {
751 let expected = [
752 TimeEvent::new(
753 Ustr::from("ALPHA"),
754 UUID4::from("00000000-0000-4000-8000-000000000001"),
755 100.into(),
756 10.into(),
757 ),
758 TimeEvent::new(
759 Ustr::from("ALPHA"),
760 UUID4::from("00000000-0000-4000-8000-000000000002"),
761 100.into(),
762 10.into(),
763 ),
764 TimeEvent::new(
765 Ustr::from("ALPHA"),
766 UUID4::from("00000000-0000-4000-8000-000000000003"),
767 100.into(),
768 11.into(),
769 ),
770 TimeEvent::new(
771 Ustr::from("BETA"),
772 UUID4::from("00000000-0000-4000-8000-000000000004"),
773 100.into(),
774 10.into(),
775 ),
776 TimeEvent::new(
777 Ustr::from("ALPHA"),
778 UUID4::from("00000000-0000-4000-8000-000000000005"),
779 101.into(),
780 10.into(),
781 ),
782 ];
783 let insertion_order = [4, 1, 3, 0, 2];
784 let mut heap = BinaryHeap::new();
785
786 for index in insertion_order {
787 heap.push(ScheduledTimeEvent::new(expected[index].clone()));
788 }
789
790 let popped = std::iter::from_fn(|| heap.pop().map(ScheduledTimeEvent::into_inner))
791 .collect::<Vec<_>>();
792 assert_eq!(popped, expected);
793 }
794
795 #[cfg(feature = "python")]
796 #[rstest]
797 fn test_python_callback_passes_time_event() {
798 Python::initialize();
799
800 Python::attach(|py| {
801 let seen = PyList::empty(py);
802 let seen_obj = seen.clone().unbind().into_any();
803
804 let callback = new_sync_py_callback(
805 py,
806 move |args: &Bound<'_, PyTuple>,
807 _kwargs: Option<&Bound<'_, PyDict>>|
808 -> PyResult<()> {
809 let arg = args.get_item(0)?;
810 let type_name = arg.get_type().name()?.to_string();
811 Python::attach(|py| seen_obj.call_method1(py, "append", (type_name,)))?;
812 Ok(())
813 },
814 )
815 .expect("callback should create")
816 .into_any()
817 .unbind();
818
819 let event = TimeEvent::new(
820 Ustr::from("PY_CALLBACK_MODE"),
821 UUID4::from("00000000-0000-4000-8000-000000000007"),
822 UnixNanos::from(100),
823 UnixNanos::from(99),
824 );
825
826 TimeEventCallback::from_python_time_event(callback).call(event);
827
828 assert_eq!(seen.len(), 1);
829 assert_eq!(
830 seen.get_item(0).unwrap().extract::<String>().unwrap(),
831 "TimeEvent"
832 );
833 });
834 }
835
836 #[cfg(feature = "python")]
837 fn new_sync_py_callback<F>(py: Python<'_>, closure: F) -> PyResult<Bound<'_, PyCFunction>>
838 where
839 F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> PyResult<()>
840 + Send
841 + Sync
842 + 'static,
843 {
844 PyCFunction::new_closure(py, None, None, closure)
845 }
846
847 #[derive(Default)]
848 struct RecordingTimeEventTap {
849 time_events: RefCell<Vec<(String, TimeEvent)>>,
850 }
851
852 impl RecordingTimeEventTap {
853 fn time_events(&self) -> Vec<(String, TimeEvent)> {
854 self.time_events.borrow().clone()
855 }
856 }
857
858 impl BusTap for RecordingTimeEventTap {
859 fn on_publish(&self, topic: MStr<Topic>, message: &dyn std::any::Any) {
860 if let Some(event) = message.downcast_ref::<TimeEvent>() {
861 self.time_events
862 .borrow_mut()
863 .push((topic.to_string(), event.clone()));
864 }
865 }
866
867 fn on_send(&self, _endpoint: MStr<Endpoint>, _message: &dyn std::any::Any) {}
868 }
869
870 #[rstest]
871 fn test_time_event_handler_run_dispatches_tap_before_callback() {
872 let event = TimeEvent::new(
873 Ustr::from("strategy.heartbeat"),
874 UUID4::from("00000000-0000-4000-8000-000000000006"),
875 UnixNanos::from(100),
876 UnixNanos::from(99),
877 );
878 let tap = Rc::new(RecordingTimeEventTap::default());
879 let callback_seen: Rc<RefCell<Vec<TimeEvent>>> = Rc::new(RefCell::new(Vec::new()));
880 let expected_topic = MessagingSwitchboard::time_event_topic().to_string();
881 let callback_expected = event.clone();
882 let callback_expected_topic = expected_topic.clone();
883 let callback_tap = Rc::clone(&tap);
884 let callback_seen_ref = Rc::clone(&callback_seen);
885
886 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |callback_event| {
887 assert_eq!(
888 callback_tap.time_events(),
889 vec![(callback_expected_topic.clone(), callback_expected.clone())],
890 );
891 callback_seen_ref.borrow_mut().push(callback_event);
892 });
893
894 set_bus_tap(tap.clone());
895 TimeEventHandler::new(event.clone(), TimeEventCallback::from(callback)).run();
896 clear_bus_tap();
897
898 assert_eq!(tap.time_events(), vec![(expected_topic, event.clone())]);
899 assert_eq!(*callback_seen.borrow(), vec![event]);
900 }
901
902 use proptest::{prelude::*, test_runner::TestCaseResult};
903
904 #[derive(Clone, Debug)]
905 enum TimerOperation {
906 AdvanceTime(u64),
907 Cancel,
908 }
909
910 fn timer_operation_strategy() -> impl Strategy<Value = TimerOperation> {
911 prop_oneof![
912 8 => (0u64..=1000).prop_map(TimerOperation::AdvanceTime),
913 2 => Just(TimerOperation::Cancel),
914 ]
915 }
916
917 fn timer_config_strategy() -> impl Strategy<Value = (u64, u64, Option<u64>, bool)> {
918 (
919 1u64..=1000,
920 timer_start_time_strategy(),
921 prop::option::of(0u64..=20_000),
922 prop::bool::ANY,
923 )
924 .prop_map(
925 |(interval_ns, start_time_ns, stop_after_ns, fire_immediately)| {
926 (
927 interval_ns,
928 start_time_ns,
929 stop_after_ns.map(|offset| start_time_ns + offset),
930 fire_immediately,
931 )
932 },
933 )
934 }
935
936 fn timer_start_time_strategy() -> impl Strategy<Value = u64> {
937 prop_oneof![
938 6 => 0u64..=u64::MAX - TIMER_TIME_HEADROOM,
939 2 => 0u64..=1_000_000,
940 1 => Just(1_700_000_000_000_000_000),
941 1 => Just(u64::MAX - TIMER_TIME_HEADROOM),
942 ]
943 }
944
945 fn timer_test_strategy()
946 -> impl Strategy<Value = (Vec<TimerOperation>, (u64, u64, Option<u64>, bool))> {
947 (
948 prop::collection::vec(timer_operation_strategy(), 5..=75),
949 timer_config_strategy(),
950 )
951 }
952
953 fn test_timer_with_operations(
954 operations: Vec<TimerOperation>,
955 (interval_ns, start_time_ns, stop_time_ns, fire_immediately): (u64, u64, Option<u64>, bool),
956 ) -> TestCaseResult {
957 let mut timer = TestTimer::new(
958 Ustr::from("PROP_TEST_TIMER"),
959 NonZeroU64::new(interval_ns).unwrap(),
960 UnixNanos::from(start_time_ns),
961 stop_time_ns.map(UnixNanos::from),
962 fire_immediately,
963 );
964
965 let mut current_time = start_time_ns;
966
967 let mut expected_next = if fire_immediately {
968 start_time_ns
969 } else {
970 start_time_ns + interval_ns
971 };
972
973 let mut expected_expired = false;
974
975 for operation in operations {
976 match operation {
977 TimerOperation::AdvanceTime(delta) => {
978 let to_time = current_time + delta;
979 let actual: Vec<(Ustr, u64, u64)> = timer
980 .advance(UnixNanos::from(to_time))
981 .map(|event| time_event_state(&event))
982 .collect();
983 let expected = expected_event_states(
984 expected_event_times(
985 to_time,
986 interval_ns,
987 stop_time_ns,
988 &mut expected_next,
989 &mut expected_expired,
990 ),
991 Ustr::from("PROP_TEST_TIMER"),
992 );
993 current_time = to_time;
994
995 prop_assert_eq!(actual, expected);
996 }
997 TimerOperation::Cancel => {
998 timer.cancel();
999 expected_expired = true;
1000 }
1001 }
1002
1003 prop_assert_eq!(timer.is_expired(), expected_expired);
1004 prop_assert_eq!(timer.next_time_ns().as_u64(), expected_next);
1005 }
1006
1007 if !expected_expired && let Some(stop_time_ns) = stop_time_ns {
1008 let to_time = stop_time_ns.saturating_add(interval_ns);
1009 let actual: Vec<(Ustr, u64, u64)> = timer
1010 .advance(UnixNanos::from(to_time))
1011 .map(|event| time_event_state(&event))
1012 .collect();
1013 let expected = expected_event_states(
1014 expected_event_times(
1015 to_time,
1016 interval_ns,
1017 Some(stop_time_ns),
1018 &mut expected_next,
1019 &mut expected_expired,
1020 ),
1021 Ustr::from("PROP_TEST_TIMER"),
1022 );
1023 prop_assert_eq!(actual, expected);
1024 prop_assert!(expected_expired);
1025 prop_assert!(timer.is_expired());
1026 prop_assert_eq!(timer.next_time_ns().as_u64(), expected_next);
1027 }
1028
1029 Ok(())
1030 }
1031
1032 fn expected_event_times(
1033 to_time: u64,
1034 interval_ns: u64,
1035 stop_time_ns: Option<u64>,
1036 next_time: &mut u64,
1037 is_expired: &mut bool,
1038 ) -> Vec<u64> {
1039 let mut events = Vec::new();
1040
1041 while !*is_expired && *next_time <= to_time {
1042 if let Some(stop_time_ns) = stop_time_ns
1043 && *next_time > stop_time_ns
1044 {
1045 *is_expired = true;
1046 break;
1047 }
1048
1049 let event_time = *next_time;
1050 events.push(event_time);
1051
1052 let Some(following_time) = event_time.checked_add(interval_ns) else {
1053 *is_expired = true;
1054 break;
1055 };
1056
1057 *next_time = following_time;
1058
1059 if Some(event_time) == stop_time_ns {
1060 *is_expired = true;
1061 break;
1062 }
1063 }
1064
1065 events
1066 }
1067
1068 proptest! {
1069 #[rstest]
1070 fn prop_timer_advance_operations((operations, config) in timer_test_strategy()) {
1071 test_timer_with_operations(operations, config)?;
1072 }
1073
1074 #[rstest]
1075 fn prop_timer_advance_batching_is_consistent(
1076 interval_ns in 1u64..=1000,
1077 start_time_ns in timer_start_time_strategy(),
1078 fire_immediately in prop::bool::ANY,
1079 advance_count in 1u64..=20,
1080 ) {
1081 let mut timer = TestTimer::new(
1082 Ustr::from("CONSISTENCY_TEST"),
1083 NonZeroU64::new(interval_ns).unwrap(),
1084 UnixNanos::from(start_time_ns),
1085 None, fire_immediately,
1087 );
1088
1089 let first_event_time = if fire_immediately { start_time_ns } else { start_time_ns + interval_ns };
1090 let final_event_time = first_event_time + interval_ns * (advance_count - 1);
1091 let expected = expected_event_states(
1092 (0..advance_count)
1093 .map(|index| first_event_time + interval_ns * index)
1094 .collect(),
1095 Ustr::from("CONSISTENCY_TEST"),
1096 );
1097
1098 let mut batched_timer = timer.clone();
1099 let batched: Vec<(Ustr, u64, u64)> = batched_timer
1100 .advance(UnixNanos::from(final_event_time))
1101 .map(|event| time_event_state(&event))
1102 .collect();
1103
1104 let mut stepped = Vec::new();
1105
1106 for event_time in
1107 (0..advance_count).map(|index| first_event_time + interval_ns * index)
1108 {
1109 stepped.extend(
1110 timer
1111 .advance(UnixNanos::from(event_time))
1112 .map(|event| time_event_state(&event)),
1113 );
1114 }
1115
1116 prop_assert_eq!(&batched, &expected);
1117 prop_assert_eq!(&stepped, &expected);
1118 prop_assert_eq!(timer.next_time_ns(), batched_timer.next_time_ns());
1119 prop_assert_eq!(timer.is_expired(), batched_timer.is_expired());
1120 }
1121
1122 #[rstest]
1123 fn prop_timer_terminal_time_does_not_require_following_time(
1124 (interval_ns, event_headroom) in terminal_time_strategy(),
1125 fire_immediately in prop::bool::ANY,
1126 bounded in prop::bool::ANY,
1127 ) {
1128 let event_time_ns = u64::MAX - event_headroom;
1129 let start_time_ns = if fire_immediately {
1130 event_time_ns
1131 } else {
1132 event_time_ns - interval_ns
1133 };
1134 let mut timer = TestTimer::new(
1135 Ustr::from("TERMINAL_STOP_TEST"),
1136 NonZeroU64::new(interval_ns).unwrap(),
1137 UnixNanos::from(start_time_ns),
1138 bounded.then_some(UnixNanos::max()),
1139 fire_immediately,
1140 );
1141
1142 let events: Vec<(Ustr, u64, u64)> = timer
1143 .advance(UnixNanos::max())
1144 .map(|event| time_event_state(&event))
1145 .collect();
1146
1147 prop_assert_eq!(
1148 events,
1149 vec![(Ustr::from("TERMINAL_STOP_TEST"), event_time_ns, event_time_ns)]
1150 );
1151 prop_assert!(timer.is_expired());
1152 prop_assert_eq!(timer.next_time_ns(), UnixNanos::from(event_time_ns));
1153 }
1154 }
1155
1156 const TIMER_TIME_HEADROOM: u64 = 100_000;
1157
1158 fn time_event_state(event: &TimeEvent) -> (Ustr, u64, u64) {
1159 (event.name, event.ts_event.as_u64(), event.ts_init.as_u64())
1160 }
1161
1162 fn expected_event_states(times: Vec<u64>, name: Ustr) -> Vec<(Ustr, u64, u64)> {
1163 times.into_iter().map(|time| (name, time, time)).collect()
1164 }
1165
1166 fn terminal_time_strategy() -> impl Strategy<Value = (u64, u64)> {
1167 (1u64..=1000).prop_flat_map(|interval_ns| (Just(interval_ns), 0u64..interval_ns))
1168 }
1169}