Skip to main content

optionstratlib_visualization/visualization/
file.rs

1use std::io::Error as IoError;
2use std::io::ErrorKind;
3use std::path::Path;
4use tracing::{debug, error, trace};
5
6/// Prepares a path for writing by removing any existing file and creating necessary directories.
7///
8/// This function:
9/// 1. Removes the file if it already exists, and accepts a file that is
10///    already gone: the guarantee is that nothing sits at `path` afterwards
11/// 2. Creates all parent directories if they don't exist
12///
13/// # Arguments
14///
15/// * `path` - The path where the file will be written
16///
17/// # Returns
18///
19/// * `Result<(), std::io::Error>` - Ok if successful, or an IoError if it failed
20///
21/// # Errors
22///
23/// Returns [`std::io::Error`] when the target file exists but cannot
24/// be removed, or when the parent directory cannot be created
25/// (typically `PermissionDenied`, `NotFound` on a broken symlink
26/// ancestor, or the platform-specific disk-full case).
27pub fn prepare_file_path(path: &Path) -> Result<(), IoError> {
28    // What this has to guarantee is that nothing sits at `path`, so a file
29    // that is already gone satisfies it. Testing `exists()` first and removing
30    // afterwards is a race — anything else deleting the file in between makes
31    // the removal fail with `NotFound` for a postcondition that already holds
32    // — so the removal is attempted unconditionally and that one kind is
33    // accepted. Two tests in one working directory are enough to lose the
34    // race: it aborted a coverage run on `main` with `Failed to remove
35    // existing file: multiple_curves_test.html`, kind `NotFound`.
36    match std::fs::remove_file(path) {
37        Ok(()) => trace!("Removed existing file: {}", path.display()),
38        Err(e) if e.kind() == ErrorKind::NotFound => {
39            trace!("Nothing to remove at: {}", path.display());
40        }
41        Err(e) => {
42            error!("Failed to remove existing file: {}", path.display());
43            return Err(IoError::new(
44                e.kind(),
45                format!("Failed to remove existing file: {}", path.display()),
46            ));
47        }
48    }
49
50    // Create parent directories if they don't exist
51    if let Some(parent) = path.parent()
52        && !parent.exists()
53    {
54        match std::fs::create_dir_all(parent) {
55            Ok(_) => {}
56            Err(e) => {
57                error!("Failed to create parent directories: {}", path.display());
58                return Err(IoError::new(
59                    e.kind(),
60                    format!("Failed to create parent directories: {}", path.display()),
61                ));
62            }
63        };
64        debug!("Created directory: {}", path.display());
65    }
66
67    Ok(())
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use std::fs;
74
75    /// The postcondition is "nothing sits at `path`", so a path that is
76    /// already clear is a success, not a failure. Calling twice in a row is
77    /// the sequential form of the race that aborted a coverage run: two tests
78    /// sharing a working directory, one removing the file between the other's
79    /// existence check and its removal.
80    #[test]
81    fn test_prepare_file_path_accepts_a_path_with_no_file() {
82        let dir = std::env::temp_dir().join("optionstratlib_prepare_file_path");
83        fs::create_dir_all(&dir).expect("the temp directory is writable");
84        let path = dir.join("absent.html");
85        let _ = fs::remove_file(&path);
86
87        prepare_file_path(&path).expect("an absent file is already prepared");
88
89        fs::write(&path, b"contents").expect("the temp directory is writable");
90        prepare_file_path(&path).expect("an existing file is removed");
91        assert!(!path.exists());
92
93        prepare_file_path(&path).expect("preparing twice is not an error");
94        assert!(!path.exists());
95    }
96
97    /// The parent directories are still created for a path that does not have
98    /// them yet, which is the other half of what this does.
99    #[test]
100    fn test_prepare_file_path_creates_missing_parents() {
101        let dir = std::env::temp_dir().join("optionstratlib_prepare_file_path/nested/deeper");
102        let _ = fs::remove_dir_all(
103            std::env::temp_dir().join("optionstratlib_prepare_file_path/nested"),
104        );
105        let path = dir.join("target.html");
106
107        prepare_file_path(&path).expect("the parents are created");
108
109        assert!(dir.exists());
110    }
111}