file_io/
cd.rs

1use crate::path::get_cwd;
2use std::path::{Path, PathBuf};
3
4/// A struct that changes the current working directory to a specified path.
5///
6/// When an instance of this struct goes out of scope (i.e. it is dropped), it automatically
7/// restores the original current working directory.
8#[must_use]
9pub struct CdGuard {
10    /// Path to the original current working directory.
11    original_cwd: PathBuf,
12}
13
14impl CdGuard {
15    /// Constructor.
16    ///
17    /// # Arguments
18    ///
19    /// * `path` - The path to change the current working directory to (can be a `&str`, [`String`],
20    ///   [`Path`], or [`PathBuf`]).
21    ///
22    /// # Returns
23    ///
24    /// An instance of [`CdGuard`] that will restore the original directory when dropped.
25    pub fn new<P: AsRef<Path>>(path: P) -> Self {
26        let path = path.as_ref();
27        let original_cwd = get_cwd();
28        std::env::set_current_dir(path)
29            .unwrap_or_else(|_| panic!("Failed to change directory to '{path:?}'."));
30        Self { original_cwd }
31    }
32}
33
34// Restore the original directory when `cd` goes out of scope.
35impl Drop for CdGuard {
36    fn drop(&mut self) {
37        let original_cwd = self.original_cwd.clone();
38        std::env::set_current_dir(&original_cwd)
39            .unwrap_or_else(|_| panic!("Failed to change directory to '{original_cwd:?}'."))
40    }
41}
42
43/// Change the current working directory.
44///
45/// This function works by creating a [`CdGuard`] instance. When the [`CdGuard`] instance goes out
46/// of scope (i.e. when it is dropped), the original current working directory is automatically
47/// restored.
48///
49/// # Arguments
50///
51/// * `path` - The path to change the current working directory to (can be a `&str`, [`String`],
52///   [`Path`], or [`PathBuf`]).
53///
54/// # Returns
55///
56/// A [`CdGuard`] instance that will automatically restore the original current working directory
57/// when it goes out of scope (i.e. when it is dropped).
58///
59/// # Panics
60///
61/// If `path` does not exist or cannot be accessed.
62///
63/// # Example
64///
65/// ```
66/// use file_io::{cd, get_cwd};
67///
68/// // Get the path to the original current working directory.
69/// let original_cwd_path = get_cwd();
70///
71/// // Verify we are in the `file_io` directory.
72/// assert!(original_cwd_path.ends_with("file_io"));
73///
74/// // Define the directory to change to.
75/// let src_path = original_cwd_path.join("src");
76///
77/// // Enter a new scope.
78/// {
79///     // Change to the `src` directory within this limited scope.
80///     let _cd = cd(&src_path);
81///
82///     // Verify the current directory has changed.
83///     assert_eq!(get_cwd(), src_path);
84/// }
85///
86/// // Verify that outside the scope, we are back in the original directory.
87/// assert_eq!(get_cwd(), original_cwd_path);
88/// ```
89pub fn cd<P: AsRef<Path>>(path: P) -> CdGuard {
90    CdGuard::new(path)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::create::create_folder;
97    use crate::path::to_path_buf;
98    use crate::test_utils::get_temp_dir_path;
99    use serial_test::serial;
100    use tempfile::tempdir;
101
102    #[test]
103    #[serial]
104    fn test_cd_without_scope() {
105        // Get the path to the original current working directory.
106        let original_cwd_path = get_cwd();
107
108        // Verify we are in the `file_io` directory.
109        assert!(original_cwd_path.ends_with("file_io"));
110
111        // Define the directory to change to.
112        let src_path = original_cwd_path.join("src");
113
114        // src directory path in different formats.
115        let src_paths: Vec<Box<dyn AsRef<Path>>> = vec![
116            Box::new(src_path.to_str().unwrap()),             // &str
117            Box::new(src_path.to_str().unwrap().to_string()), // String
118            Box::new(src_path.as_path()),                     // Path
119            Box::new(src_path.clone()),                       // PathBuf
120        ];
121
122        // Test with all different path formats.
123        for src_path in src_paths {
124            // Get a reference to this path representation (i.e. "unbox").
125            let src_path = src_path.as_ref();
126
127            // Change to the `src` directory.
128            let _cd = cd(src_path);
129
130            // Verify the current directory has changed.
131            assert_eq!(get_cwd(), to_path_buf(src_path));
132
133            // Change back to the original directory.
134            let _cd = cd(&original_cwd_path);
135
136            // Verify that we are back in the original directory.
137            assert_eq!(get_cwd(), original_cwd_path);
138        }
139    }
140
141    #[test]
142    #[serial]
143    fn test_cd_with_scope() {
144        // Get the path to the original current working directory.
145        let original_cwd_path = get_cwd();
146
147        // Verify we are in the `file_io` directory.
148        assert!(original_cwd_path.ends_with("file_io"));
149
150        // Define the directory to change to.
151        let src_path = original_cwd_path.join("src");
152
153        // src directory path in different formats.
154        let src_paths: Vec<Box<dyn AsRef<Path>>> = vec![
155            Box::new(src_path.to_str().unwrap()),             // &str
156            Box::new(src_path.to_str().unwrap().to_string()), // String
157            Box::new(src_path.as_path()),                     // Path
158            Box::new(src_path.clone()),                       // PathBuf
159        ];
160
161        // Test with all different path formats.
162        for src_path in src_paths {
163            // Get a reference to this path representation (i.e. "unbox").
164            let src_path = src_path.as_ref();
165
166            // Enter a new scope.
167            {
168                // Change to the `src` directory within this limited scope.
169                let _cd = cd(src_path);
170
171                // Verify the current directory has changed.
172                assert_eq!(get_cwd(), to_path_buf(src_path));
173            }
174
175            // Verify that outside the scope, we are back in the original directory.
176            assert_eq!(get_cwd(), original_cwd_path);
177        }
178    }
179
180    #[test]
181    #[serial]
182    fn test_cd_with_panic() {
183        // Create a temporary directory to work in and get its path.
184        let temp_dir = tempdir().unwrap();
185        let temp_dir_path = get_temp_dir_path(&temp_dir);
186
187        // Get the path to the original current working directory.
188        let original_cwd_path = get_cwd();
189
190        // Create a folder within the temporary directory to move into.
191        let new_cwd_path = temp_dir_path.join("subfolder");
192        create_folder(&new_cwd_path);
193
194        // Catch the panic inside this scope.
195        let result = std::panic::catch_unwind(|| {
196            // Change to the new directory.
197            let _cd = cd(&temp_dir);
198
199            // Ensure we changed into the new directory.
200            assert_eq!(get_cwd(), new_cwd_path);
201
202            // Simulate failure.
203            panic!("Simulated failure.");
204        });
205
206        // Make sure a panic actually occurred.
207        assert!(result.is_err());
208
209        // Ensure we are back in the original directory.
210        assert_eq!(get_cwd(), original_cwd_path);
211    }
212}