xvc_test_helper/
lib.rs

1//! Helper functions to create random temporary directories, random binary or text files, and set logging in unit/integration
2//! tests.
3//! The directories may be initialized as Git or Xvc repositories.
4#![warn(missing_docs)]
5#![forbid(unsafe_code)]
6use log::LevelFilter;
7use rand::distributions::Alphanumeric;
8use rand::rngs::StdRng;
9use rand::Rng;
10use rand::RngCore;
11use rand::SeedableRng;
12use std::cmp;
13use std::env;
14use std::fs::OpenOptions;
15
16use std::{
17    fs::{self, File},
18    process::Command,
19};
20use std::{
21    io::{self, BufWriter, Write},
22    path::{Path, PathBuf},
23};
24
25use anyhow::Result;
26use xvc_logging::{setup_logging, watch};
27
28#[cfg(unix)]
29use std::os::unix::fs as unix_fs;
30#[cfg(windows)]
31use std::os::windows::fs as windows_fs;
32
33/// Turn on logging for testing purposes.
34/// Testing always send traces to `$TMPDIR/xvc.log`.
35/// The `level` here determines whether these are sent to `stdout`.
36pub fn test_logging(level: LevelFilter) {
37    setup_logging(Some(level), Some(level));
38}
39
40fn interleave_with_dash(s: &str, n: usize) -> String {
41    let chars: Vec<char> = s.chars().collect();
42    let chunks: Vec<String> = chars
43        .chunks(n)
44        .map(|chunk| chunk.iter().collect())
45        .collect();
46    chunks.join("-")
47}
48
49/// Generates a random name with `prefix` and a random number generated from `seed`.
50/// If `seed` is `None`, a random number `from_entropy` is used.
51pub fn random_dir_name(prefix: &str, seed: Option<u64>) -> String {
52    let mut rng = if let Some(seed) = seed {
53        rand::rngs::StdRng::seed_from_u64(seed)
54    } else {
55        rand::rngs::StdRng::from_entropy()
56    };
57
58    let rand: u32 = rng.next_u32();
59    format!("{}-{}", prefix, interleave_with_dash(&rand.to_string(), 3))
60}
61
62/// Return name of a random directory under $TMPDIR.
63/// It doesn't create the directory, just returns the path.
64pub fn random_temp_dir(prefix: Option<&str>) -> PathBuf {
65    let mut temp_dir = env::temp_dir();
66    loop {
67        let cand = temp_dir.join(Path::new(&random_dir_name(
68            prefix.unwrap_or("xvc-repo"),
69            None,
70        )));
71        if !cand.exists() {
72            temp_dir = cand;
73            break;
74        }
75    }
76
77    temp_dir
78}
79
80/// Return a temp directory created with a seed.
81/// If `seed` is `None`, it creates a random directory name.
82/// This function doesn't create the directory.
83pub fn seeded_temp_dir(prefix: &str, seed: Option<u64>) -> PathBuf {
84    let temp_dir = env::temp_dir();
85    temp_dir.join(Path::new(&random_dir_name(prefix, seed)))
86}
87
88/// Create a random named temp directory under $TMPDIR
89pub fn create_temp_dir() -> PathBuf {
90    let temp_dir = random_temp_dir(None);
91
92    fs::create_dir_all(&temp_dir).expect("Cannot create directory.");
93    temp_dir
94}
95
96/// Create a temporary dir under $TMPDIR and cd to it
97pub fn run_in_temp_dir() -> PathBuf {
98    let temp_dir = create_temp_dir();
99    watch!(temp_dir);
100    env::set_current_dir(&temp_dir).expect("Cannot change directory");
101    temp_dir
102}
103
104/// Create an empty temporary Git repository without XVC_DIR
105pub fn run_in_temp_git_dir() -> PathBuf {
106    let temp_dir = run_in_temp_dir();
107    let output = Command::new("git")
108        .arg("init")
109        .output()
110        .unwrap_or_else(|e| panic!("failed to execute process: {}", e));
111    watch!(output);
112    temp_dir
113}
114
115/// Create a random directory and run `git init` in it.
116pub fn temp_git_dir() -> PathBuf {
117    let temp_dir = create_temp_dir();
118    watch!(temp_dir);
119    Command::new("git")
120        .arg("-C")
121        .arg(temp_dir.as_os_str())
122        .arg("init")
123        .output()
124        .unwrap_or_else(|e| panic!("failed to execute process: {}", e));
125    temp_dir
126}
127
128/// Generate a random binary file
129pub fn generate_random_file(filename: &Path, size: usize, seed: Option<u64>) {
130    let f = OpenOptions::new()
131        .create(true)
132        .truncate(true)
133        .write(true)
134        .open(filename)
135        .unwrap();
136    let mut writer = BufWriter::new(f);
137
138    let mut rng: StdRng = seed
139        .map(StdRng::seed_from_u64)
140        .unwrap_or_else(StdRng::from_entropy);
141    let mut buffer = [0u8; 1024];
142    let mut remaining_size = size;
143
144    while remaining_size > 0 {
145        let to_write = cmp::min(remaining_size, buffer.len());
146        let buffer = &mut buffer[0..to_write];
147        rng.fill(buffer);
148        writer.write_all(buffer).unwrap();
149
150        remaining_size -= to_write;
151    }
152}
153
154/// Creates a file filled with byte
155pub fn generate_filled_file(filename: &Path, size: usize, byte: u8) {
156    let f = File::create(filename).unwrap();
157    let mut writer = BufWriter::new(f);
158    let buffer = [byte; 1024];
159    let mut remaining_size = size;
160    while remaining_size > 0 {
161        let to_write = cmp::min(remaining_size, buffer.len());
162        let buffer = &buffer[0..to_write];
163        writer.write_all(buffer).unwrap();
164        remaining_size -= to_write;
165    }
166}
167
168/// Generate a random text file composed of alphanumerics
169pub fn generate_random_text_file(filename: &Path, num_lines: usize) {
170    let mut f = File::create(filename).unwrap();
171    let rng = rand::thread_rng();
172    let line_length = 100;
173    for _ in 0..num_lines {
174        let line: String = rng
175            .clone()
176            .sample_iter(&Alphanumeric)
177            .take(line_length)
178            .map(char::from)
179            .collect();
180        writeln!(f, "{}\n", line).expect("Could not write to file.");
181    }
182}
183
184/// Build a directory tree containing `n_dirs` under `root`.
185/// Each of these directories contain `n_files_per_dir` random binary files.
186pub fn create_directory_tree(
187    root: &Path,
188    n_dirs: usize,
189    n_files_per_dir: usize,
190    min_size: usize,
191    seed: Option<u64>,
192) -> Result<Vec<PathBuf>> {
193    let mut paths = Vec::<PathBuf>::with_capacity(n_dirs * n_files_per_dir);
194    let dirs: Vec<String> = (1..=n_dirs).map(|i| format!("dir-{:04}", i)).collect();
195    let files: Vec<(String, usize)> = (1..=n_files_per_dir)
196        .map(|i| (format!("file-{:04}.bin", i), min_size + i + 1000))
197        .collect();
198    for dir in dirs {
199        std::fs::create_dir_all(root.join(Path::new(&dir)))?;
200        paths.extend(files.iter().map(|(name, size)| {
201            let filename = PathBuf::from(&format!("{}/{}/{}", root.to_string_lossy(), dir, name));
202            generate_random_file(&filename, *size, seed);
203            filename
204        }));
205    }
206    Ok(paths)
207}
208
209#[cfg(unix)]
210/// Creates a symlink from target to original
211pub fn make_symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
212    unix_fs::symlink(original, link)
213}
214
215#[cfg(windows)]
216/// Creates a file symlink from target to original
217pub fn make_symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
218    windows_fs::symlink_file(original, link)
219}