1pub(crate) mod hash;
2
3pub mod parser;
4pub mod reader;
5pub mod tokens;
6
7use std::borrow::Cow;
8use std::collections::HashMap;
9use std::env;
10use std::fmt::Write;
11use std::path::PathBuf;
12use std::process::{Command, Stdio};
13
14use self::parser::Parser;
15use self::reader::{ReadError, Reader};
16use self::tokens::{CronJob, Token};
17
18const DEFAULT_SHELL: &str = "/bin/sh";
20
21#[derive(Debug)]
22struct ShellCommand {
23 env: HashMap<String, String>,
24 shell: String,
25 home: PathBuf,
26 command: String,
27}
28
29#[derive(Debug, Eq, PartialEq)]
34pub enum RunResultDetail {
35 DidRun {
37 exit_code: Option<i32>,
39 },
40 DidNotRun {
43 reason: String,
45 },
46 IsRunning { pid: u32 },
49}
50
51#[derive(Debug, Eq, PartialEq)]
53pub struct RunResult {
54 pub was_successful: bool,
68 pub detail: RunResultDetail,
71}
72
73#[derive(Debug)]
78pub struct Crontab {
79 pub tokens: Vec<Token>,
80 env: Option<HashMap<String, String>>,
81}
82
83impl Crontab {
84 #[must_use]
85 pub fn new(tokens: Vec<Token>) -> Self {
86 Self { tokens, env: None }
87 }
88
89 #[must_use]
94 pub fn has_runnable_jobs(&self) -> bool {
95 self.tokens
96 .iter()
97 .any(|token| matches!(token, Token::CronJob(_)))
98 }
99
100 #[must_use]
102 pub fn jobs(&self) -> Vec<&CronJob> {
103 self.tokens
104 .iter()
105 .filter_map(|token| {
106 if let Token::CronJob(job) = token {
107 Some(job)
108 } else {
109 None
110 }
111 })
112 .collect()
113 }
114
115 #[must_use]
117 pub fn has_job(&self, job: &CronJob) -> bool {
118 self.jobs().contains(&job)
119 }
120
121 #[must_use]
123 pub fn get_job_from_uid(&self, uid: usize) -> Option<&CronJob> {
124 self.jobs().into_iter().find(|job| job.uid == uid)
125 }
126
127 #[must_use]
129 pub fn get_job_from_fingerprint(&self, fingerprint: u64) -> Option<&CronJob> {
130 self.jobs()
131 .into_iter()
132 .find(|job| job.fingerprint == fingerprint)
133 }
134
135 #[must_use]
137 pub fn get_job_from_tag(&self, tag: &str) -> Option<&CronJob> {
138 self.jobs()
139 .into_iter()
140 .find(|job| job.tag.as_ref().is_some_and(|job_tag| job_tag == tag))
141 }
142
143 pub fn set_env(&mut self, env: HashMap<String, String>) {
178 self.env = Some(env);
179 }
180
181 #[must_use]
231 pub fn run(&self, job: &CronJob) -> RunResult {
232 let mut command = match self.prepare_command(job) {
233 Ok(command) => command,
234 Err(res) => return res,
235 };
236
237 let status = command.status();
238
239 match status {
240 Ok(status) => RunResult {
241 was_successful: status.success(),
242 detail: RunResultDetail::DidRun {
243 exit_code: status.code(),
244 },
245 },
246 Err(_) => RunResult {
247 was_successful: false,
248 detail: RunResultDetail::DidNotRun {
249 reason: String::from("Failed to run command (does shell exist?)."),
250 },
251 },
252 }
253 }
254
255 #[must_use]
296 pub fn run_detached(&self, job: &CronJob) -> RunResult {
297 let mut command = match self.prepare_command(job) {
298 Ok(command) => command,
299 Err(res) => return res,
300 };
301
302 #[cfg(not(tarpaulin_include))] let child = command
304 .stdin(Stdio::null())
305 .stdout(Stdio::null())
306 .stderr(Stdio::null())
307 .spawn();
308
309 match child {
310 Ok(child) => RunResult {
311 was_successful: false,
315 detail: RunResultDetail::IsRunning { pid: child.id() },
316 },
317 Err(_) => RunResult {
318 was_successful: false,
319 detail: RunResultDetail::DidNotRun {
320 reason: String::from("Failed to run command (does shell exist?)."),
321 },
322 },
323 }
324 }
325
326 fn prepare_command(&self, job: &CronJob) -> Result<Command, RunResult> {
327 let shell_command = match self.make_shell_command(job) {
328 Ok(shell_command) => shell_command,
329 Err(reason) => {
330 return Err(RunResult {
331 was_successful: false,
332 detail: RunResultDetail::DidNotRun { reason },
333 });
334 }
335 };
336
337 #[cfg(not(tarpaulin_include))] {
339 let mut command = Command::new(shell_command.shell);
340
341 if let Some(env) = self.env.as_ref() {
342 command.env_clear().envs(env);
343 }
344
345 command
346 .envs(&shell_command.env)
347 .current_dir(shell_command.home)
348 .arg("-c")
349 .arg(shell_command.command);
350
351 Ok(command)
352 }
353 }
354
355 fn make_shell_command(&self, job: &CronJob) -> Result<ShellCommand, String> {
356 self.ensure_job_exists(job)?;
357
358 let mut env = self.extract_variables(job);
359 let shell = Self::determine_shell_to_use(&mut env);
360 let home = Self::determine_home_to_use(&mut env)?;
361 let command = job.command.clone();
362
363 Ok(ShellCommand {
364 env,
365 shell,
366 home,
367 command,
368 })
369 }
370
371 fn ensure_job_exists(&self, job: &CronJob) -> Result<(), String> {
372 if !self.has_job(job) {
373 return Err(String::from("The given job is not in the crontab."));
374 }
375 Ok(())
376 }
377
378 fn extract_variables(&self, target_job: &CronJob) -> HashMap<String, String> {
379 let mut variables: HashMap<String, String> = HashMap::new();
380 for token in &self.tokens {
381 if let Token::Variable(variable) = token {
382 variables.insert(variable.identifier.clone(), variable.value.clone());
383 } else if let Token::CronJob(job) = token {
384 if job == target_job {
385 break; }
387 }
388 }
389 variables
390 }
391
392 fn determine_shell_to_use(env: &mut HashMap<String, String>) -> String {
393 if let Some(shell) = env.remove("SHELL") {
394 shell
396 } else {
397 String::from(DEFAULT_SHELL)
398 }
399 }
400
401 fn determine_home_to_use(env: &mut HashMap<String, String>) -> Result<PathBuf, String> {
402 if let Some(home) = env.remove("HOME") {
403 Ok(PathBuf::from(home))
405 } else {
406 Ok(Self::get_home_directory()?)
407 }
408 }
409
410 fn get_home_directory() -> Result<PathBuf, String> {
411 if let Some(home_directory) = env::home_dir() {
412 Ok(home_directory)
413 } else {
414 Err(String::from("Could not determine Home directory."))
415 }
416 }
417}
418
419impl Crontab {
420 #[must_use]
421 pub fn to_json(&self) -> String {
422 let jobs = self.jobs();
423
424 let mut json = String::with_capacity(jobs.len() * 250);
425 let mut jobs = jobs.iter().peekable();
426
427 _ = write!(json, "[");
428 while let Some(job) = jobs.next() {
429 _ = write!(json, "{{");
430 _ = write!(json, r#""uid":{},"#, job.uid);
431 _ = write!(json, r#""fingerprint":"{:x}","#, job.fingerprint);
432 _ = write!(
433 json,
434 r#""tag":{},"#,
435 job.tag.as_ref().map_or_else(
436 || Cow::Borrowed("null"),
437 |tag| { Cow::Owned(format!(r#""{}""#, tag.replace('"', r#"\""#))) }
438 )
439 );
440 _ = write!(json, r#""schedule":"{}","#, job.schedule);
441 _ = write!(
442 json,
443 r#""command":"{}","#,
444 job.command.replace('"', r#"\""#)
445 );
446 _ = write!(
447 json,
448 r#""description":{},"#,
449 job.description.as_ref().map_or_else(
450 || Cow::Borrowed("null"),
451 |description| {
452 Cow::Owned(format!(r#""{}""#, description.0.replace('"', r#"\""#)))
453 }
454 )
455 );
456 _ = write!(
457 json,
458 r#""section":{}"#,
459 job.section.as_ref().map_or_else(
460 || Cow::Borrowed("null"),
461 |section| Cow::Owned(format!(
462 r#"{{"uid":{},"title":"{}"}}"#,
463 section.uid,
464 section.title.replace('"', r#"\""#)
465 ))
466 )
467 );
468 _ = write!(json, "}}");
469
470 if jobs.peek().is_some() {
471 _ = write!(json, ",");
472 }
473 }
474 _ = write!(json, "]");
475
476 json
477 }
478}
479
480pub fn make_instance() -> Result<Crontab, ReadError> {
500 let crontab: String = Reader::read()?;
501 let tokens: Vec<Token> = Parser::parse(&crontab);
502
503 Ok(Crontab::new(tokens))
504}
505
506#[cfg(test)]
507mod tests {
508 use self::tokens::{Comment, CommentKind, JobDescription, Variable};
509 use super::*;
510
511 fn tokens() -> Vec<Token> {
516 vec![
517 Token::Comment(Comment {
518 value: String::from("# CronRunner Demo"),
519 kind: CommentKind::Regular,
520 }),
521 Token::Comment(Comment {
522 value: String::from("# ---------------"),
523 kind: CommentKind::Regular,
524 }),
525 Token::CronJob(CronJob {
526 uid: 1,
527 fingerprint: 13_376_942,
528 tag: None,
529 schedule: String::from("@reboot"),
530 command: String::from("/usr/bin/bash ~/startup.sh"),
531 description: None,
532 section: None,
533 }),
534 Token::Comment(Comment {
535 value: String::from(
536 "# Double-hash comments (##) immediately preceding a job are used as",
537 ),
538 kind: CommentKind::Regular,
539 }),
540 Token::Comment(Comment {
541 value: String::from("# description. See below:"),
542 kind: CommentKind::Regular,
543 }),
544 Token::Comment(Comment {
545 value: String::from("## Update brew."),
546 kind: CommentKind::Description,
547 }),
548 Token::CronJob(CronJob {
549 uid: 2,
550 fingerprint: 13_376_942,
551 tag: None,
552 schedule: String::from("30 20 * * *"),
553 command: String::from("/usr/local/bin/brew update && /usr/local/bin/brew upgrade"),
554 description: Some(JobDescription(String::from("Update brew."))),
555 section: None,
556 }),
557 Token::Variable(Variable {
558 identifier: String::from("FOO"),
559 value: String::from("bar"),
560 }),
561 Token::Comment(Comment {
562 value: String::from("## Print variable."),
563 kind: CommentKind::Description,
564 }),
565 Token::CronJob(CronJob {
566 uid: 3,
567 fingerprint: 13_376_942,
568 tag: None,
569 schedule: String::from("* * * * *"),
570 command: String::from("echo $FOO"),
571 description: Some(JobDescription(String::from("Print variable."))),
572 section: None,
573 }),
574 Token::Comment(Comment {
575 value: String::from("# Do nothing (this is a regular comment)."),
576 kind: CommentKind::Regular,
577 }),
578 Token::CronJob(CronJob {
579 uid: 4,
580 fingerprint: 13_376_942,
581 tag: None,
582 schedule: String::from("@reboot"),
583 command: String::from(":"),
584 description: None,
585 section: None,
586 }),
587 Token::Variable(Variable {
588 identifier: String::from("SHELL"),
589 value: String::from("/bin/bash"),
590 }),
591 Token::CronJob(CronJob {
592 uid: 5,
593 fingerprint: 13_376_942,
594 tag: None,
595 schedule: String::from("@hourly"),
596 command: String::from("echo 'I am echoed by bash!'"),
597 description: None,
598 section: None,
599 }),
600 Token::Variable(Variable {
601 identifier: String::from("HOME"),
602 value: String::from("/home/<custom>"),
603 }),
604 Token::CronJob(CronJob {
605 uid: 6,
606 fingerprint: 13_376_942,
607 tag: None,
608 schedule: String::from("@yerly"),
609 command: String::from("./cleanup.sh"),
610 description: None,
611 section: None,
612 }),
613 ]
614 }
615
616 #[test]
617 fn has_runnable_jobs() {
618 let crontab = Crontab::new(vec![Token::CronJob(CronJob {
619 uid: 1,
620 fingerprint: 13_376_942,
621 tag: None,
622 schedule: String::from("@hourly"),
623 command: String::from("echo 'hello, world'"),
624 description: None,
625 section: None,
626 })]);
627
628 assert!(crontab.has_runnable_jobs());
629 }
630
631 #[test]
632 fn has_no_runnable_jobs() {
633 let crontab = Crontab::new(vec![
634 Token::Comment(Comment {
635 value: String::from("# This is a comment"),
636 kind: CommentKind::Regular,
637 }),
638 Token::Variable(Variable {
639 identifier: String::from("SHELL"),
640 value: String::from("/bin/bash"),
641 }),
642 ]);
643
644 assert!(!crontab.has_runnable_jobs());
645 }
646
647 #[test]
648 fn has_no_runnable_jobs_because_crontab_is_empty() {
649 let crontab = Crontab::new(vec![]);
650
651 assert!(!crontab.has_runnable_jobs());
652 }
653
654 #[test]
655 fn list_of_jobs() {
656 let crontab = Crontab::new(tokens());
657
658 let tokens = tokens();
659 let jobs: Vec<&CronJob> = tokens
660 .iter()
661 .filter_map(|token| {
662 if let Token::CronJob(job) = token {
663 Some(job)
664 } else {
665 None
666 }
667 })
668 .collect();
669
670 assert_eq!(crontab.jobs(), jobs);
671 }
672
673 #[test]
674 fn has_job() {
675 let crontab = Crontab::new(vec![Token::CronJob(CronJob {
676 uid: 1,
677 fingerprint: 13_376_942,
678 tag: None,
679 schedule: String::from("@daily"),
680 command: String::from("docker image prune --force"),
681 description: None,
682 section: None,
683 })]);
684
685 assert!(crontab.has_job(&CronJob {
687 uid: 1,
688 fingerprint: 13_376_942,
689 tag: None,
690 schedule: String::from("@daily"),
691 command: String::from("docker image prune --force"),
692 description: None,
693 section: None,
694 }),);
695 assert!(!crontab.has_job(&CronJob {
697 uid: 0,
698 fingerprint: 13_376_942,
699 tag: None,
700 schedule: String::from("@daily"),
701 command: String::from("docker image prune --force"),
702 description: None,
703 section: None,
704 }),);
705 assert!(!crontab.has_job(&CronJob {
707 uid: 1,
708 fingerprint: 13_376_942,
709 tag: None,
710 schedule: String::from("<invalid>"),
711 command: String::from("<invalid>"),
712 description: None,
713 section: None,
714 }),);
715 }
716
717 #[test]
718 fn get_job_from_uid() {
719 let crontab = Crontab::new(vec![Token::CronJob(CronJob {
720 uid: 1,
721 fingerprint: 13_376_942,
722 tag: None,
723 schedule: String::from("@reboot"),
724 command: String::from("echo 'hello, world'"),
725 description: None,
726 section: None,
727 })]);
728
729 let job = crontab.get_job_from_uid(1).unwrap();
730
731 assert_eq!(
732 *job,
733 CronJob {
734 uid: 1,
735 fingerprint: 13_376_942,
736 tag: None,
737 schedule: String::from("@reboot"),
738 command: String::from("echo 'hello, world'"),
739 description: None,
740 section: None,
741 }
742 );
743 }
744
745 #[test]
746 fn get_job_from_uid_not_in_crontab() {
747 let crontab = Crontab::new(vec![Token::CronJob(CronJob {
748 uid: 1,
749 fingerprint: 13_376_942,
750 tag: None,
751 schedule: String::from("@daily"),
752 command: String::from("echo 'hello, world'"),
753 description: None,
754 section: None,
755 })]);
756
757 let job = crontab.get_job_from_uid(42);
758
759 assert!(job.is_none());
760 }
761
762 #[test]
763 fn get_job_from_fingerprint() {
764 let crontab = Crontab::new(vec![Token::CronJob(CronJob {
765 uid: 1,
766 fingerprint: 13_376_942,
767 tag: None,
768 schedule: String::from("@reboot"),
769 command: String::from("echo 'hello, world'"),
770 description: None,
771 section: None,
772 })]);
773
774 let job = crontab.get_job_from_fingerprint(13_376_942).unwrap();
775
776 assert_eq!(
777 *job,
778 CronJob {
779 uid: 1,
780 fingerprint: 13_376_942,
781 tag: None,
782 schedule: String::from("@reboot"),
783 command: String::from("echo 'hello, world'"),
784 description: None,
785 section: None,
786 }
787 );
788 }
789
790 #[test]
791 fn get_job_from_fingerprint_not_in_crontab() {
792 let crontab = Crontab::new(vec![Token::CronJob(CronJob {
793 uid: 1,
794 fingerprint: 13_376_942,
795 tag: None,
796 schedule: String::from("@daily"),
797 command: String::from("echo 'hello, world'"),
798 description: None,
799 section: None,
800 })]);
801
802 let job = crontab.get_job_from_fingerprint(42);
803
804 assert!(job.is_none());
805 }
806
807 #[test]
808 fn get_job_from_tag() {
809 let crontab = Crontab::new(vec![Token::CronJob(CronJob {
810 uid: 1,
811 fingerprint: 13_376_942,
812 tag: Some(String::from("my-tag")),
813 schedule: String::from("@reboot"),
814 command: String::from("echo 'hello, world'"),
815 description: None,
816 section: None,
817 })]);
818
819 let job = crontab.get_job_from_tag("my-tag").unwrap();
820
821 assert_eq!(
822 *job,
823 CronJob {
824 uid: 1,
825 fingerprint: 13_376_942,
826 tag: Some(String::from("my-tag")),
827 schedule: String::from("@reboot"),
828 command: String::from("echo 'hello, world'"),
829 description: None,
830 section: None,
831 }
832 );
833 }
834
835 #[test]
836 fn get_job_from_tag_not_in_crontab() {
837 let crontab = Crontab::new(vec![
838 Token::CronJob(CronJob {
839 uid: 1,
840 fingerprint: 13_376_942,
841 tag: None,
842 schedule: String::from("@daily"),
843 command: String::from("echo 'hello, world'"),
844 description: None,
845 section: None,
846 }),
847 Token::CronJob(CronJob {
848 uid: 2,
849 fingerprint: 369_108,
850 tag: Some(String::from("MY-TAG")),
851 schedule: String::from("@daily"),
852 command: String::from("echo 'hello, world'"),
853 description: None,
854 section: None,
855 }),
856 ]);
857
858 let job = crontab.get_job_from_tag("my-tag");
859
860 assert!(job.is_none());
861 }
862
863 #[test]
864 fn two_equal_jobs_are_treated_as_different_jobs() {
865 let crontab = Crontab::new(vec![
866 Token::CronJob(CronJob {
867 uid: 1,
868 fingerprint: 13_376_942,
869 tag: None,
870 schedule: String::from("@daily"),
871 command: String::from("df -h > ~/track_disk_usage.txt"),
872 description: Some(JobDescription(String::from("Track disk usage."))),
873 section: None,
874 }),
875 Token::Variable(Variable {
876 identifier: String::from("FOO"),
877 value: String::from("bar"),
878 }),
879 Token::CronJob(CronJob {
880 uid: 2,
881 fingerprint: 108_216_215,
882 tag: None,
883 schedule: String::from("@daily"),
884 command: String::from("df -h > ~/track_disk_usage.txt"),
885 description: Some(JobDescription(String::from("Track disk usage."))),
886 section: None,
887 }),
888 ]);
889
890 let job = crontab.get_job_from_uid(2).unwrap();
891 let command = crontab.make_shell_command(job).unwrap();
892
893 assert_eq!(
896 command.env,
897 HashMap::from([(String::from("FOO"), String::from("bar"))])
898 );
899 assert_eq!(command.command, "df -h > ~/track_disk_usage.txt");
900 }
901
902 #[test]
903 fn set_env() {
904 let mut crontab = Crontab::new(Vec::new());
905
906 assert!(crontab.env.is_none());
907
908 crontab.set_env(HashMap::from([(String::from("FOO"), String::from("bar"))]));
909
910 assert!(
911 crontab.env.is_some_and(
912 |env| env == HashMap::from([(String::from("FOO"), String::from("bar"))])
913 )
914 );
915 }
916
917 #[test]
918 fn set_env_replaces_previous_one() {
919 let mut crontab = Crontab::new(Vec::new());
920
921 let env1 = HashMap::from([(String::from("FOO"), String::from("bar"))]);
922 let env2 = HashMap::from([(String::from("BAZ"), String::from("42"))]);
923
924 crontab.set_env(env1);
925 crontab.set_env(env2.clone());
926
927 assert!(crontab.env.is_some_and(|env| env == env2));
928 }
929
930 #[test]
931 fn working_directory_is_home_directory() {
932 unsafe {
933 env::set_var("HOME", "/home/<test>");
934 }
935
936 let home_directory = Crontab::get_home_directory().unwrap();
937
938 assert_eq!(home_directory.to_string_lossy(), "/home/<test>");
939 }
940
941 #[test]
942 fn run_cron_without_variable() {
943 let crontab = Crontab::new(vec![Token::CronJob(CronJob {
944 uid: 1,
945 fingerprint: 13_376_942,
946 tag: None,
947 schedule: String::from("@reboot"),
948 command: String::from("/usr/bin/bash ~/startup.sh"),
949 description: Some(JobDescription(String::from("Description."))),
950 section: None,
951 })]);
952
953 let job = crontab.get_job_from_uid(1).unwrap();
954 let command = crontab.make_shell_command(job).unwrap();
955
956 assert_eq!(command.command, "/usr/bin/bash ~/startup.sh");
957 }
958
959 #[test]
960 fn run_cron_with_variable() {
961 let crontab = Crontab::new(vec![
962 Token::Variable(Variable {
963 identifier: String::from("FOO"),
964 value: String::from("bar"),
965 }),
966 Token::CronJob(CronJob {
967 uid: 1,
968 fingerprint: 13_376_942,
969 tag: None,
970 schedule: String::from("* * * * *"),
971 command: String::from("echo $FOO"),
972 description: Some(JobDescription(String::from("Print variable."))),
973 section: None,
974 }),
975 ]);
976
977 let job = crontab.get_job_from_uid(1).unwrap();
978 let command = crontab.make_shell_command(job).unwrap();
979
980 assert_eq!(
981 command.env,
982 HashMap::from([(String::from("FOO"), String::from("bar"))])
983 );
984 assert_eq!(command.command, "echo $FOO");
985 }
986
987 #[test]
988 fn run_cron_after_variable_but_not_right_after_it() {
989 let crontab = Crontab::new(vec![
990 Token::Variable(Variable {
991 identifier: String::from("FOO"),
992 value: String::from("bar"),
993 }),
994 Token::Comment(Comment {
995 value: String::from("## Print variable."),
996 kind: CommentKind::Description,
997 }),
998 Token::CronJob(CronJob {
999 uid: 1,
1000 fingerprint: 13_376_942,
1001 tag: None,
1002 schedule: String::from("* * * * *"),
1003 command: String::from("echo $FOO"),
1004 description: Some(JobDescription(String::from("Print variable."))),
1005 section: None,
1006 }),
1007 Token::Comment(Comment {
1008 value: String::from("# Do nothing (this is a regular comment)."),
1009 kind: CommentKind::Regular,
1010 }),
1011 Token::CronJob(CronJob {
1012 uid: 2,
1013 fingerprint: 13_376_942,
1014 tag: None,
1015 schedule: String::from("@reboot"),
1016 command: String::from(":"),
1017 description: None,
1018 section: None,
1019 }),
1020 ]);
1021
1022 let job = crontab.get_job_from_uid(2).unwrap();
1023 let command = crontab.make_shell_command(job).unwrap();
1024
1025 assert_eq!(
1026 command.env,
1027 HashMap::from([(String::from("FOO"), String::from("bar"))])
1028 );
1029 assert_eq!(command.command, ":");
1030 }
1031
1032 #[test]
1033 fn double_variable_change() {
1034 let crontab = Crontab::new(vec![
1035 Token::Variable(Variable {
1036 identifier: String::from("FOO"),
1037 value: String::from("bar"),
1038 }),
1039 Token::Variable(Variable {
1040 identifier: String::from("FOO"),
1041 value: String::from("baz"),
1042 }),
1043 Token::CronJob(CronJob {
1044 uid: 1,
1045 fingerprint: 13_376_942,
1046 tag: None,
1047 schedule: String::from("30 9 * * * "),
1048 command: String::from("echo 'gm'"),
1049 description: None,
1050 section: None,
1051 }),
1052 ]);
1053
1054 let job = crontab.get_job_from_uid(1).unwrap();
1055 let command = crontab.make_shell_command(job).unwrap();
1056
1057 assert_eq!(
1058 command.env,
1059 HashMap::from([(String::from("FOO"), String::from("baz"))])
1060 );
1061 assert_eq!(command.command, "echo 'gm'");
1062 }
1063
1064 #[test]
1065 fn run_cron_with_default_shell() {
1066 let crontab = Crontab::new(vec![Token::CronJob(CronJob {
1067 uid: 1,
1068 fingerprint: 13_376_942,
1069 tag: None,
1070 schedule: String::from("@reboot"),
1071 command: String::from("cat a-file.txt"),
1072 description: None,
1073 section: None,
1074 })]);
1075
1076 let job = crontab.get_job_from_uid(1).unwrap();
1077 let command = crontab.make_shell_command(job).unwrap();
1078
1079 assert_eq!(command.shell, DEFAULT_SHELL);
1080 assert_eq!(command.command, "cat a-file.txt");
1081 }
1082
1083 #[test]
1084 fn run_cron_with_different_shell() {
1085 let crontab = Crontab::new(vec![
1086 Token::Variable(Variable {
1087 identifier: String::from("SHELL"),
1088 value: String::from("/bin/bash"),
1089 }),
1090 Token::CronJob(CronJob {
1091 uid: 1,
1092 fingerprint: 13_376_942,
1093 tag: None,
1094 schedule: String::from("@hourly"),
1095 command: String::from("echo 'I am echoed by bash!'"),
1096 description: None,
1097 section: None,
1098 }),
1099 ]);
1100
1101 let job = crontab.get_job_from_uid(1).unwrap();
1102 let command = crontab.make_shell_command(job).unwrap();
1103
1104 assert_eq!(command.env, HashMap::new());
1105 assert_eq!(command.shell, "/bin/bash");
1106 assert_eq!(command.command, "echo 'I am echoed by bash!'");
1107 }
1108
1109 #[test]
1110 fn shell_variable_is_removed_from_env() {
1111 let crontab = Crontab::new(vec![
1112 Token::Variable(Variable {
1113 identifier: String::from("SHELL"),
1114 value: String::from("/bin/<custom>"),
1115 }),
1116 Token::CronJob(CronJob {
1117 uid: 1,
1118 fingerprint: 13_376_942,
1119 tag: None,
1120 schedule: String::from("@hourly"),
1121 command: String::from("echo 'I am echoed by a custom shell!'"),
1122 description: None,
1123 section: None,
1124 }),
1125 ]);
1126
1127 let job = crontab.get_job_from_uid(1).unwrap();
1128 let command = crontab.make_shell_command(job).unwrap();
1129
1130 assert!(!command.env.contains_key("SHELL"));
1131 assert_eq!(command.shell, "/bin/<custom>");
1132 }
1133
1134 #[test]
1135 fn double_shell_change() {
1136 let crontab = Crontab::new(vec![
1137 Token::Variable(Variable {
1138 identifier: String::from("SHELL"),
1139 value: String::from("/bin/bash"),
1140 }),
1141 Token::CronJob(CronJob {
1142 uid: 1,
1143 fingerprint: 13_376_942,
1144 tag: None,
1145 schedule: String::from("@hourly"),
1146 command: String::from("echo 'I am echoed by bash!'"),
1147 description: None,
1148 section: None,
1149 }),
1150 Token::Variable(Variable {
1151 identifier: String::from("SHELL"),
1152 value: String::from("/bin/zsh"),
1153 }),
1154 Token::CronJob(CronJob {
1155 uid: 2,
1156 fingerprint: 13_376_942,
1157 tag: None,
1158 schedule: String::from("@hourly"),
1159 command: String::from("echo 'I am echoed by zsh!'"),
1160 description: None,
1161 section: None,
1162 }),
1163 ]);
1164
1165 let job = crontab.get_job_from_uid(2).unwrap();
1166 let command = crontab.make_shell_command(job).unwrap();
1167
1168 assert_eq!(command.shell, "/bin/zsh");
1169 assert_eq!(command.command, "echo 'I am echoed by zsh!'");
1170 }
1171
1172 #[test]
1173 fn run_cron_with_default_home() {
1174 unsafe {
1175 env::set_var("HOME", "/home/<default>");
1176 }
1177
1178 let crontab = Crontab::new(vec![Token::CronJob(CronJob {
1179 uid: 1,
1180 fingerprint: 13_376_942,
1181 tag: None,
1182 schedule: String::from("@daily"),
1183 command: String::from("/usr/bin/bash ~/startup.sh"),
1184 description: None,
1185 section: None,
1186 })]);
1187
1188 let job = crontab.get_job_from_uid(1).unwrap();
1189 let command = crontab.make_shell_command(job).unwrap();
1190
1191 assert_eq!(command.home.to_string_lossy(), "/home/<default>");
1192 }
1193
1194 #[test]
1195 fn run_cron_with_different_home() {
1196 unsafe {
1197 env::set_var("HOME", "/home/<default>");
1198 }
1199
1200 let crontab = Crontab::new(vec![
1201 Token::Variable(Variable {
1202 identifier: String::from("HOME"),
1203 value: String::from("/home/<custom>"),
1204 }),
1205 Token::CronJob(CronJob {
1206 uid: 1,
1207 fingerprint: 13_376_942,
1208 tag: None,
1209 schedule: String::from("@yearly"),
1210 command: String::from("./cleanup.sh"),
1211 description: None,
1212 section: None,
1213 }),
1214 ]);
1215
1216 let job = crontab.get_job_from_uid(1).unwrap();
1217 let command = crontab.make_shell_command(job).unwrap();
1218
1219 assert_eq!(command.env, HashMap::new());
1220 assert_eq!(command.home.to_string_lossy(), "/home/<custom>");
1221 assert_eq!(command.command, "./cleanup.sh");
1222 }
1223
1224 #[test]
1225 fn home_variable_is_removed_from_env() {
1226 let crontab = Crontab::new(vec![
1227 Token::Variable(Variable {
1228 identifier: String::from("HOME"),
1229 value: String::from("/home/<custom>"),
1230 }),
1231 Token::CronJob(CronJob {
1232 uid: 1,
1233 fingerprint: 13_376_942,
1234 tag: None,
1235 schedule: String::from("@hourly"),
1236 command: String::from("echo 'I am echoed in a different Home!'"),
1237 description: None,
1238 section: None,
1239 }),
1240 ]);
1241
1242 let job = crontab.get_job_from_uid(1).unwrap();
1243 let command = crontab.make_shell_command(job).unwrap();
1244
1245 assert!(!command.env.contains_key("HOME"));
1246 assert_eq!(command.home.to_string_lossy(), "/home/<custom>");
1247 }
1248
1249 #[test]
1278 fn double_home_change() {
1279 let crontab = Crontab::new(vec![
1280 Token::Variable(Variable {
1281 identifier: String::from("HOME"),
1282 value: String::from("/home/user1"),
1283 }),
1284 Token::CronJob(CronJob {
1285 uid: 1,
1286 fingerprint: 13_376_942,
1287 tag: None,
1288 schedule: String::from("@hourly"),
1289 command: String::from("echo 'I run is user1's Home!'"),
1290 description: None,
1291 section: None,
1292 }),
1293 Token::Variable(Variable {
1294 identifier: String::from("HOME"),
1295 value: String::from("/home/user2"),
1296 }),
1297 Token::CronJob(CronJob {
1298 uid: 2,
1299 fingerprint: 13_376_942,
1300 tag: None,
1301 schedule: String::from("@hourly"),
1302 command: String::from("echo 'I run is user2's Home!'"),
1303 description: None,
1304 section: None,
1305 }),
1306 ]);
1307
1308 let job = crontab.get_job_from_uid(2).unwrap();
1309 let command = crontab.make_shell_command(job).unwrap();
1310
1311 assert_eq!(command.home.to_string_lossy(), "/home/user2");
1312 assert_eq!(command.command, "echo 'I run is user2's Home!'");
1313 }
1314
1315 #[test]
1316 fn run_cron_with_non_existing_job() {
1317 let crontab = Crontab::new(vec![Token::CronJob(CronJob {
1318 uid: 1,
1319 fingerprint: 13_376_942,
1320 tag: None,
1321 schedule: String::from("@hourly"),
1322 command: String::from("echo 'I am echoed by bash!'"),
1323 description: None,
1324 section: None,
1325 })]);
1326 let job_not_in_crontab = CronJob {
1327 uid: 42,
1328 fingerprint: 13_376_942,
1329 tag: None,
1330 schedule: String::from("@never"),
1331 command: String::from("sleep infinity"),
1332 description: None,
1333 section: None,
1334 };
1335
1336 let error = crontab.make_shell_command(&job_not_in_crontab).unwrap_err();
1337
1338 assert_eq!(error, "The given job is not in the crontab.");
1339 }
1340
1341 #[test]
1342 fn to_json() {
1343 let crontab = Crontab::new(vec![
1344 Token::Variable(Variable {
1345 identifier: String::from("HOME"),
1346 value: String::from("/home/user1"),
1347 }),
1348 Token::CronJob(CronJob {
1349 uid: 1,
1350 fingerprint: 13_376_942,
1351 tag: Some(String::from("taggy \"tag\"")),
1352 schedule: String::from("@daily"),
1353 command: String::from("/usr/bin/bash ~/startup.sh"),
1354 description: None,
1355 section: None,
1356 }),
1357 Token::Variable(Variable {
1358 identifier: String::from("HOME"),
1359 value: String::from("/home/user2"),
1360 }),
1361 Token::CronJob(CronJob {
1362 uid: 2,
1363 fingerprint: 17_118_619_922_108_271_534,
1364 tag: None,
1365 schedule: String::from("* * * * *"),
1366 command: String::from("echo \"$FOO\""),
1367 description: Some(JobDescription(String::from("Print \"variable\"."))),
1368 section: Some(tokens::JobSection {
1369 uid: 1,
1370 title: String::from("Some \"testing\" going on here..."),
1371 }),
1372 }),
1373 ]);
1374
1375 let json = crontab.to_json();
1376
1377 println!("{}", &json);
1378 assert_eq!(
1379 json,
1380 r#"[{"uid":1,"fingerprint":"cc1dae","tag":"taggy \"tag\"","schedule":"@daily","command":"/usr/bin/bash ~/startup.sh","description":null,"section":null},{"uid":2,"fingerprint":"ed918e1eee304bae","tag":null,"schedule":"* * * * *","command":"echo \"$FOO\"","description":"Print \"variable\".","section":{"uid":1,"title":"Some \"testing\" going on here..."}}]"#
1381 );
1382 }
1383}