flexi_logger/parameters/
file_spec.rs

1use crate::{writers::file_log_writer::InfixFilter, DeferredNow, FlexiLoggerError};
2use std::{
3    ffi::{OsStr, OsString},
4    ops::Add,
5    path::{Path, PathBuf},
6};
7
8/// Builder object for specifying the name and path of the log output file.
9///
10/// The filename is built from several partially components, using this pattern:
11///
12/// ```<filename> = [<basename>][_][<discriminant>][_][<starttime>][_][<infix>][.<suffix>]```
13///
14/// - `[<basename>]`: This is by default the program's name, but can be set to a different value
15///   or suppressed at all.
16///
17/// - `[_]`: Consecutive name parts are separated by an underscore.
18///   No underscore is used at the beginning of the filename and directly before the suffix.
19///
20/// - `[<discriminant>]`: some optional name part that allows further differentiations.
21///
22/// - `[<starttime>]`: denotes the point in time when the program was started, if used.
23///
24/// - `[infix]`: used with rotation to differentiate consecutive files.
25///
26/// Without rotation, the default filename pattern uses the program name as basename,
27/// no discriminant, the timestamp of the program start
28/// (printed in the format "YYYY-MM-DD_hh-mm-ss"),
29/// and the suffix `.log`, e.g.
30///
31/// ```myprog_2015-07-08_10-44-11.log```.
32///
33/// This ensures that with every program start a new trace file is written that can easily
34/// be associated with a concrete program run.
35///
36/// When the timestamp is suppressed with [`FileSpec::suppress_timestamp`],
37/// you get a fixed output file name.
38/// It is then worth considering whether a new program start should discard
39/// the content of an already existing outputfile or if it should append its new content to it
40/// (see [`Logger::append`](crate::Logger::append)).
41///
42/// With rotation, the timestamp is by default suppressed and instead the infix is used.
43/// The infix starts always with "r".
44/// For more details how its precise content can be influenced, see [`Naming`](crate::Naming).
45///
46#[derive(Debug, Clone, Eq, PartialEq)]
47pub struct FileSpec {
48    pub(crate) directory: PathBuf,
49    pub(crate) basename: String,
50    pub(crate) o_discriminant: Option<String>,
51    timestamp_cfg: TimestampCfg,
52    o_suffix: Option<String>,
53    pub(crate) use_utc: bool,
54}
55impl Default for FileSpec {
56    /// Describes a file in the current folder,
57    /// using, as its filestem, the program name followed by the current timestamp,
58    /// and the suffix ".log".
59    fn default() -> Self {
60        FileSpec {
61            directory: PathBuf::from("."),
62            basename: Self::default_basename(),
63            o_discriminant: None,
64            timestamp_cfg: TimestampCfg::Default,
65            o_suffix: Some(String::from("log")),
66            use_utc: false,
67        }
68    }
69}
70impl FileSpec {
71    fn default_basename() -> String {
72        let arg0 = std::env::args().next().unwrap_or_else(|| "rs".to_owned());
73        Path::new(&arg0).file_stem().map(OsStr::to_string_lossy).unwrap(/*cannot fail*/).to_string()
74    }
75
76    /// The provided path should describe a log file.
77    /// If it exists, it must be a file, not a folder.
78    /// If necessary, parent folders will be created.
79    ///
80    /// ```rust
81    /// # use flexi_logger::FileSpec;
82    /// assert_eq!(
83    ///     FileSpec::default()
84    ///         .directory("/a/b/c")
85    ///         .basename("foo")
86    ///         .suppress_timestamp()
87    ///         .suffix("bar"),
88    ///     FileSpec::try_from("/a/b/c/foo.bar").unwrap()
89    /// );
90    /// ```
91    /// # Errors
92    ///
93    /// [`FlexiLoggerError::OutputBadFile`] if the given path exists and is a folder.
94    /// [`FlexiLoggerError::BadFileSpec`] if deriving the `FileSpec` from the given path fails.
95    #[allow(clippy::missing_panics_doc)]
96    pub fn try_from<P: Into<PathBuf>>(p: P) -> Result<Self, FlexiLoggerError> {
97        let input: PathBuf = p.into();
98
99        if input.is_dir() {
100            Err(FlexiLoggerError::BadFileSpec("File path is a directory"))
101        } else {
102            let input_as_str = input.as_os_str().to_string_lossy();
103            if input_as_str.is_empty() {
104                Err(FlexiLoggerError::BadFileSpec("File path is empty"))
105            } else if input_as_str.ends_with('/')
106                || input_as_str.ends_with("/.")
107                || input_as_str.ends_with("/..")
108            {
109                Err(FlexiLoggerError::BadFileSpec(
110                    "Path ends with '/' or '/.' or '/..'",
111                ))
112            } else if input
113                .file_name()
114                .ok_or(FlexiLoggerError::OutputBadFile)?
115                .to_string_lossy()
116                .starts_with('.')
117                && input.extension().is_none()
118            {
119                Err(FlexiLoggerError::BadFileSpec(
120                    "File name cannot start with '.' without an extension",
121                ))
122            } else {
123                match input.parent() {
124                    None => Err(FlexiLoggerError::BadFileSpec(
125                        "File path has no parent directory",
126                    )),
127                    Some(parent) => {
128                        let filespec = FileSpec {
129                            directory: if parent.as_os_str().is_empty() {
130                                PathBuf::from(".")
131                            } else {
132                                parent.to_path_buf()
133                            },
134                            basename: input.file_stem().unwrap(/*ok*/).to_string_lossy().to_string(),
135                            o_discriminant: None,
136                            o_suffix: input.extension().map(|s| s.to_string_lossy().to_string()),
137                            timestamp_cfg: TimestampCfg::No,
138                            use_utc: false,
139                        };
140                        Ok(filespec)
141                    }
142                }
143            }
144        }
145    }
146
147    /// Makes the logger not include a basename into the names of the log files
148    ///
149    /// Equivalent to `basename("")`.
150    #[must_use]
151    pub fn suppress_basename(self) -> Self {
152        self.basename("")
153    }
154
155    /// The specified String is used as the basename of the log file name,
156    /// instead of the program name. Using a file separator within the argument is discouraged.
157    #[must_use]
158    pub fn basename<S: Into<String>>(mut self, basename: S) -> Self {
159        self.basename = basename.into();
160        self
161    }
162
163    /// The specified String is used as the basename of the log file,
164    /// instead of the program name, which is used when `None` is given.
165    #[must_use]
166    pub fn o_basename<S: Into<String>>(mut self, o_basename: Option<S>) -> Self {
167        self.basename = o_basename.map_or_else(Self::default_basename, Into::into);
168        self
169    }
170
171    /// Specifies a folder for the log files.
172    ///
173    /// If the specified folder does not exist, it will be created.
174    /// By default, the log files are created in the folder where the program was started.
175    #[must_use]
176    pub fn directory<P: Into<PathBuf>>(mut self, directory: P) -> Self {
177        self.directory = directory.into();
178        self
179    }
180
181    /// Specifies a folder for the log files.
182    ///
183    /// If the specified folder does not exist, it will be created.
184    /// With None, the log files are created in the folder where the program was started.
185    #[must_use]
186    pub fn o_directory<P: Into<PathBuf>>(mut self, directory: Option<P>) -> Self {
187        self.directory = directory.map_or_else(|| PathBuf::from("."), Into::into);
188        self
189    }
190
191    /// The specified String is added to the log file name.
192    #[must_use]
193    pub fn discriminant<S: Into<String>>(self, discriminant: S) -> Self {
194        self.o_discriminant(Some(discriminant))
195    }
196
197    /// The specified String is added to the log file name.
198    #[must_use]
199    pub fn o_discriminant<S: Into<String>>(mut self, o_discriminant: Option<S>) -> Self {
200        self.o_discriminant = o_discriminant.map(Into::into);
201        self
202    }
203    /// Specifies a suffix for the log files.
204    ///
205    /// Equivalent to `o_suffix(Some(suffix))`.
206    #[must_use]
207    pub fn suffix<S: Into<String>>(self, suffix: S) -> Self {
208        self.o_suffix(Some(suffix))
209    }
210
211    /// Specifies a suffix for the log files, or supresses the use of a suffix completely.
212    ///
213    /// The default suffix is "log".
214    #[must_use]
215    pub fn o_suffix<S: Into<String>>(mut self, o_suffix: Option<S>) -> Self {
216        self.o_suffix = o_suffix.map(Into::into);
217        self
218    }
219
220    /// Makes the logger not include the start time into the names of the log files
221    ///
222    /// Equivalent to `use_timestamp(false)`.
223    #[must_use]
224    pub fn suppress_timestamp(self) -> Self {
225        self.use_timestamp(false)
226    }
227
228    /// Defines if the start time should be included into the names of the log files.
229    ///
230    /// The _default_ behavior depends on the usage:
231    /// - without rotation, a timestamp is by default included into the name
232    /// - with rotation, the timestamp is by default suppressed
233    #[must_use]
234    pub fn use_timestamp(mut self, use_timestamp: bool) -> Self {
235        self.timestamp_cfg = if use_timestamp {
236            TimestampCfg::Yes
237        } else {
238            TimestampCfg::No
239        };
240        self
241    }
242
243    #[doc(hidden)]
244    #[must_use]
245    pub fn used_directory(&self) -> PathBuf {
246        self.directory.clone()
247    }
248    pub(crate) fn has_basename(&self) -> bool {
249        !self.basename.is_empty()
250    }
251    pub(crate) fn has_discriminant(&self) -> bool {
252        self.o_discriminant.is_some()
253    }
254    pub(crate) fn uses_timestamp(&self) -> bool {
255        matches!(self.timestamp_cfg, TimestampCfg::Yes)
256    }
257
258    // If no decision was done yet, decide now whether to include a timestamp
259    // into the names of the log files.
260    pub(crate) fn if_default_use_timestamp(&mut self, use_timestamp: bool) {
261        if let TimestampCfg::Default = self.timestamp_cfg {
262            self.timestamp_cfg = if use_timestamp {
263                TimestampCfg::Yes
264            } else {
265                TimestampCfg::No
266            };
267        }
268    }
269
270    pub(crate) fn get_directory(&self) -> PathBuf {
271        self.directory.clone()
272    }
273
274    pub(crate) fn get_suffix(&self) -> Option<String> {
275        self.o_suffix.clone()
276    }
277
278    // basename + o_discriminant + o_timestamp
279    pub(crate) fn fixed_name_part(&self) -> String {
280        let mut fixed_name_part = self.basename.clone();
281        fixed_name_part.reserve(50);
282
283        if let Some(discriminant) = &self.o_discriminant {
284            append_underscore_if_not_empty(&mut fixed_name_part);
285            fixed_name_part.push_str(discriminant);
286        }
287        if let Some(timestamp) = &self.timestamp_cfg.get_timestamp() {
288            append_underscore_if_not_empty(&mut fixed_name_part);
289            fixed_name_part.push_str(timestamp);
290        }
291        fixed_name_part
292    }
293
294    /// Derives a `PathBuf` from the spec and the given infix.
295    #[must_use]
296    pub fn as_pathbuf(&self, o_infix: Option<&str>) -> PathBuf {
297        let mut filename = self.fixed_name_part();
298
299        if let Some(infix) = o_infix {
300            if !infix.is_empty() {
301                append_underscore_if_not_empty(&mut filename);
302                filename.push_str(infix);
303            }
304        }
305        if let Some(suffix) = &self.o_suffix {
306            filename.push('.');
307            filename.push_str(suffix);
308        }
309
310        let mut p_path = self.directory.clone();
311        p_path.push(filename);
312        p_path
313    }
314
315    // handles collisions by appending ".restart-<number>" to the infix, if necessary
316    pub(crate) fn collision_free_infix_for_rotated_file(&self, infix: &str) -> String {
317        let uncompressed_files = self.list_of_files(
318            &InfixFilter::Equls(infix.to_string()),
319            self.o_suffix.as_deref(),
320        );
321        let compressed_files =
322            self.list_of_files(&InfixFilter::Equls(infix.to_string()), Some("gz"));
323
324        let mut restart_siblings = uncompressed_files
325            .into_iter()
326            .chain(compressed_files)
327            .filter(|pb| {
328                // ignore .gz suffix
329                let mut pb2 = PathBuf::from(pb);
330                if pb2.extension() == Some(OsString::from("gz").as_ref()) {
331                    pb2.set_extension("");
332                }
333                // suffix must match the given suffix, if one is given
334                match self.o_suffix {
335                    Some(ref sfx) => pb2.extension() == Some(OsString::from(sfx).as_ref()),
336                    None => true,
337                }
338            })
339            .filter(|pb| {
340                pb.file_name()
341                    .unwrap()
342                    .to_string_lossy()
343                    .contains(".restart-")
344            })
345            .collect::<Vec<PathBuf>>();
346
347        let new_path = self.as_pathbuf(Some(infix));
348        let new_path_with_gz = {
349            let mut new_path_with_gz = new_path.clone();
350            new_path_with_gz
351                .set_extension([self.o_suffix.as_deref().unwrap_or(""), ".gz"].concat());
352            new_path_with_gz
353        };
354
355        // if collision would occur (new_path or compressed new_path exists already),
356        // find highest restart and add 1, else continue without restart
357        if new_path.exists() || new_path_with_gz.exists() || !restart_siblings.is_empty() {
358            let next_number = if restart_siblings.is_empty() {
359                0
360            } else {
361                restart_siblings.sort_unstable();
362                let new_path = restart_siblings.pop().unwrap(/*ok*/);
363                let file_stem_string = if self.o_suffix.is_some() {
364                    new_path
365                    .file_stem().unwrap(/*ok*/)
366                    .to_string_lossy().to_string()
367                } else {
368                    new_path.to_string_lossy().to_string()
369                };
370                let index = file_stem_string.find(".restart-").unwrap(/*ok*/);
371                file_stem_string[(index + 9)..(index + 13)].parse::<usize>().unwrap(/*ok*/) + 1
372            };
373
374            infix.to_string().add(&format!(".restart-{next_number:04}"))
375        } else {
376            infix.to_string()
377        }
378    }
379
380    pub(crate) fn list_of_files(
381        &self,
382        infix_filter: &InfixFilter,
383        o_suffix: Option<&str>,
384    ) -> Vec<PathBuf> {
385        self.filter_files(&self.read_dir_related_files(), infix_filter, o_suffix)
386    }
387
388    // returns an ordered list of all files in the right directory that start with the fixed_name_part
389    pub(crate) fn read_dir_related_files(&self) -> Vec<PathBuf> {
390        let fixed_name_part = self.fixed_name_part();
391        let mut log_files = std::fs::read_dir(&self.directory)
392            .unwrap(/*ignore errors from reading the directory*/)
393            .flatten(/*ignore errors from reading entries in the directory*/)
394            .filter(|entry| entry.path().is_file())
395            .map(|de| de.path())
396            .filter(|path| {
397                // fixed name part must match
398                if let Some(fln) = path.file_name() {
399                    fln.to_string_lossy(/*good enough*/).starts_with(&fixed_name_part)
400                } else {
401                    false
402                }
403            })
404            .collect::<Vec<PathBuf>>();
405        log_files.sort_unstable();
406        log_files.reverse();
407        log_files
408    }
409
410    pub(crate) fn filter_files(
411        &self,
412        files: &[PathBuf],
413        infix_filter: &InfixFilter,
414        o_suffix: Option<&str>,
415    ) -> Vec<PathBuf> {
416        let fixed_name_part = self.fixed_name_part();
417        files
418            .iter()
419            .filter(|path| {
420                // if suffix is specified, it must match
421                if let Some(suffix) = o_suffix {
422                    path.extension().is_some_and(|ext| {
423                        let s = ext.to_string_lossy();
424                        s == suffix
425                    })
426                } else {
427                    true
428                }
429            })
430            .filter(|path| {
431                // infix filter must pass
432                let stem = path.file_stem().unwrap(/* CANNOT FAIL*/).to_string_lossy();
433                let infix_start = if fixed_name_part.is_empty() {
434                    0
435                } else {
436                    fixed_name_part.len() + 1 // underscore at the end
437                };
438                if stem.len() <= infix_start {
439                    return false;
440                }
441                let maybe_infix = &stem[infix_start..];
442                let end = maybe_infix.find('.').unwrap_or(maybe_infix.len());
443                infix_filter.filter_infix(&maybe_infix[..end])
444            })
445            .map(PathBuf::clone)
446            .collect::<Vec<PathBuf>>()
447    }
448
449    #[cfg(test)]
450    pub(crate) fn get_timestamp(&self) -> Option<String> {
451        self.timestamp_cfg.get_timestamp()
452    }
453}
454
455fn append_underscore_if_not_empty(filename: &mut String) {
456    if !filename.is_empty() {
457        filename.push('_');
458    }
459}
460
461const TS_USCORE_DASHES_USCORE_DASHES: &str = "%Y-%m-%d_%H-%M-%S";
462
463#[derive(Debug, Clone, Eq, PartialEq)]
464enum TimestampCfg {
465    Default,
466    Yes,
467    No,
468}
469impl TimestampCfg {
470    fn get_timestamp(&self) -> Option<String> {
471        match self {
472            Self::Default | Self::Yes => Some(
473                DeferredNow::new()
474                    .format(TS_USCORE_DASHES_USCORE_DASHES)
475                    .to_string(),
476            ),
477            Self::No => None,
478        }
479    }
480}
481
482#[cfg(test)]
483mod test {
484    use super::{FileSpec, TimestampCfg};
485    use crate::writers::file_log_writer::InfixFilter;
486    use std::{
487        fs::File,
488        path::{Path, PathBuf},
489    };
490
491    #[test]
492    fn test_timstamp_cfg() {
493        let ts = TimestampCfg::Yes;
494        let s = ts.get_timestamp().unwrap(/* OK */);
495        let bytes = s.into_bytes();
496        assert_eq!(bytes[4], b'-');
497        assert_eq!(bytes[7], b'-');
498        assert_eq!(bytes[10], b'_');
499        assert_eq!(bytes[13], b'-');
500        assert_eq!(bytes[16], b'-');
501    }
502
503    #[test]
504    fn test_default() {
505        let path = FileSpec::default().as_pathbuf(None);
506        assert_file_spec(&path, &PathBuf::from("."), true, "log");
507    }
508
509    #[test]
510    fn issue_194() {
511        assert!(dbg!(FileSpec::try_from("")).is_err());
512        assert!(dbg!(FileSpec::try_from(".")).is_err());
513        assert!(dbg!(FileSpec::try_from("..")).is_err());
514        // assert!(dbg!(FileSpec::try_from("/Users")).is_err());
515        // assert!(dbg!(FileSpec::try_from("/Users/")).is_err());
516        assert!(dbg!(FileSpec::try_from("./f/")).is_err());
517        assert!(dbg!(FileSpec::try_from("./f/.")).is_err());
518        assert!(dbg!(FileSpec::try_from("./f/..")).is_err());
519        assert!(dbg!(FileSpec::try_from(".log")).is_err());
520        assert!(dbg!(FileSpec::try_from("./.log")).is_err());
521        assert!(dbg!(FileSpec::try_from("./f/.log")).is_err());
522
523        let filespec = FileSpec::try_from("test.log").unwrap();
524        std::fs::create_dir_all(filespec.get_directory()).unwrap();
525        assert!(std::fs::metadata(filespec.get_directory())
526            .unwrap()
527            .is_dir());
528    }
529
530    // todo: does not support suppress_timestamp & suppress_basename & use discriminant
531    fn assert_file_spec(path: &Path, folder: &Path, with_timestamp: bool, suffix: &str) {
532        // check folder
533        assert_eq!(
534            path.parent().unwrap(), // .canonicalize().unwrap()
535            folder                  // .canonicalize().unwrap()
536        );
537        // check file stem
538        //  - should start with progname
539        let progname = PathBuf::from(std::env::args().next().unwrap())
540            .file_stem()
541            .unwrap()
542            .to_string_lossy()
543            .clone()
544            .to_string();
545        let stem = path
546            .file_stem()
547            .unwrap()
548            .to_string_lossy()
549            .clone()
550            .to_string();
551        assert!(
552            stem.starts_with(&progname),
553            "stem: {stem:?}, progname: {progname:?}",
554        );
555        if with_timestamp {
556            // followed by _ and timestamp
557            assert_eq!(stem.as_bytes()[progname.len()], b'_');
558            let s_ts = &stem[progname.len() + 1..];
559            assert!(
560                chrono::NaiveDateTime::parse_from_str(s_ts, "%Y-%m-%d_%H-%M-%S").is_ok(),
561                "s_ts: \"{s_ts}\"",
562            );
563        } else {
564            assert_eq!(
565                stem.len(),
566                progname.len(),
567                "stem: {stem:?}, progname: {progname:?}",
568            );
569        }
570
571        // check suffix
572        assert_eq!(path.extension().unwrap(), suffix);
573    }
574
575    #[test]
576    fn test_if_default_use_timestamp() {
577        // default() + if_default_use_timestamp(false) => false
578        {
579            let mut fs = FileSpec::default();
580            fs.if_default_use_timestamp(false);
581            let path = fs.as_pathbuf(None);
582            assert_file_spec(&path, &PathBuf::from("."), false, "log");
583        }
584        // default() + use_timestamp(true) + if_default_use_timestamp(false) => true
585        {
586            let mut fs = FileSpec::default().use_timestamp(true);
587            fs.if_default_use_timestamp(false);
588            let path = fs.as_pathbuf(None);
589            assert_file_spec(&path, &PathBuf::from("."), true, "log");
590        }
591        // default() + use_timestamp(false) + if_default_use_timestamp(true) +  => true
592        {
593            let mut fs = FileSpec::default();
594            fs.if_default_use_timestamp(false);
595            let path = fs.use_timestamp(true).as_pathbuf(None);
596            assert_file_spec(&path, &PathBuf::from("."), true, "log");
597        }
598        // default() + if_default_use_timestamp(false) + use_timestamp(true) => true
599        {
600            let mut fs = FileSpec::default();
601            fs.if_default_use_timestamp(false);
602            let path = fs.use_timestamp(true).as_pathbuf(None);
603            assert_file_spec(&path, &PathBuf::from("."), true, "log");
604        }
605    }
606
607    #[test]
608    fn test_from_url() {
609        let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
610            .unwrap()
611            .as_pathbuf(None);
612        // check folder
613        assert_eq!(path.parent().unwrap(), PathBuf::from("/a/b/c"));
614        // check filestem
615        //  - should start with progname
616        let stem = path
617            .file_stem()
618            .unwrap()
619            .to_string_lossy()
620            .clone()
621            .to_string();
622        assert_eq!(stem, "d_foo_bar");
623
624        // check suffix
625        assert_eq!(path.extension().unwrap(), "trc");
626    }
627
628    #[test]
629    fn test_basename() {
630        {
631            let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
632                .unwrap()
633                .o_basename(Some("boo_far"))
634                .as_pathbuf(None);
635            // check folder
636            assert_eq!(path.parent().unwrap(), PathBuf::from("/a/b/c"));
637
638            // check filestem
639            //  - should start with progname
640            let stem = path
641                .file_stem()
642                .unwrap()
643                .to_string_lossy()
644                .clone()
645                .to_string();
646            assert_eq!(stem, "boo_far");
647
648            // check suffix
649            assert_eq!(path.extension().unwrap(), "trc");
650        }
651        {
652            let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
653                .unwrap()
654                .o_basename(Option::<String>::None)
655                .as_pathbuf(None);
656            assert_file_spec(&path, &PathBuf::from("/a/b/c"), false, "trc");
657        }
658    }
659
660    #[test]
661    fn test_directory_and_suffix() {
662        {
663            let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
664                .unwrap()
665                .directory("/x/y/z")
666                .o_suffix(Some("txt"))
667                .o_basename(Option::<String>::None)
668                .as_pathbuf(None);
669            assert_file_spec(&path, &PathBuf::from("/x/y/z"), false, "txt");
670        }
671    }
672
673    #[test]
674    fn test_discriminant() {
675        let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
676            .unwrap()
677            .directory("/x/y/z")
678            .o_suffix(Some("txt"))
679            .o_discriminant(Some("1234"))
680            .as_pathbuf(None);
681        assert_eq!(
682            path.file_name().unwrap().to_str().unwrap(),
683            "d_foo_bar_1234.txt"
684        );
685    }
686
687    #[test]
688    fn test_suppress_basename() {
689        let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
690            .unwrap()
691            .suppress_basename()
692            .o_suffix(Some("txt"))
693            .o_discriminant(Some("1234"))
694            .as_pathbuf(None);
695        assert_eq!(path.file_name().unwrap().to_str().unwrap(), "1234.txt");
696    }
697
698    #[test]
699    fn test_empty_base_name() {
700        let path = FileSpec::default()
701            .suppress_basename()
702            .suppress_timestamp()
703            .o_discriminant(Option::<String>::None)
704            .as_pathbuf(None);
705        assert_eq!(path.file_name().unwrap(), ".log");
706    }
707
708    #[test]
709    fn test_empty_name() {
710        let path = FileSpec::default()
711            .suppress_basename()
712            .suppress_timestamp()
713            .o_suffix(Option::<String>::None)
714            .as_pathbuf(None);
715        assert!(path.file_name().is_none());
716    }
717
718    #[test]
719    fn issue_178() {
720        let path = FileSpec::default()
721            .basename("BASENAME")
722            .suppress_timestamp()
723            .as_pathbuf(Some(""));
724        assert_eq!(path.file_name().unwrap().to_string_lossy(), "BASENAME.log");
725
726        let path = FileSpec::default()
727            .basename("BASENAME")
728            .discriminant("1")
729            .suppress_timestamp()
730            .as_pathbuf(Some(""));
731        assert_eq!(
732            path.file_name().unwrap().to_string_lossy(),
733            "BASENAME_1.log"
734        );
735    }
736
737    #[test]
738    fn test_list_of_files() {
739        let dir = temp_dir::TempDir::new().unwrap();
740        let pd = dir.path();
741        let filespec: FileSpec = FileSpec::default()
742            .directory(pd)
743            .basename("Base")
744            .discriminant("Discr")
745            .use_timestamp(true);
746        println!("Filespec: {}", filespec.as_pathbuf(Some("Infix")).display());
747
748        let mut fn1 = String::new();
749        fn1.push_str("Base_Discr_");
750        fn1.push_str(&filespec.get_timestamp().unwrap());
751        fn1.push_str("_Infix");
752        fn1.push_str(".log");
753        assert_eq!(
754            filespec
755                .as_pathbuf(Some("Infix"))
756                .file_name()
757                .unwrap()
758                .to_string_lossy(),
759            fn1
760        );
761        // create typical set of files, and noise
762        create_file(pd, "test1.txt");
763        create_file(pd, &build_filename(&filespec, "Infix1"));
764        create_file(pd, &build_filename(&filespec, "Infix2"));
765
766        println!("\nFolder content:");
767        for entry in std::fs::read_dir(pd).unwrap() {
768            println!("  {}", entry.unwrap().path().display());
769        }
770
771        println!("\nRelevant subset:");
772        for pb in filespec.list_of_files(&InfixFilter::StartsWth("Infix".to_string()), Some("log"))
773        {
774            println!("  {}", pb.display());
775        }
776    }
777
778    fn build_filename(file_spec: &FileSpec, infix: &str) -> String {
779        let mut fn1 = String::new();
780        fn1.push_str("Base_Discr_");
781        fn1.push_str(&file_spec.get_timestamp().unwrap());
782        fn1.push('_');
783        fn1.push_str(infix);
784        fn1.push_str(".log");
785        fn1
786    }
787
788    fn create_file(dir: &Path, filename: &str) {
789        File::create(dir.join(filename)).unwrap();
790    }
791}