testfile/
lib.rs

1// Copyright 2023 Radim Kolar <hsn@sendmail.cz>
2// SPDX-License-Identifier: MIT
3
4//! Temporary 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#![warn(rustdoc::unescaped_backticks)]
18
19/**
20 * File content initializer closure.
21 * Initializes TestFile with content.
22*/
23
24/*
25Trait alias is currently unstable Rust feature:
26https://github.com/rust-lang/rust/issues/41517
27https://doc.rust-lang.org/beta/unstable-book/language-features/trait-alias.html
28
29#![feature(trait_alias)]
30pub trait FileInitializer = Fn (&mut TestFile) -> std::io::Result<()>;
31*/
32
33use std::mem::ManuallyDrop;
34
35/**
36 * Main TestFile structure.
37 *
38 * Contains two private fields: opened `File` and `PathBuf` with file name
39 * used for deleting file on `Drop`.
40 *
41 * Files are created in temporary directory with unique names.
42 * Reading and writing access is supported.
43 * This structure can be dereferenced as ordinary `&File` and implements
44 * `AsRef` / `AsMut` for `Path`, `PathBuf`, `File`, `Read`, `Write`.
45*/
46pub struct TestFile {
47      file : ManuallyDrop<std::fs::File>,
48      path : std::path::PathBuf
49}
50
51/**
52 * Removes TestFile from disk on `Drop`.
53 *
54 * If removal fails it is not retried and no error message is printed.
55*/
56impl Drop for TestFile {
57    fn drop(&mut self) {
58        unsafe { ManuallyDrop::drop(&mut self.file) };
59        let _ = std::fs::remove_file(&self.path);
60    }
61}
62
63/**
64How many times is retried attempt to
65generate valid unique random test file name
66before panicking.
67*/
68const RETRIES: u32 = 10;
69
70//  implementing different kinds of dereferencing constructors
71
72use std::convert::{AsRef,AsMut};
73
74impl AsRef<std::path::Path> for TestFile {
75   fn as_ref(&self) -> &std::path::Path {
76       &self.path
77   }
78}
79
80impl AsRef<std::path::PathBuf> for TestFile {
81    fn as_ref(&self) -> &std::path::PathBuf {
82        &self.path
83    }
84}
85
86impl AsRef<std::fs::File> for TestFile {
87   fn as_ref(&self) -> &std::fs::File {
88       &self.file
89   }
90}
91
92impl AsMut<std::fs::File> for TestFile {
93   fn as_mut(&mut self) -> &mut std::fs::File {
94       &mut self.file
95   }
96}
97
98impl AsMut<dyn std::io::Read> for TestFile {
99   fn as_mut(&mut self) -> &mut (dyn std::io::Read + 'static) {
100        self.file.deref_mut()
101   }
102}
103
104impl AsMut<dyn std::io::Write> for TestFile {
105   fn as_mut(&mut self) -> &mut (dyn std::io::Write + 'static) {
106        self.file.deref_mut()
107   }
108}
109
110impl AsRef<TestFile> for TestFile {
111   fn as_ref(&self) -> &TestFile {
112        self
113   }
114}
115
116impl AsMut<TestFile> for TestFile {
117   fn as_mut(&mut self) -> &mut TestFile {
118        self
119   }
120}
121
122use std::ops::{Deref,DerefMut};
123
124impl Deref for TestFile {
125   type Target = std::fs::File;
126   fn deref(&self) -> &Self::Target { &self.file }
127}
128
129impl DerefMut for TestFile {
130   fn deref_mut(&mut self) -> &mut Self::Target { &mut self.file }
131}
132
133//  implementing different kinds of value conversions
134
135/**
136 * Converts String to TestFile
137*/
138impl From<String> for TestFile {
139    fn from(content: String) -> Self {
140        self::from(content)
141    }
142}
143
144// we can not have From <AsRef<str>>
145
146/**
147 * Converts &String to TestFile
148*/
149impl From<&String> for TestFile {
150    fn from(content: &String) -> Self {
151        self::from(content)
152    }
153}
154
155/**
156 * Converts PathBuf to TestFile
157*/
158impl From <std::path::PathBuf> for TestFile {
159    fn from(buf: std::path::PathBuf) -> Self {
160        from_file(buf)
161    }
162}
163
164/**
165 * Converts &PathBuf to TestFile
166*/
167impl From <&std::path::PathBuf> for TestFile {
168    fn from(buf: &std::path::PathBuf) -> Self {
169        from_file(buf)
170    }
171}
172
173
174/**
175 * Create an empty test file.
176*/
177pub fn empty() -> TestFile {
178    create(|_unused| Ok(()))
179}
180
181/**
182 * Create a TestFile from string.
183 *
184 * String is file content, not file name.
185*/
186pub fn from<T: AsRef<str>>(content: T) -> TestFile {
187    use std::io::Write;
188    create(|tfile| tfile.file.write_all(content.as_ref().as_bytes()))
189}
190
191/**
192 * Turns an existing file into TestFile.
193 *
194 * File must exist and be readable and writable.
195*/
196pub fn from_file(path: impl AsRef<std::path::Path>) -> TestFile {
197   std::fs::OpenOptions::new().read(true).write(true).create_new(false).open(path.as_ref()).map(
198      |f| TestFile{ file:ManuallyDrop::new(f), path:std::path::PathBuf::new() }).map(
199      |mut tf| { tf.path.push(path.as_ref());tf})
200      .unwrap()
201}
202
203/**
204 * Create test file with content supplied by initializer closure.
205 *
206 * If closure returns error `Result`, operation is retried several times
207 * before panicking.
208 */
209pub fn create<I>(initializer: I) -> TestFile
210       where I: Fn (&mut TestFile) -> std::io::Result<()> {
211
212   let mut retries = RETRIES;
213   let mut rng = rand::thread_rng();
214   use std::fs::OpenOptions;
215   use std::io::{Seek, SeekFrom};
216
217   while retries > 0 {
218      retries -= 1;
219      let mut tmp = std::env::temp_dir();
220      tmp.push(format!("TMP{}",generate_random(&mut rng)));
221      match OpenOptions::new().read(true).write(true).create_new(true).open(&tmp).map(
222      |f| TestFile{ file:ManuallyDrop::new(f), path:tmp }
223      ).and_then(|mut tfile:TestFile| -> std::io::Result<TestFile> { match initializer(<TestFile as AsMut<TestFile>>::as_mut(&mut tfile)) { Ok(_) => Ok(tfile), Err(ioerr) => Err(ioerr) }}
224      ).and_then(|mut tfile| match tfile.file.seek(SeekFrom::Start(0)) { Ok(_) => Ok(tfile), Err(ioerr) => Err(ioerr) }
225      ).and_then(|mut tfile| match <dyn std::io::Write>::flush(<TestFile as AsMut<std::fs::File>>::as_mut(&mut tfile)) { Ok(_) => Ok(tfile), Err(ioerr) => Err(ioerr) }
226      ) {
227           Ok(tfile) => return tfile,
228           Err(_) => continue
229      }
230   }
231   panic!("Can't create temporary file")
232}
233
234use rand::prelude::*;
235
236/**
237 * Generate random number in range 1000 to 9999 inclusive using
238 * provided thread bound random generator.
239 */
240fn generate_random(rng : &mut ThreadRng) -> u32 {
241   rng.gen_range(1000..10000)
242}
243
244/**
245 * Generate random unique tmp file name.
246 *
247 * Returned file does not exist.
248 */
249pub fn generate_name() -> std::path::PathBuf {
250   let mut retries = RETRIES;
251   let mut rng = rand::thread_rng();
252   use std::fs::OpenOptions;
253
254   while retries > 0 {
255      retries -= 1;
256      let mut tmp = std::env::temp_dir();
257      tmp.push(format!("TMP{}", generate_random(&mut rng)));
258      match OpenOptions::new().read(true).write(true).create_new(true).open(&tmp).map(
259      |f| { drop(f); let _ = std::fs::remove_file(&tmp); })
260      {
261         Ok(()) => return tmp,
262         Err(_) => continue
263      }
264   }
265   panic!("Can't generate suitable tmp file name");
266}
267
268#[cfg(test)]
269#[path= "./lib_tests.rs"]
270mod tests;