testfile/
lib.rs

1// Copyright 2023 Radim Kolar <hsn@sendmail.cz>
2// SPDX-License-Identifier: MIT
3
4//! Temporary dir/file management for unit tests.
5//!
6//! Create and manage temporary files used for unit tests.
7//! Managed temporary files are created in system temporary directory
8//! with unique file names. They can be dereferenced as ordinary `File`
9//! and are automatically deleted during `Drop`.
10//!
11//! Functions do not return
12//! `Result <T,E>` but `panic!` allowing you to focus on your tests and
13//! not on error handling.
14
15#![forbid(missing_docs)]
16#![forbid(rustdoc::missing_crate_level_docs)]
17#![forbid(non_fmt_panics)]
18#![warn(rustdoc::unescaped_backticks)]
19
20/**
21 * Generate random number in range 10000 to 99999 inclusive using
22 * stdrandom fast random generator.
23 */
24fn generate_random() -> u32 {
25    // Pick a fast random u64 generator
26    let random_generator = stdrandom::fast_u64;
27    stdrandom::gen_range(10000..=99999, random_generator)
28}
29
30/**
31  How many times is retried attempt to
32  generate valid unique random file name
33  before panicking.
34*/
35const FILE_RETRIES: u32 = 30;
36
37/**
38  How many times is retried generator closure
39  when creating a new file
40  before panicking.
41*/
42const GEN_RETRIES: u32 = 5;
43
44/** prefix for temporary files on UNIX */
45#[cfg(target_family = "unix")]
46fn filename_prefix() -> &'static str {
47    "tmp"
48}
49
50/** prefix for temporary files on Windows */
51#[cfg(not(target_family = "unix"))]
52fn filename_prefix() -> &'static str {
53    "TMP"
54}
55
56/** prefix for temporary directories on UNIX */
57#[cfg(target_family = "unix")]
58fn dirname_prefix() -> &'static str {
59    "tmpdir"
60}
61
62/** prefix for temporary directories on Windows */
63#[cfg(not(target_family = "unix"))]
64fn dirname_prefix() -> &'static str {
65    "TMPDIR"
66}
67
68/**
69 * Generate a random unique temporary file name.
70 *
71 * Returned file is located in system temporary
72 * directory and does not exist.
73 */
74pub fn generate_name() -> std::path::PathBuf {
75      generate_name_in(std::env::temp_dir())
76}
77
78/**
79 * Generate a random unique temporary directory name.
80 *
81 * Returned directory is located in system temporary
82 * directory and does not exist.
83 */
84pub fn generate_dirname() -> std::path::PathBuf {
85      generate_dirname_in(std::env::temp_dir())
86}
87
88/**
89 * Generate a random unique temporary file name within the given directory.
90 *
91 * Returned file does not exist.
92 */
93pub fn generate_name_in<P: AsRef<std::path::Path>>(dir: P) -> std::path::PathBuf {
94    let closure = || {
95       std::path::PathBuf::from(format!("{}{}", filename_prefix(), generate_random()))
96    };
97    filename_generator(dir, closure)
98}
99
100/**
101 * Generate a random unique temporary directory name within the given directory.
102 *
103 * Returned directory does not exist.
104 */
105pub fn generate_dirname_in<P: AsRef<std::path::Path>>(dir: P) -> std::path::PathBuf {
106    let closure = || {
107       std::path::PathBuf::from(format!("{}{}", dirname_prefix(), generate_random()))
108    };
109    filename_generator(dir, closure)
110}
111
112/**
113 * Unique temporary name generator.
114 *
115 * Generate unique name within specified directory. Possible file names are
116 * returned by template closure.
117 * Returned name does not exist.
118 */
119pub fn filename_generator<P1, F, P2>(dir: P1, mut template: F) -> std::path::PathBuf
120   where
121      P1: AsRef<std::path::Path>,
122      F: FnMut() -> P2,
123      P2: AsRef<std::path::Path>,
124{
125    use std::fs::OpenOptions;
126    let mut retries = FILE_RETRIES;
127    let base_dir = dir.as_ref();
128
129    // Ensure the path exists and is a directory
130    if !base_dir.exists() || !base_dir.is_dir() {
131        panic!("Can't generate temporary name in non existing directory");
132    }
133
134    // Ensure path is writable
135    if std::fs::metadata(base_dir).map(|m| m.permissions().readonly()).unwrap_or(true) {
136        panic!("Directory is not writable.");
137    }
138
139    while retries > 0 {
140        retries -= 1;
141        let mut tmp = base_dir.to_path_buf();
142        tmp.push(template().as_ref());
143
144        match OpenOptions::new().read(true).write(true).create_new(true).open(&tmp) {
145            Ok(f) => {
146                drop(f); // Close file before attempt to remove it
147                let _ = std::fs::remove_file(&tmp); // Cleanup
148                return tmp;
149            }
150            Err(_) => continue,
151        }
152    }
153
154    panic!("Failed to generate a suitable temporary name");
155}
156
157
158include!("lib_testfile.rs");
159include!("lib_testdir.rs");
160include!("lib_sharedtestdir.rs");
161
162#[cfg(test)]
163#[path= "./lib_tests.rs"]
164mod tests;
165
166/**
167   Closures for initializing TestFile content.
168
169   These closures are used for initializing content of TestFile
170   during call to [`create`], [`create_in`] and [`create_as`].
171*/
172#[path= "./lib_content.rs"]
173pub mod content;