summaryrefslogtreecommitdiff
path: root/rola-utils/functions/src/test_sandbox.rs
blob: 2705389268da406e9fd8e9eebec968c3a9d72db9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
use std::fs;
use std::ops::Deref;
use std::path::{Path, PathBuf};

/// Takes a name, returns a `Sandbox` pointing to a created empty directory
///
/// # Example
///
/// ```rust
/// # use shared_functions::rola_test_sandbox;
/// let sandbox = rola_test_sandbox("my_test");
/// let file_path = sandbox.join("output.txt");
/// std::fs::write(&file_path, "hello").unwrap();
/// assert!(file_path.exists());
/// // The directory is automatically removed when `sandbox` goes out of scope.
/// ```
#[must_use]
pub fn rola_test_sandbox(name: &str) -> Sandbox {
    let root = find_workspace_root();
    let path = find_available_path(&root.join(".temp").join("test_sandbox").join(name));
    ensure_empty(&path);
    Sandbox { path }
}

/// Searches upward from the current directory until a directory with `.cargo` is found
fn find_workspace_root() -> PathBuf {
    let mut dir = std::env::current_dir().expect("failed to get current directory");
    loop {
        if dir.join(".cargo").is_dir() {
            return dir;
        }
        if !dir.pop() {
            panic!(
                "could not find workspace root: no `.cargo` directory found in any parent of `{}`",
                std::env::current_dir().unwrap().display()
            );
        }
    }
}

/// Finds an available path
fn find_available_path(base: &Path) -> PathBuf {
    if !base.exists() {
        return base.to_path_buf();
    }

    if try_clear_contents(base).is_ok() {
        return base.to_path_buf();
    }

    let parent = base.parent().unwrap();
    let stem = base
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("sandbox");

    for i in 1.. {
        let candidate = parent.join(format!("{}_{}", stem, i));
        if !candidate.exists() {
            return candidate;
        }
        if try_clear_contents(&candidate).is_ok() {
            return candidate;
        }
    }

    unreachable!()
}

/// Clears directory contents while keeping the directory itself; returns an error on failure.
fn try_clear_contents(path: &Path) -> std::io::Result<()> {
    if path.is_dir() {
        for entry in fs::read_dir(path)? {
            let entry = entry?;
            if entry.file_type()?.is_dir() {
                fs::remove_dir_all(entry.path())?;
            } else {
                fs::remove_file(entry.path())?;
            }
        }
    }
    Ok(())
}

fn ensure_empty(path: &Path) {
    if path.exists() {
        let _ = fs::remove_dir_all(path);
    }
    fs::create_dir_all(path).expect("failed to create sandbox directory");
}

/// A sandbox directory that is automatically cleaned up when dropped.
///
/// The sandbox directory is created at a path under the workspace's `.temp/test_sandbox/` directory.
/// When the `Sandbox` is dropped, the directory and all its contents are removed.
pub struct Sandbox {
    /// The path to the sandbox directory.
    pub path: PathBuf,
}

impl Drop for Sandbox {
    fn drop(&mut self) {
        if self.path.exists() {
            let _ = fs::remove_dir_all(&self.path);
        }
    }
}

impl Deref for Sandbox {
    type Target = PathBuf;

    fn deref(&self) -> &Self::Target {
        &self.path
    }
}