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}