1use std::{fmt::Display, str::FromStr};
2
3use cosmwasm_schema::cw_serde;
4use cosmwasm_std::{
5 Addr, Binary, CosmosMsg, Empty, Env, Timestamp, TransactionInfo, Uint64, WasmQuery,
6};
7use cron_schedule::Schedule;
8use croncat_mod_generic::types::PathToValue;
9pub use croncat_sdk_core::types::AmountForOneTask;
10use cw20::Cw20Coin;
11use hex::ToHex;
12use sha2::{Digest, Sha256};
13
14#[cw_serde]
15pub struct Config {
16 pub owner_addr: Addr,
18
19 pub pause_admin: Addr,
23
24 pub croncat_factory_addr: Addr,
26
27 pub chain_name: String,
29
30 pub version: String,
32
33 pub croncat_manager_key: (String, [u8; 2]),
35
36 pub croncat_agents_key: (String, [u8; 2]),
38
39 pub slot_granularity_time: u64,
41
42 pub gas_base_fee: u64,
44
45 pub gas_action_fee: u64,
47
48 pub gas_query_fee: u64,
50
51 pub gas_limit: u64,
53}
54
55#[cw_serde]
57pub struct TaskRequest {
58 pub interval: Interval,
59 pub boundary: Option<Boundary>,
60 pub stop_on_fail: bool,
61 pub actions: Vec<Action>,
62 pub queries: Option<Vec<CosmosQuery>>,
63 pub transforms: Option<Vec<Transform>>,
64
65 pub cw20: Option<Cw20Coin>,
73}
74
75#[cw_serde]
81pub enum Interval {
82 Once,
84
85 Immediate,
87
88 Block(u64),
90
91 Cron(String),
93}
94
95impl Interval {
96 pub fn next(
97 &self,
98 env: &Env,
99 boundary: &Boundary,
100 slot_granularity_time: u64,
101 ) -> (u64, SlotType) {
102 match (self, boundary) {
103 (Interval::Once, Boundary::Height(boundary_height))
106 | (Interval::Immediate, Boundary::Height(boundary_height)) => (
107 get_next_block_limited(env, boundary_height),
108 SlotType::Block,
109 ),
110 (Interval::Once, Boundary::Time(boundary_time))
113 | (Interval::Immediate, Boundary::Time(boundary_time)) => {
114 (get_next_time_in_window(env, boundary_time), SlotType::Cron)
115 }
116 (Interval::Cron(crontab), Boundary::Time(boundary_time)) => (
119 get_next_cron_time(env, boundary_time, crontab, slot_granularity_time),
120 SlotType::Cron,
121 ),
122 (Interval::Block(block), Boundary::Height(boundary_height)) => (
128 get_next_block_by_offset(env.block.height, boundary_height, *block),
129 SlotType::Block,
130 ),
131 _ => unreachable!(),
133 }
134 }
135
136 pub fn is_valid(&self) -> bool {
137 match self {
138 Interval::Once | Interval::Immediate | Interval::Block(_) => true,
139 Interval::Cron(crontab) => {
140 let s = Schedule::from_str(crontab);
141 s.is_ok()
142 }
143 }
144 }
145}
146
147#[cw_serde]
149pub enum Boundary {
150 Height(BoundaryHeight),
151 Time(BoundaryTime),
152}
153
154impl Boundary {
155 pub fn is_block(&self) -> bool {
156 matches!(self, Boundary::Height(_))
157 }
158}
159
160#[cw_serde]
161pub struct BoundaryHeight {
162 pub start: Option<Uint64>,
163 pub end: Option<Uint64>,
164}
165
166#[cw_serde]
167pub struct BoundaryTime {
168 pub start: Option<Timestamp>,
169 pub end: Option<Timestamp>,
170}
171
172#[cw_serde]
173pub struct Action<T = Empty> {
174 pub msg: CosmosMsg<T>,
176
177 pub gas_limit: Option<u64>,
179}
180
181#[cw_serde]
183pub struct Transform {
184 pub action_idx: u64,
187
188 pub query_idx: u64,
191
192 pub action_path: PathToValue,
197 pub query_response_path: PathToValue,
202}
203
204#[cw_serde]
205pub struct Task {
206 pub owner_addr: Addr,
208
209 pub interval: Interval,
211 pub boundary: Boundary,
212
213 pub stop_on_fail: bool,
215
216 pub actions: Vec<Action>,
218 pub queries: Vec<CosmosQuery>,
222 pub transforms: Vec<Transform>,
223
224 pub version: String,
226
227 pub amount_for_one_task: AmountForOneTask,
229 }
231
232impl Task {
233 pub fn to_hash(&self, prefix: &str) -> String {
235 let message = format!(
236 "{:?}{:?}{:?}{:?}{:?}{:?}",
237 self.owner_addr,
238 self.interval,
239 self.boundary,
240 self.actions,
241 self.queries,
242 self.transforms
243 );
244
245 let hash = Sha256::digest(message.as_bytes());
246 let encoded: String = hash.encode_hex();
247
248 let (_, l) = encoded.split_at(prefix.len() + 1);
255 format!("{}:{}", prefix, l)
256 }
257
258 pub fn to_hash_vec(&self, prefix: &str) -> Vec<u8> {
260 self.to_hash(prefix).into_bytes()
261 }
262
263 pub fn recurring(&self) -> bool {
264 !matches!(self.interval, Interval::Once)
265 }
266
267 pub fn is_evented(&self) -> bool {
268 !self.queries.is_empty()
269 && (self.interval == Interval::Once || self.interval == Interval::Immediate)
270 }
271
272 pub fn into_response(self, prefix: &str) -> TaskResponse {
273 let task_hash = self.to_hash(prefix);
274
275 let queries = if !self.queries.is_empty() {
276 Some(self.queries)
277 } else {
278 None
279 };
280
281 TaskResponse {
282 task: Some(TaskInfo {
283 task_hash,
284 owner_addr: self.owner_addr,
285 interval: self.interval,
286 boundary: self.boundary,
287 stop_on_fail: self.stop_on_fail,
288 amount_for_one_task: self.amount_for_one_task,
289 actions: self.actions,
290 queries,
291 transforms: self.transforms,
292 version: self.version,
293 }),
294 }
295 }
296}
297
298#[cw_serde]
300pub struct CroncatQuery {
301 pub contract_addr: String,
304 pub msg: Binary,
305 pub check_result: bool,
308}
309
310#[cw_serde]
312pub enum CosmosQuery<T = WasmQuery> {
313 Croncat(CroncatQuery),
315
316 Wasm(T),
318}
319
320#[cw_serde]
321pub struct SlotTasksTotalResponse {
322 pub block_tasks: u64,
323 pub cron_tasks: u64,
324 pub evented_tasks: u64,
325}
326
327#[cw_serde]
328pub struct CurrentTaskInfoResponse {
329 pub total: Uint64,
330 pub last_created_task: Timestamp,
331}
332
333#[cw_serde]
334pub struct TaskInfo {
335 pub task_hash: String,
336
337 pub owner_addr: Addr,
338
339 pub interval: Interval,
340 pub boundary: Boundary,
341
342 pub stop_on_fail: bool,
343 pub amount_for_one_task: AmountForOneTask,
344
345 pub actions: Vec<Action>,
346 pub queries: Option<Vec<CosmosQuery>>,
347 pub transforms: Vec<Transform>,
348 pub version: String,
349}
350#[cw_serde]
351pub struct TaskResponse {
352 pub task: Option<TaskInfo>,
353}
354
355#[cw_serde]
361pub struct TaskExecutionInfo {
362 pub block_height: u64,
363 pub tx_info: Option<TransactionInfo>,
364 pub task_hash: String,
365 pub owner_addr: Addr,
366 pub amount_for_one_task: AmountForOneTask,
367 pub version: String,
368}
369
370impl Default for TaskExecutionInfo {
371 fn default() -> Self {
372 Self {
373 block_height: u64::default(),
374 tx_info: None,
375 task_hash: String::default(),
376 owner_addr: Addr::unchecked(""),
377 amount_for_one_task: AmountForOneTask::default(),
378 version: String::default(),
379 }
380 }
381}
382
383#[cw_serde]
384pub struct SlotHashesResponse {
385 pub block_id: u64,
386 pub block_task_hash: Vec<String>,
387 pub time_id: u64,
388 pub time_task_hash: Vec<String>,
389}
390
391#[cw_serde]
392pub struct SlotIdsResponse {
393 pub time_ids: Vec<u64>,
394 pub block_ids: Vec<u64>,
395}
396
397#[cw_serde]
398pub enum SlotType {
399 Block,
400 Cron,
401}
402
403impl Display for SlotType {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 match self {
406 SlotType::Block => write!(f, "block"),
407 SlotType::Cron => write!(f, "cron"),
408 }
409 }
410}
411
412fn get_next_block_limited(env: &Env, boundary_height: &BoundaryHeight) -> u64 {
414 let current_block_height = env.block.height;
415
416 let next_block_height = match boundary_height.start {
417 Some(id) if current_block_height < id.u64() => id.u64() - 1,
419 _ => current_block_height,
420 };
421
422 match boundary_height.end {
423 Some(end) if current_block_height > end.u64() => 0,
425
426 Some(end) if next_block_height > end.u64() => end.u64(),
428
429 _ => next_block_height + 1,
431 }
432}
433
434fn get_next_time_in_window(env: &Env, boundary: &BoundaryTime) -> u64 {
437 let current_block_time = env.block.time.nanos();
438
439 let next_block_time = match boundary.start {
440 Some(id) if current_block_time < id.nanos() => id.nanos(),
441 _ => current_block_time,
442 };
443
444 match boundary.end {
445 Some(end) if current_block_time > end.nanos() => 0,
447
448 Some(end) if next_block_time > end.nanos() => end.nanos(),
450
451 _ => next_block_time,
453 }
454}
455
456pub(crate) fn get_next_block_by_offset(
460 block_height: u64,
461 boundary_height: &BoundaryHeight,
462 block: u64,
463) -> u64 {
464 let current_block_height = block_height;
465 let modulo_block = if block > 0 {
466 current_block_height.saturating_sub(current_block_height % block) + block
467 } else {
468 return 0;
469 };
470
471 let next_block_height = match boundary_height.start {
472 Some(id) if current_block_height < id.u64() => {
473 let rem = id.u64() % block;
474 if rem > 0 {
475 id.u64().saturating_sub(rem) + block
476 } else {
477 id.u64()
478 }
479 }
480 _ => modulo_block,
481 };
482
483 match boundary_height.end {
484 Some(end) if current_block_height > end.u64() => 0,
486
487 Some(end) => {
489 let end_height = if let Some(rem) = end.u64().checked_rem(block) {
490 end.u64().saturating_sub(rem)
491 } else {
492 end.u64()
493 };
494 if next_block_height > end_height {
496 0
497 } else {
498 next_block_height
499 }
500 }
501 None => next_block_height,
502 }
503}
504
505fn get_next_cron_time(
508 env: &Env,
509 boundary: &BoundaryTime,
510 crontab: &str,
511 slot_granularity_time: u64,
512) -> u64 {
513 let current_block_ts = env.block.time.nanos();
514 let current_block_slot =
515 current_block_ts.saturating_sub(current_block_ts % slot_granularity_time);
516
517 let current_ts = match boundary.start {
519 Some(ts) if current_block_ts < ts.nanos() => ts.nanos(),
520 _ => current_block_ts,
521 };
522
523 let schedule = Schedule::from_str(crontab).unwrap();
525 let next_ts = schedule.next_after(¤t_ts).unwrap();
526 let next_ts_slot = next_ts.saturating_sub(next_ts % slot_granularity_time);
527
528 let next_slot = if next_ts_slot == current_block_slot {
530 next_ts_slot + slot_granularity_time
531 } else {
532 next_ts_slot
533 };
534
535 match boundary.end {
536 Some(end) if current_block_ts > end.nanos() => 0,
537 Some(end) => {
538 let end_slot = end
539 .nanos()
540 .saturating_sub(end.nanos() % slot_granularity_time);
541 u64::min(end_slot, next_slot)
542 }
543 _ => next_slot,
544 }
545}
546
547#[cfg(test)]
548mod test {
549 use cosmwasm_std::{testing::mock_env, Addr, CosmosMsg, Timestamp, Uint64, WasmMsg};
550 use croncat_sdk_core::types::{AmountForOneTask, GasPrice};
551 use hex::ToHex;
552 use sha2::{Digest, Sha256};
553
554 use crate::types::{Action, BoundaryHeight, CosmosQuery, CroncatQuery, Transform};
555
556 use super::{Boundary, BoundaryTime, Interval, SlotType, Task};
557
558 const TWO_MINUTES: u64 = 120_000_000_000;
559
560 #[test]
561 fn is_valid_test() {
562 let once = Interval::Once;
563 assert!(once.is_valid());
564
565 let immediate = Interval::Immediate;
566 assert!(immediate.is_valid());
567
568 let block = Interval::Block(100);
569 assert!(block.is_valid());
570
571 let cron_correct = Interval::Cron("1 * * * * *".to_string());
572 assert!(cron_correct.is_valid());
573
574 let cron_wrong = Interval::Cron("1 * * * * * *".to_string());
575 assert!(cron_wrong.is_valid());
576 }
577
578 #[test]
579 fn hashing() {
580 let task = Task {
581 owner_addr: Addr::unchecked("bob"),
582 interval: Interval::Block(5),
583 boundary: Boundary::Height(BoundaryHeight {
584 start: Some(Uint64::new(4)),
585 end: None,
586 }),
587 stop_on_fail: false,
588 amount_for_one_task: AmountForOneTask {
589 cw20: None,
590 coin: [None, None],
591 gas: 100,
592 agent_fee: u16::default(),
593 treasury_fee: u16::default(),
594 gas_price: GasPrice::default(),
595 },
596 actions: vec![Action {
597 msg: CosmosMsg::Wasm(WasmMsg::ClearAdmin {
598 contract_addr: "alice".to_string(),
599 }),
600 gas_limit: Some(5),
601 }],
602 queries: vec![CosmosQuery::Croncat(CroncatQuery {
603 msg: Default::default(),
604 contract_addr: "addr".to_owned(),
605 check_result: true,
606 })],
607 transforms: vec![Transform {
608 action_idx: 0,
609 query_idx: 0,
610 action_path: vec![].into(),
611 query_response_path: vec![].into(),
612 }],
613 version: String::from(""),
614 };
615
616 let message = format!(
617 "{:?}{:?}{:?}{:?}{:?}{:?}",
618 task.owner_addr,
619 task.interval,
620 task.boundary,
621 task.actions,
622 task.queries,
623 task.transforms
624 );
625
626 let hash = Sha256::digest(message.as_bytes());
627
628 let encode: String = hash.encode_hex();
629 let prefix = "atom";
630 let (_, l) = encode.split_at(prefix.len() + 1);
631 let encoded = format!("{}:{}", prefix, l);
632 let bytes = encoded.clone().into_bytes();
633
634 assert_eq!(encoded, task.to_hash(prefix));
636 assert_eq!(bytes, task.to_hash_vec(prefix));
637 }
638
639 #[test]
640 fn interval_get_next_block_limited() {
641 let cases: Vec<(Interval, Boundary, u64, SlotType)> = vec![
643 (
645 Interval::Once,
646 Boundary::Height(BoundaryHeight {
647 start: Some(Uint64::new(12345)),
648 end: None,
649 }),
650 12346,
651 SlotType::Block,
652 ),
653 (
654 Interval::Once,
655 Boundary::Height(BoundaryHeight {
656 start: Some(Uint64::new(12348)),
657 end: None,
658 }),
659 12348,
660 SlotType::Block,
661 ),
662 (
663 Interval::Once,
664 Boundary::Height(BoundaryHeight {
665 start: Some(Uint64::new(12345)),
666 end: Some(Uint64::new(12346)),
667 }),
668 12346,
669 SlotType::Block,
670 ),
671 (
672 Interval::Once,
673 Boundary::Height(BoundaryHeight {
674 start: Some(Uint64::new(12345)),
675 end: Some(Uint64::new(12340)),
676 }),
677 0,
678 SlotType::Block,
679 ),
680 (
682 Interval::Immediate,
683 Boundary::Height(BoundaryHeight {
684 start: Some(Uint64::new(12345)),
685 end: None,
686 }),
687 12346,
688 SlotType::Block,
689 ),
690 (
691 Interval::Immediate,
692 Boundary::Height(BoundaryHeight {
693 start: Some(Uint64::new(12348)),
694 end: None,
695 }),
696 12348,
697 SlotType::Block,
698 ),
699 (
700 Interval::Immediate,
701 Boundary::Height(BoundaryHeight {
702 start: Some(Uint64::new(12345)),
703 end: Some(Uint64::new(12346)),
704 }),
705 12346,
706 SlotType::Block,
707 ),
708 (
709 Interval::Immediate,
710 Boundary::Height(BoundaryHeight {
711 start: Some(Uint64::new(12345)),
712 end: Some(Uint64::new(12340)),
713 }),
714 0,
715 SlotType::Block,
716 ),
717 ];
718 for (interval, boundary, outcome_block, outcome_slot_kind) in cases.iter() {
720 let env = mock_env();
721 let (next_id, slot_kind) = interval.next(&env, boundary, 1);
722 assert_eq!(outcome_block, &next_id);
723 assert_eq!(outcome_slot_kind, &slot_kind);
724 }
725 }
726
727 #[test]
728 fn interval_get_next_block_by_offset() {
729 let cases: Vec<(Interval, Boundary, u64, SlotType)> = vec![
731 (
733 Interval::Block(1),
734 Boundary::Height(BoundaryHeight {
735 start: Some(Uint64::new(12345)),
736 end: None,
737 }),
738 12346,
739 SlotType::Block,
740 ),
741 (
742 Interval::Block(10),
743 Boundary::Height(BoundaryHeight {
744 start: Some(Uint64::new(12345)),
745 end: None,
746 }),
747 12350,
748 SlotType::Block,
749 ),
750 (
751 Interval::Block(100),
752 Boundary::Height(BoundaryHeight {
753 start: Some(Uint64::new(12345)),
754 end: None,
755 }),
756 12400,
757 SlotType::Block,
758 ),
759 (
760 Interval::Block(1000),
761 Boundary::Height(BoundaryHeight {
762 start: Some(Uint64::new(12345)),
763 end: None,
764 }),
765 13000,
766 SlotType::Block,
767 ),
768 (
769 Interval::Block(10000),
770 Boundary::Height(BoundaryHeight {
771 start: Some(Uint64::new(12345)),
772 end: None,
773 }),
774 20000,
775 SlotType::Block,
776 ),
777 (
778 Interval::Block(100000),
779 Boundary::Height(BoundaryHeight {
780 start: Some(Uint64::new(12345)),
781 end: None,
782 }),
783 100000,
784 SlotType::Block,
785 ),
786 (
788 Interval::Block(1),
789 Boundary::Height(BoundaryHeight {
790 start: Some(Uint64::new(12348)),
791 end: None,
792 }),
793 12348,
794 SlotType::Block,
795 ),
796 (
797 Interval::Block(10),
798 Boundary::Height(BoundaryHeight {
799 start: Some(Uint64::new(12360)),
800 end: None,
801 }),
802 12360,
803 SlotType::Block,
804 ),
805 (
806 Interval::Block(10),
807 Boundary::Height(BoundaryHeight {
808 start: Some(Uint64::new(12364)),
809 end: None,
810 }),
811 12370,
812 SlotType::Block,
813 ),
814 (
815 Interval::Block(100),
816 Boundary::Height(BoundaryHeight {
817 start: Some(Uint64::new(12364)),
818 end: None,
819 }),
820 12400,
821 SlotType::Block,
822 ),
823 (
825 Interval::Block(1),
826 Boundary::Height(BoundaryHeight {
827 start: Some(Uint64::new(12345)),
828 end: Some(Uint64::new(12345)),
829 }),
830 0,
831 SlotType::Block,
832 ),
833 (
834 Interval::Block(10),
835 Boundary::Height(BoundaryHeight {
836 start: Some(Uint64::new(12345)),
837 end: Some(Uint64::new(12355)),
838 }),
839 12350,
840 SlotType::Block,
841 ),
842 (
843 Interval::Block(100),
844 Boundary::Height(BoundaryHeight {
845 start: Some(Uint64::new(12345)),
846 end: Some(Uint64::new(12355)),
847 }),
848 0,
849 SlotType::Block,
850 ),
851 (
852 Interval::Block(100),
853 Boundary::Height(BoundaryHeight {
854 start: Some(Uint64::new(12345)),
855 end: Some(Uint64::new(12300)),
856 }),
857 0,
858 SlotType::Block,
859 ),
860 (
861 Interval::Block(100),
862 Boundary::Height(BoundaryHeight {
863 start: Some(Uint64::new(12345)),
864 end: Some(Uint64::new(12545)),
865 }),
866 12400,
867 SlotType::Block,
868 ),
869 (
870 Interval::Block(100),
871 Boundary::Height(BoundaryHeight {
872 start: Some(Uint64::new(11345)),
873 end: Some(Uint64::new(11545)),
874 }),
875 0,
876 SlotType::Block,
877 ),
878 (
879 Interval::Block(100_000),
880 Boundary::Height(BoundaryHeight {
881 start: Some(Uint64::new(12345)),
882 end: Some(Uint64::new(120355)),
883 }),
884 100_000,
885 SlotType::Block,
886 ),
887 (
889 Interval::Block(100_000),
890 Boundary::Height(BoundaryHeight {
891 start: Some(Uint64::new(12345)),
892 end: Some(Uint64::new(12355)),
893 }),
894 0,
895 SlotType::Block,
896 ),
897 (
898 Interval::Block(0),
899 Boundary::Height(BoundaryHeight {
900 start: Some(Uint64::new(12345)),
901 end: Some(Uint64::new(12355)),
902 }),
903 0,
904 SlotType::Block,
905 ),
906 ];
907
908 let env = mock_env();
910 for (interval, boundary, outcome_block, outcome_slot_kind) in cases.iter() {
911 let (next_id, slot_kind) = interval.next(&env, boundary, 1);
912 assert_eq!(outcome_block, &next_id);
913 assert_eq!(outcome_slot_kind, &slot_kind);
914 }
915 }
916
917 #[test]
918 fn interval_get_next_cron_time() {
919 let cases: Vec<(Interval, Boundary, u64, SlotType)> = vec![
922 (
923 Interval::Cron("* * * * * *".to_string()),
924 Boundary::Time(BoundaryTime {
925 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
926 end: None,
927 }),
928 1_571_797_420_000_000_000, SlotType::Cron,
930 ),
931 (
932 Interval::Cron("1 * * * * *".to_string()),
933 Boundary::Time(BoundaryTime {
934 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
935 end: None,
936 }),
937 1_571_797_441_000_000_000,
938 SlotType::Cron,
939 ),
940 (
941 Interval::Cron("* 0 * * * *".to_string()),
942 Boundary::Time(BoundaryTime {
943 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
944 end: None,
945 }),
946 1_571_799_600_000_000_000,
947 SlotType::Cron,
948 ),
949 (
950 Interval::Cron("15 0 * * * *".to_string()),
951 Boundary::Time(BoundaryTime {
952 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
953 end: None,
954 }),
955 1_571_799_615_000_000_000,
956 SlotType::Cron,
957 ),
958 (
960 Interval::Cron("15 0 * * * *".to_string()),
961 Boundary::Time(BoundaryTime {
962 start: Some(Timestamp::from_nanos(1_471_799_600_000_000_000)),
963 end: None,
964 }),
965 1_571_799_615_000_000_000,
966 SlotType::Cron,
967 ),
968 (
969 Interval::Cron("15 0 * * * *".to_string()),
970 Boundary::Time(BoundaryTime {
971 start: Some(Timestamp::from_nanos(1_571_799_600_000_000_000)),
972 end: None,
973 }),
974 1_571_799_615_000_000_000,
975 SlotType::Cron,
976 ),
977 (
978 Interval::Cron("15 0 * * * *".to_string()),
979 Boundary::Time(BoundaryTime {
980 start: Some(Timestamp::from_nanos(1_571_799_700_000_000_000)),
981 end: None,
982 }),
983 1_571_803_215_000_000_000,
984 SlotType::Cron,
985 ),
986 (
989 Interval::Cron("* * * * * *".to_string()),
990 Boundary::Time(BoundaryTime {
991 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
992 end: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
993 }),
994 1_571_797_419_879_305_533,
995 SlotType::Cron,
996 ),
997 (
999 Interval::Cron("* * * * * *".to_string()),
1000 Boundary::Time(BoundaryTime {
1001 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1002 end: Some(Timestamp::from_nanos(1_571_797_419_879_305_535)),
1003 }),
1004 1_571_797_419_879_305_535,
1005 SlotType::Cron,
1006 ),
1007 (
1009 Interval::Cron("* * * * * *".to_string()),
1010 Boundary::Time(BoundaryTime {
1011 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1012 end: Some(Timestamp::from_nanos(1_571_797_420_000_000_000)),
1013 }),
1014 1_571_797_420_000_000_000,
1015 SlotType::Cron,
1016 ),
1017 (
1019 Interval::Cron("* * * * * *".to_string()),
1020 Boundary::Time(BoundaryTime {
1021 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1022 end: Some(Timestamp::from_nanos(1_571_797_419_879_305_532)),
1023 }),
1024 0,
1025 SlotType::Cron,
1026 ),
1027 (
1028 Interval::Cron("15 0 * * * *".to_string()),
1029 Boundary::Time(BoundaryTime {
1030 start: Some(Timestamp::from_nanos(1_471_799_600_000_000_000)),
1031 end: Some(Timestamp::from_nanos(1_471_799_600_000_000_000)),
1032 }),
1033 0,
1034 SlotType::Cron,
1035 ),
1036 (
1037 Interval::Cron("1 * * * * *".to_string()),
1038 Boundary::Time(BoundaryTime {
1039 start: Some(Timestamp::from_nanos(1_471_797_441_000_000_000)),
1040 end: Some(Timestamp::from_nanos(1_671_797_441_000_000_000)),
1041 }),
1042 1_571_797_441_000_000_000,
1043 SlotType::Cron,
1044 ),
1045 ];
1046 for (interval, boundary, outcome_time, outcome_slot_kind) in cases.iter() {
1048 let env = mock_env();
1049 let (next_id, slot_kind) = interval.next(&env, boundary, 1);
1050 assert_eq!(outcome_time, &next_id);
1051 assert_eq!(outcome_slot_kind, &slot_kind);
1052 }
1053
1054 let cases: Vec<(Interval, Boundary, u64, SlotType)> = vec![
1056 (
1057 Interval::Cron("* * * * * *".to_string()),
1058 Boundary::Time(BoundaryTime {
1059 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1060 end: None,
1061 }),
1062 1_571_797_420_000_000_000_u64
1064 .saturating_sub(1_571_797_420_000_000_000 % TWO_MINUTES)
1065 + TWO_MINUTES, SlotType::Cron,
1067 ),
1068 (
1069 Interval::Cron("1 * * * * *".to_string()),
1070 Boundary::Time(BoundaryTime {
1071 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1072 end: None,
1073 }),
1074 1_571_797_440_000_000_000,
1075 SlotType::Cron,
1076 ),
1077 (
1078 Interval::Cron("* 0 * * * *".to_string()),
1079 Boundary::Time(BoundaryTime {
1080 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1081 end: None,
1082 }),
1083 1_571_799_600_000_000_000,
1084 SlotType::Cron,
1085 ),
1086 (
1087 Interval::Cron("15 0 * * * *".to_string()),
1088 Boundary::Time(BoundaryTime {
1089 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1090 end: None,
1091 }),
1092 1_571_799_600_000_000_000,
1093 SlotType::Cron,
1094 ),
1095 (
1097 Interval::Cron("15 0 * * * *".to_string()),
1098 Boundary::Time(BoundaryTime {
1099 start: Some(Timestamp::from_nanos(1_471_799_600_000_000_000)),
1100 end: None,
1101 }),
1102 1_571_799_600_000_000_000,
1103 SlotType::Cron,
1104 ),
1105 (
1106 Interval::Cron("15 0 * * * *".to_string()),
1107 Boundary::Time(BoundaryTime {
1108 start: Some(Timestamp::from_nanos(1_571_799_600_000_000_000)),
1109 end: None,
1110 }),
1111 1_571_799_600_000_000_000,
1112 SlotType::Cron,
1113 ),
1114 (
1115 Interval::Cron("15 0 * * * *".to_string()),
1116 Boundary::Time(BoundaryTime {
1117 start: Some(Timestamp::from_nanos(1_571_799_700_000_000_000)),
1118 end: None,
1119 }),
1120 1_571_803_200_000_000_000,
1121 SlotType::Cron,
1122 ),
1123 (
1126 Interval::Cron("* * * * * *".to_string()),
1127 Boundary::Time(BoundaryTime {
1128 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1129 end: Some(Timestamp::from_nanos(1_571_797_419_879_305_535)),
1130 }),
1131 1_571_797_320_000_000_000,
1132 SlotType::Cron,
1133 ),
1134 (
1136 Interval::Cron("1 * * * * *".to_string()),
1137 Boundary::Time(BoundaryTime {
1138 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1139 end: Some(Timestamp::from_nanos(1_571_797_560_000_000_000)),
1140 }),
1141 1_571_797_440_000_000_000,
1142 SlotType::Cron,
1143 ),
1144 (
1146 Interval::Cron("1 * * * * *".to_string()),
1147 Boundary::Time(BoundaryTime {
1148 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1149 end: Some(Timestamp::from_nanos(1_571_797_420_000_000_000)),
1150 }),
1151 1_571_797_320_000_000_000,
1152 SlotType::Cron,
1153 ),
1154 (
1156 Interval::Cron("* * * * * *".to_string()),
1157 Boundary::Time(BoundaryTime {
1158 start: Some(Timestamp::from_nanos(1_571_797_419_879_305_533)),
1159 end: Some(Timestamp::from_nanos(1_571_797_419_879_305_532)),
1160 }),
1161 0,
1162 SlotType::Cron,
1163 ),
1164 ];
1165 for (interval, boundary, outcome_time, outcome_slot_kind) in cases.iter() {
1167 let env = mock_env();
1168 let (next_id, slot_kind) = interval.next(&env, boundary, TWO_MINUTES);
1169 assert_eq!(outcome_time, &next_id);
1170 assert_eq!(outcome_slot_kind, &slot_kind);
1171 }
1172 }
1173}