1use crate::sync::{RwLock, RwLockReadGuard};
30use std::{
31 fmt::{self, Debug},
32 fs::{self, File, OpenOptions},
33 io::{self, Write},
34 path::{Path, PathBuf},
35 sync::atomic::{AtomicUsize, Ordering},
36};
37use symlink::{remove_symlink_file, symlink_file};
38use time::{format_description, Date, Duration, OffsetDateTime, Time};
39
40mod builder;
41pub use builder::{Builder, InitError};
42
43pub struct RollingFileAppender {
88 state: Inner,
89 writer: RwLock<File>,
90 #[cfg(test)]
91 now: Box<dyn Fn() -> OffsetDateTime + Send + Sync>,
92}
93
94#[derive(Debug)]
101pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);
102
103#[derive(Debug)]
104struct Inner {
105 log_directory: PathBuf,
106 log_filename_prefix: Option<String>,
107 log_filename_suffix: Option<String>,
108 date_format: Vec<format_description::FormatItem<'static>>,
109 rotation: Rotation,
110 next_date: AtomicUsize,
111 max_files: Option<usize>,
112}
113
114impl RollingFileAppender {
117 pub fn new(
143 rotation: Rotation,
144 directory: impl AsRef<Path>,
145 filename_prefix: impl AsRef<Path>,
146 ) -> RollingFileAppender {
147 let filename_prefix = filename_prefix
148 .as_ref()
149 .to_str()
150 .expect("filename prefix must be a valid UTF-8 string");
151 Self::builder()
152 .rotation(rotation)
153 .filename_prefix(filename_prefix)
154 .build(directory)
155 .expect("initializing rolling file appender failed")
156 }
157
158 #[must_use]
184 pub fn builder() -> Builder {
185 Builder::new()
186 }
187
188 fn from_builder(builder: &Builder, directory: impl AsRef<Path>) -> Result<Self, InitError> {
189 let Builder {
190 ref rotation,
191 ref prefix,
192 ref suffix,
193 ref max_files,
194 } = builder;
195 let directory = directory.as_ref().to_path_buf();
196 let now = OffsetDateTime::now_utc().to_offset(
197 clia_local_offset::current_local_offset().expect("Can not get local offset!"),
198 );
199 let (state, writer) = Inner::new(
200 now,
201 rotation.clone(),
202 directory,
203 prefix.clone(),
204 suffix.clone(),
205 *max_files,
206 )?;
207 Ok(Self {
208 state,
209 writer,
210 #[cfg(test)]
211 now: Box::new(OffsetDateTime::now_utc),
212 })
213 }
214
215 #[inline]
216 fn now(&self) -> OffsetDateTime {
217 #[cfg(test)]
218 return (self.now)();
219
220 #[cfg(not(test))]
221 OffsetDateTime::now_utc().to_offset(
222 clia_local_offset::current_local_offset().expect("Can not get local offset!"),
223 )
224 }
225}
226
227impl io::Write for RollingFileAppender {
228 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
229 let now = self.now();
230 let writer = self.writer.get_mut();
231 if let Some(current_time) = self.state.should_rollover(now) {
232 let _did_cas = self.state.advance_date(now, current_time);
233 debug_assert!(_did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp...");
234 self.state.refresh_writer(now, writer);
235 }
236 writer.write(buf)
237 }
238
239 fn flush(&mut self) -> io::Result<()> {
240 self.writer.get_mut().flush()
241 }
242}
243
244impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender {
245 type Writer = RollingWriter<'a>;
246 fn make_writer(&'a self) -> Self::Writer {
247 let now = self.now();
248
249 if let Some(current_time) = self.state.should_rollover(now) {
251 if self.state.advance_date(now, current_time) {
254 self.state.refresh_writer(now, &mut self.writer.write());
255 }
256 }
257 RollingWriter(self.writer.read())
258 }
259}
260
261impl fmt::Debug for RollingFileAppender {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 f.debug_struct("RollingFileAppender")
266 .field("state", &self.state)
267 .field("writer", &self.writer)
268 .finish()
269 }
270}
271
272pub fn minutely(
301 directory: impl AsRef<Path>,
302 file_name_prefix: impl AsRef<Path>,
303) -> RollingFileAppender {
304 RollingFileAppender::new(Rotation::MINUTELY, directory, file_name_prefix)
305}
306
307pub fn hourly(
336 directory: impl AsRef<Path>,
337 file_name_prefix: impl AsRef<Path>,
338) -> RollingFileAppender {
339 RollingFileAppender::new(Rotation::HOURLY, directory, file_name_prefix)
340}
341
342pub fn daily(
372 directory: impl AsRef<Path>,
373 file_name_prefix: impl AsRef<Path>,
374) -> RollingFileAppender {
375 RollingFileAppender::new(Rotation::DAILY, directory, file_name_prefix)
376}
377
378pub fn never(directory: impl AsRef<Path>, file_name: impl AsRef<Path>) -> RollingFileAppender {
406 RollingFileAppender::new(Rotation::NEVER, directory, file_name)
407}
408
409#[derive(Clone, Eq, PartialEq, Debug)]
445pub struct Rotation(RotationKind);
446
447#[derive(Clone, Eq, PartialEq, Debug)]
448enum RotationKind {
449 Minutely,
450 Hourly,
451 Daily,
452 Never,
453}
454
455impl Rotation {
456 pub const MINUTELY: Self = Self(RotationKind::Minutely);
458 pub const HOURLY: Self = Self(RotationKind::Hourly);
460 pub const DAILY: Self = Self(RotationKind::Daily);
462 pub const NEVER: Self = Self(RotationKind::Never);
464
465 pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option<OffsetDateTime> {
466 let unrounded_next_date = match *self {
467 Rotation::MINUTELY => *current_date + Duration::minutes(1),
468 Rotation::HOURLY => *current_date + Duration::hours(1),
469 Rotation::DAILY => *current_date + Duration::days(1),
470 Rotation::NEVER => return None,
471 };
472 Some(self.round_date(&unrounded_next_date))
473 }
474
475 pub(crate) fn round_date(&self, date: &OffsetDateTime) -> OffsetDateTime {
477 match *self {
478 Rotation::MINUTELY => {
479 let time = Time::from_hms(date.hour(), date.minute(), 0)
480 .expect("Invalid time; this is a bug in tracing-appender");
481 date.replace_time(time)
482 }
483 Rotation::HOURLY => {
484 let time = Time::from_hms(date.hour(), 0, 0)
485 .expect("Invalid time; this is a bug in tracing-appender");
486 date.replace_time(time)
487 }
488 Rotation::DAILY => {
489 let time = Time::from_hms(0, 0, 0)
490 .expect("Invalid time; this is a bug in tracing-appender");
491 date.replace_time(time)
492 }
493 Rotation::NEVER => {
495 unreachable!("Rotation::NEVER is impossible to round.")
496 }
497 }
498 }
499
500 fn date_format(&self) -> Vec<format_description::FormatItem<'static>> {
501 match *self {
502 Rotation::MINUTELY => format_description::parse("[year]-[month]-[day]-[hour]-[minute]"),
503 Rotation::HOURLY => format_description::parse("[year]-[month]-[day]-[hour]"),
504 Rotation::DAILY => format_description::parse("[year]-[month]-[day]"),
505 Rotation::NEVER => format_description::parse("[year]-[month]-[day]"),
506 }
507 .expect("Unable to create a formatter; this is a bug in tracing-appender")
508 }
509}
510
511impl io::Write for RollingWriter<'_> {
514 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
515 (&*self.0).write(buf)
516 }
517
518 fn flush(&mut self) -> io::Result<()> {
519 (&*self.0).flush()
520 }
521}
522
523impl Inner {
526 fn new(
527 now: OffsetDateTime,
528 rotation: Rotation,
529 directory: impl AsRef<Path>,
530 log_filename_prefix: Option<String>,
531 log_filename_suffix: Option<String>,
532 max_files: Option<usize>,
533 ) -> Result<(Self, RwLock<File>), builder::InitError> {
534 let log_directory = directory.as_ref().to_path_buf();
535 let date_format = rotation.date_format();
536 let next_date = rotation.next_date(&now);
537
538 let symlink_path = Path::new(&log_directory).join(
539 &log_filename_prefix
540 .clone()
541 .unwrap_or("nofilename".to_owned()),
542 );
543
544 let inner = Inner {
545 log_directory,
546 log_filename_prefix,
547 log_filename_suffix,
548 date_format,
549 next_date: AtomicUsize::new(
550 next_date
551 .map(|date| date.unix_timestamp() as usize)
552 .unwrap_or(0),
553 ),
554 rotation,
555 max_files,
556 };
557 let filename = inner.join_date(&now);
558 let writer = RwLock::new(create_writer(inner.log_directory.as_ref(), &filename)?);
559
560 let latest_path = Path::new(&filename);
562 let _ = remove_symlink_file(&symlink_path);
563 if let Err(err) = symlink_file(&latest_path, &symlink_path) {
564 eprintln!("Couldn't create symlink: {}", err);
565 }
566
567 Ok((inner, writer))
568 }
569
570 pub(crate) fn join_date(&self, date: &OffsetDateTime) -> String {
571 let date = date
572 .format(&self.date_format)
573 .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
574
575 match (
576 &self.rotation,
577 &self.log_filename_prefix,
578 &self.log_filename_suffix,
579 ) {
580 (&Rotation::NEVER, Some(filename), None) => filename.to_string(),
581 (&Rotation::NEVER, Some(filename), Some(suffix)) => format!("{}.{}", filename, suffix),
582 (&Rotation::NEVER, None, Some(suffix)) => suffix.to_string(),
583 (_, Some(filename), Some(suffix)) => format!("{}.{}.{}", filename, date, suffix),
584 (_, Some(filename), None) => format!("{}.{}", filename, date),
585 (_, None, Some(suffix)) => format!("{}.{}", date, suffix),
586 (_, None, None) => date,
587 }
588 }
589
590 fn prune_old_logs(&self, max_files: usize) {
591 let files = fs::read_dir(&self.log_directory).map(|dir| {
592 dir.filter_map(|entry| {
593 let entry = entry.ok()?;
594 let metadata = entry.metadata().ok()?;
595
596 if !metadata.is_file() {
599 return None;
600 }
601
602 let filename = entry.file_name();
603 let filename = filename.to_str()?;
605 if let Some(prefix) = &self.log_filename_prefix {
606 if !filename.starts_with(prefix) {
607 return None;
608 }
609 }
610
611 if let Some(suffix) = &self.log_filename_suffix {
612 if !filename.ends_with(suffix) {
613 return None;
614 }
615 }
616
617 if self.log_filename_prefix.is_none()
618 && self.log_filename_suffix.is_none()
619 && Date::parse(filename, &self.date_format).is_err()
620 {
621 return None;
622 }
623
624 let created = metadata.created().ok()?;
625 Some((entry, created))
626 })
627 .collect::<Vec<_>>()
628 });
629
630 let mut files = match files {
631 Ok(files) => files,
632 Err(error) => {
633 eprintln!("Error reading the log directory/files: {}", error);
634 return;
635 }
636 };
637 if files.len() < max_files {
638 return;
639 }
640
641 files.sort_by_key(|(_, created_at)| *created_at);
643
644 for (file, _) in files.iter().take(files.len() - (max_files - 1)) {
646 if let Err(error) = fs::remove_file(file.path()) {
647 eprintln!(
648 "Failed to remove old log file {}: {}",
649 file.path().display(),
650 error
651 );
652 }
653 }
654 }
655
656 fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
657 let filename = self.join_date(&now);
658
659 if let Some(max_files) = self.max_files {
660 self.prune_old_logs(max_files);
661 }
662
663 match create_writer(&self.log_directory, &filename) {
664 Ok(new_file) => {
665 if let Err(err) = file.flush() {
666 eprintln!("Couldn't flush previous writer: {}", err);
667 }
668 *file = new_file;
669
670 let latest_path = Path::new(&filename);
672 let symlink_path = Path::new(&self.log_directory).join(
673 &self
674 .log_filename_prefix
675 .clone()
676 .unwrap_or("nofilename".to_owned()),
677 );
678 let _ = remove_symlink_file(&symlink_path);
679 if let Err(err) = symlink_file(&latest_path, &symlink_path) {
680 eprintln!("Couldn't create symlink: {}", err);
681 }
682 }
683 Err(err) => eprintln!("Couldn't create writer for logs: {}", err),
684 }
685 }
686
687 fn should_rollover(&self, date: OffsetDateTime) -> Option<usize> {
696 let next_date = self.next_date.load(Ordering::Acquire);
697 if next_date == 0 {
699 return None;
700 }
701
702 if date.unix_timestamp() as usize >= next_date {
703 return Some(next_date);
704 }
705
706 None
707 }
708
709 fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool {
710 let next_date = self
711 .rotation
712 .next_date(&now)
713 .map(|date| date.unix_timestamp() as usize)
714 .unwrap_or(0);
715 self.next_date
716 .compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire)
717 .is_ok()
718 }
719}
720
721fn create_writer(directory: &Path, filename: &str) -> Result<File, InitError> {
722 let path = directory.join(filename);
723 let mut open_options = OpenOptions::new();
724 open_options.append(true).create(true);
725
726 let new_file = open_options.open(path.as_path());
727 if new_file.is_err() {
728 if let Some(parent) = path.parent() {
729 fs::create_dir_all(parent).map_err(InitError::ctx("failed to create log directory"))?;
730 return open_options
731 .open(path)
732 .map_err(InitError::ctx("failed to create initial log file"));
733 }
734 }
735
736 new_file.map_err(InitError::ctx("failed to create initial log file"))
737}
738
739#[cfg(test)]
740mod test {
741 use super::*;
742 use std::fs;
743 use std::io::Write;
744
745 fn find_str_in_log(dir_path: &Path, expected_value: &str) -> bool {
746 let dir_contents = fs::read_dir(dir_path).expect("Failed to read directory");
747
748 for entry in dir_contents {
749 let path = entry.expect("Expected dir entry").path();
750 let file = fs::read_to_string(&path).expect("Failed to read file");
751 println!("path={}\nfile={:?}", path.display(), file);
752
753 if file.as_str() == expected_value {
754 return true;
755 }
756 }
757
758 false
759 }
760
761 fn write_to_log(appender: &mut RollingFileAppender, msg: &str) {
762 appender
763 .write_all(msg.as_bytes())
764 .expect("Failed to write to appender");
765 appender.flush().expect("Failed to flush!");
766 }
767
768 fn test_appender(rotation: Rotation, file_prefix: &str) {
769 let directory = tempfile::tempdir().expect("failed to create tempdir");
770 let mut appender = RollingFileAppender::new(rotation, directory.path(), file_prefix);
771
772 let expected_value = "Hello";
773 write_to_log(&mut appender, expected_value);
774 assert!(find_str_in_log(directory.path(), expected_value));
775
776 directory
777 .close()
778 .expect("Failed to explicitly close TempDir. TempDir should delete once out of scope.")
779 }
780
781 #[test]
782 fn write_minutely_log() {
783 test_appender(Rotation::HOURLY, "minutely.log");
784 }
785
786 #[test]
787 fn write_hourly_log() {
788 test_appender(Rotation::HOURLY, "hourly.log");
789 }
790
791 #[test]
792 fn write_daily_log() {
793 test_appender(Rotation::DAILY, "daily.log");
794 }
795
796 #[test]
797 fn write_never_log() {
798 test_appender(Rotation::NEVER, "never.log");
799 }
800
801 #[test]
802 fn test_rotations() {
803 let now = OffsetDateTime::now_utc();
805 let next = Rotation::MINUTELY.next_date(&now).unwrap();
806 assert_eq!((now + Duration::MINUTE).minute(), next.minute());
807
808 let now = OffsetDateTime::now_utc();
810 let next = Rotation::HOURLY.next_date(&now).unwrap();
811 assert_eq!((now + Duration::HOUR).hour(), next.hour());
812
813 let now = OffsetDateTime::now_utc();
815 let next = Rotation::DAILY.next_date(&now).unwrap();
816 assert_eq!((now + Duration::DAY).day(), next.day());
817
818 let now = OffsetDateTime::now_utc();
820 let next = Rotation::NEVER.next_date(&now);
821 assert!(next.is_none());
822 }
823
824 #[test]
825 #[should_panic(
826 expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round."
827 )]
828 fn test_never_date_rounding() {
829 let now = OffsetDateTime::now_utc();
830 let _ = Rotation::NEVER.round_date(&now);
831 }
832
833 #[test]
834 fn test_path_concatenation() {
835 let format = format_description::parse(
836 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
837 sign:mandatory]:[offset_minute]:[offset_second]",
838 )
839 .unwrap();
840 let directory = tempfile::tempdir().expect("failed to create tempdir");
841
842 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
843
844 struct TestCase {
845 expected: &'static str,
846 rotation: Rotation,
847 prefix: Option<&'static str>,
848 suffix: Option<&'static str>,
849 }
850
851 let test = |TestCase {
852 expected,
853 rotation,
854 prefix,
855 suffix,
856 }| {
857 let (inner, _) = Inner::new(
858 now,
859 rotation.clone(),
860 directory.path(),
861 prefix.map(ToString::to_string),
862 suffix.map(ToString::to_string),
863 None,
864 )
865 .unwrap();
866 let path = inner.join_date(&now);
867 assert_eq!(
868 expected, path,
869 "rotation = {:?}, prefix = {:?}, suffix = {:?}",
870 rotation, prefix, suffix
871 );
872 };
873
874 let test_cases = vec![
875 TestCase {
877 expected: "app.log.2020-02-01-10-01",
878 rotation: Rotation::MINUTELY,
879 prefix: Some("app.log"),
880 suffix: None,
881 },
882 TestCase {
883 expected: "app.log.2020-02-01-10",
884 rotation: Rotation::HOURLY,
885 prefix: Some("app.log"),
886 suffix: None,
887 },
888 TestCase {
889 expected: "app.log.2020-02-01",
890 rotation: Rotation::DAILY,
891 prefix: Some("app.log"),
892 suffix: None,
893 },
894 TestCase {
895 expected: "app.log",
896 rotation: Rotation::NEVER,
897 prefix: Some("app.log"),
898 suffix: None,
899 },
900 TestCase {
902 expected: "app.2020-02-01-10-01.log",
903 rotation: Rotation::MINUTELY,
904 prefix: Some("app"),
905 suffix: Some("log"),
906 },
907 TestCase {
908 expected: "app.2020-02-01-10.log",
909 rotation: Rotation::HOURLY,
910 prefix: Some("app"),
911 suffix: Some("log"),
912 },
913 TestCase {
914 expected: "app.2020-02-01.log",
915 rotation: Rotation::DAILY,
916 prefix: Some("app"),
917 suffix: Some("log"),
918 },
919 TestCase {
920 expected: "app.log",
921 rotation: Rotation::NEVER,
922 prefix: Some("app"),
923 suffix: Some("log"),
924 },
925 TestCase {
927 expected: "2020-02-01-10-01.log",
928 rotation: Rotation::MINUTELY,
929 prefix: None,
930 suffix: Some("log"),
931 },
932 TestCase {
933 expected: "2020-02-01-10.log",
934 rotation: Rotation::HOURLY,
935 prefix: None,
936 suffix: Some("log"),
937 },
938 TestCase {
939 expected: "2020-02-01.log",
940 rotation: Rotation::DAILY,
941 prefix: None,
942 suffix: Some("log"),
943 },
944 TestCase {
945 expected: "log",
946 rotation: Rotation::NEVER,
947 prefix: None,
948 suffix: Some("log"),
949 },
950 ];
951 for test_case in test_cases {
952 test(test_case)
953 }
954 }
955
956 #[test]
957 fn test_make_writer() {
958 use std::sync::{Arc, Mutex};
959 use tracing_subscriber::prelude::*;
960
961 let format = format_description::parse(
962 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
963 sign:mandatory]:[offset_minute]:[offset_second]",
964 )
965 .unwrap();
966
967 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
968 let directory = tempfile::tempdir().expect("failed to create tempdir");
969 let (state, writer) = Inner::new(
970 now,
971 Rotation::HOURLY,
972 directory.path(),
973 Some("test_make_writer".to_string()),
974 None,
975 None,
976 )
977 .unwrap();
978
979 let clock = Arc::new(Mutex::new(now));
980 let now = {
981 let clock = clock.clone();
982 Box::new(move || *clock.lock().unwrap())
983 };
984 let appender = RollingFileAppender { state, writer, now };
985 let default = tracing_subscriber::fmt()
986 .without_time()
987 .with_level(false)
988 .with_target(false)
989 .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
990 .with_writer(appender)
991 .finish()
992 .set_default();
993
994 tracing::info!("file 1");
995
996 (*clock.lock().unwrap()) += Duration::seconds(1);
998
999 tracing::info!("file 1");
1000
1001 (*clock.lock().unwrap()) += Duration::hours(1);
1003
1004 tracing::info!("file 2");
1005
1006 (*clock.lock().unwrap()) += Duration::seconds(1);
1008
1009 tracing::info!("file 2");
1010
1011 drop(default);
1012
1013 let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
1014 println!("dir={:?}", dir_contents);
1015 for entry in dir_contents {
1016 println!("entry={:?}", entry);
1017 let path = entry.expect("Expected dir entry").path();
1018 let file = fs::read_to_string(&path).expect("Failed to read file");
1019 println!("path={}\nfile={:?}", path.display(), file);
1020
1021 match path
1022 .extension()
1023 .expect("found a file without a date!")
1024 .to_str()
1025 .expect("extension should be UTF8")
1026 {
1027 "2020-02-01-10" => {
1028 assert_eq!("file 1\nfile 1\n", file);
1029 }
1030 "2020-02-01-11" => {
1031 assert_eq!("file 2\nfile 2\n", file);
1032 }
1033 x => panic!("unexpected date {}", x),
1034 }
1035 }
1036 }
1037
1038 #[test]
1039 fn test_max_log_files() {
1040 use std::sync::{Arc, Mutex};
1041 use tracing_subscriber::prelude::*;
1042
1043 let format = format_description::parse(
1044 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1045 sign:mandatory]:[offset_minute]:[offset_second]",
1046 )
1047 .unwrap();
1048
1049 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1050 let directory = tempfile::tempdir().expect("failed to create tempdir");
1051 let (state, writer) = Inner::new(
1052 now,
1053 Rotation::HOURLY,
1054 directory.path(),
1055 Some("test_max_log_files".to_string()),
1056 None,
1057 Some(2),
1058 )
1059 .unwrap();
1060
1061 let clock = Arc::new(Mutex::new(now));
1062 let now = {
1063 let clock = clock.clone();
1064 Box::new(move || *clock.lock().unwrap())
1065 };
1066 let appender = RollingFileAppender { state, writer, now };
1067 let default = tracing_subscriber::fmt()
1068 .without_time()
1069 .with_level(false)
1070 .with_target(false)
1071 .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
1072 .with_writer(appender)
1073 .finish()
1074 .set_default();
1075
1076 tracing::info!("file 1");
1077
1078 (*clock.lock().unwrap()) += Duration::seconds(1);
1080
1081 tracing::info!("file 1");
1082
1083 (*clock.lock().unwrap()) += Duration::hours(1);
1085
1086 std::thread::sleep(std::time::Duration::from_secs(1));
1090
1091 tracing::info!("file 2");
1092
1093 (*clock.lock().unwrap()) += Duration::seconds(1);
1095
1096 tracing::info!("file 2");
1097
1098 (*clock.lock().unwrap()) += Duration::hours(1);
1100
1101 std::thread::sleep(std::time::Duration::from_secs(1));
1103
1104 tracing::info!("file 3");
1105
1106 (*clock.lock().unwrap()) += Duration::seconds(1);
1108
1109 tracing::info!("file 3");
1110
1111 drop(default);
1112
1113 let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
1114 println!("dir={:?}", dir_contents);
1115
1116 for entry in dir_contents {
1117 println!("entry={:?}", entry);
1118 let path = entry.expect("Expected dir entry").path();
1119 let file = fs::read_to_string(&path).expect("Failed to read file");
1120 println!("path={}\nfile={:?}", path.display(), file);
1121
1122 match path
1123 .extension()
1124 .expect("found a file without a date!")
1125 .to_str()
1126 .expect("extension should be UTF8")
1127 {
1128 "2020-02-01-10" => {
1129 panic!("this file should have been pruned already!");
1130 }
1131 "2020-02-01-11" => {
1132 assert_eq!("file 2\nfile 2\n", file);
1133 }
1134 "2020-02-01-12" => {
1135 assert_eq!("file 3\nfile 3\n", file);
1136 }
1137 x => panic!("unexpected date {}", x),
1138 }
1139 }
1140 }
1141}