zombienet_support/fs/
local.rs
1use std::{fs::Permissions, os::unix::fs::PermissionsExt, path::Path};
2
3use async_trait::async_trait;
4use tokio::io::AsyncWriteExt;
5
6use super::{FileSystem, FileSystemError, FileSystemResult};
7
8#[derive(Default, Debug, Clone)]
9pub struct LocalFileSystem;
10
11#[async_trait]
12impl FileSystem for LocalFileSystem {
13 async fn create_dir<P>(&self, path: P) -> FileSystemResult<()>
14 where
15 P: AsRef<Path> + Send,
16 {
17 tokio::fs::create_dir(path).await.map_err(Into::into)
18 }
19
20 async fn create_dir_all<P>(&self, path: P) -> FileSystemResult<()>
21 where
22 P: AsRef<Path> + Send,
23 {
24 tokio::fs::create_dir_all(path).await.map_err(Into::into)
25 }
26
27 async fn read<P>(&self, path: P) -> FileSystemResult<Vec<u8>>
28 where
29 P: AsRef<Path> + Send,
30 {
31 tokio::fs::read(path).await.map_err(Into::into)
32 }
33
34 async fn read_to_string<P>(&self, path: P) -> FileSystemResult<String>
35 where
36 P: AsRef<Path> + Send,
37 {
38 tokio::fs::read_to_string(path).await.map_err(Into::into)
39 }
40
41 async fn write<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
42 where
43 P: AsRef<Path> + Send,
44 C: AsRef<[u8]> + Send,
45 {
46 tokio::fs::write(path, contents).await.map_err(Into::into)
47 }
48
49 async fn append<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
50 where
51 P: AsRef<Path> + Send,
52 C: AsRef<[u8]> + Send,
53 {
54 let contents = contents.as_ref();
55 let mut file = tokio::fs::OpenOptions::new()
56 .create(true)
57 .append(true)
58 .open(path)
59 .await
60 .map_err(Into::<FileSystemError>::into)?;
61
62 file.write_all(contents)
63 .await
64 .map_err(Into::<FileSystemError>::into)?;
65
66 file.flush().await.and(Ok(())).map_err(Into::into)
67 }
68
69 async fn copy<P1, P2>(&self, from: P1, to: P2) -> FileSystemResult<()>
70 where
71 P1: AsRef<Path> + Send,
72 P2: AsRef<Path> + Send,
73 {
74 tokio::fs::copy(from, to)
75 .await
76 .and(Ok(()))
77 .map_err(Into::into)
78 }
79
80 async fn set_mode<P>(&self, path: P, mode: u32) -> FileSystemResult<()>
81 where
82 P: AsRef<Path> + Send,
83 {
84 tokio::fs::set_permissions(path, Permissions::from_mode(mode))
85 .await
86 .map_err(Into::into)
87 }
88
89 async fn exists<P>(&self, path: P) -> bool
90 where
91 P: AsRef<Path> + Send,
92 {
93 path.as_ref().exists()
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use uuid::Uuid;
100
101 use super::*;
102
103 const FILE_BITS: u32 = 0o100000;
104 const DIR_BITS: u32 = 0o40000;
105
106 fn setup() -> String {
107 let test_dir = format!("/tmp/unit_test_{}", Uuid::new_v4());
108 std::fs::create_dir(&test_dir).unwrap();
109 test_dir
110 }
111
112 fn teardown(test_dir: String) {
113 std::fs::remove_dir_all(test_dir).unwrap();
114 }
115
116 #[tokio::test]
117 async fn create_dir_should_create_a_new_directory_at_path() {
118 let test_dir = setup();
119 let fs = LocalFileSystem;
120
121 let new_dir = format!("{test_dir}/mynewdir");
122 fs.create_dir(&new_dir).await.unwrap();
123
124 let new_dir_path = Path::new(&new_dir);
125 assert!(new_dir_path.exists() && new_dir_path.is_dir());
126 teardown(test_dir);
127 }
128
129 #[tokio::test]
130 async fn create_dir_should_bubble_up_error_if_some_happens() {
131 let test_dir = setup();
132 let fs = LocalFileSystem;
133
134 let new_dir = format!("{test_dir}/mynewdir");
135 std::fs::create_dir(&new_dir).unwrap();
137 let err = fs.create_dir(&new_dir).await.unwrap_err();
138
139 assert_eq!(err.to_string(), "File exists (os error 17)");
140 teardown(test_dir);
141 }
142
143 #[tokio::test]
144 async fn create_dir_all_should_create_a_new_directory_and_all_of_it_ancestors_at_path() {
145 let test_dir = setup();
146 let fs = LocalFileSystem;
147
148 let new_dir = format!("{test_dir}/the/path/to/mynewdir");
149 fs.create_dir_all(&new_dir).await.unwrap();
150
151 let new_dir_path = Path::new(&new_dir);
152 assert!(new_dir_path.exists() && new_dir_path.is_dir());
153 teardown(test_dir);
154 }
155
156 #[tokio::test]
157 async fn create_dir_all_should_bubble_up_error_if_some_happens() {
158 let test_dir = setup();
159 let fs = LocalFileSystem;
160
161 let new_dir = format!("{test_dir}/the/path/to/mynewdir");
162 std::fs::write(format!("{test_dir}/the"), b"test").unwrap();
164 let err = fs.create_dir_all(&new_dir).await.unwrap_err();
165
166 assert_eq!(err.to_string(), "Not a directory (os error 20)");
167 teardown(test_dir);
168 }
169
170 #[tokio::test]
171 async fn read_should_return_the_contents_of_the_file_at_path() {
172 let test_dir = setup();
173 let fs = LocalFileSystem;
174
175 let file_path = format!("{test_dir}/myfile");
176 std::fs::write(&file_path, b"Test").unwrap();
177 let contents = fs.read(file_path).await.unwrap();
178
179 assert_eq!(contents, b"Test");
180 teardown(test_dir);
181 }
182
183 #[tokio::test]
184 async fn read_should_bubble_up_error_if_some_happens() {
185 let test_dir = setup();
186 let fs = LocalFileSystem;
187
188 let file_path = format!("{test_dir}/myfile");
189 let err = fs.read(file_path).await.unwrap_err();
191
192 assert_eq!(err.to_string(), "No such file or directory (os error 2)");
193 teardown(test_dir);
194 }
195
196 #[tokio::test]
197 async fn read_to_string_should_return_the_contents_of_the_file_at_path_as_string() {
198 let test_dir = setup();
199 let fs = LocalFileSystem;
200
201 let file_path = format!("{test_dir}/myfile");
202 std::fs::write(&file_path, b"Test").unwrap();
203 let contents = fs.read_to_string(file_path).await.unwrap();
204
205 assert_eq!(contents, "Test");
206 teardown(test_dir);
207 }
208
209 #[tokio::test]
210 async fn read_to_string_should_bubble_up_error_if_some_happens() {
211 let test_dir = setup();
212 let fs = LocalFileSystem;
213
214 let file_path = format!("{test_dir}/myfile");
215 let err = fs.read_to_string(file_path).await.unwrap_err();
217
218 assert_eq!(err.to_string(), "No such file or directory (os error 2)");
219 teardown(test_dir);
220 }
221
222 #[tokio::test]
223 async fn write_should_create_a_new_file_at_path_with_contents() {
224 let test_dir = setup();
225 let fs = LocalFileSystem;
226
227 let file_path = format!("{test_dir}/myfile");
228 fs.write(&file_path, "Test").await.unwrap();
229
230 assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test");
231 teardown(test_dir);
232 }
233
234 #[tokio::test]
235 async fn write_should_overwrite_an_existing_file_with_contents() {
236 let test_dir = setup();
237 let fs = LocalFileSystem;
238
239 let file_path = format!("{test_dir}/myfile");
240 std::fs::write(&file_path, "Test").unwrap();
241 assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "Test");
242 fs.write(&file_path, "Test updated").await.unwrap();
243
244 assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test updated");
245 teardown(test_dir);
246 }
247
248 #[tokio::test]
249 async fn write_should_bubble_up_error_if_some_happens() {
250 let test_dir = setup();
251 let fs = LocalFileSystem;
252
253 let file_path = format!("{test_dir}/myfile");
254 std::fs::create_dir(&file_path).unwrap();
256 let err = fs.write(&file_path, "Test").await.unwrap_err();
257
258 assert_eq!(err.to_string(), "Is a directory (os error 21)");
259 teardown(test_dir);
260 }
261
262 #[tokio::test]
263 async fn append_should_create_a_new_file_at_path_with_contents() {
264 let test_dir = setup();
265 let fs = LocalFileSystem;
266
267 let file_path = format!("{test_dir}/myfile");
268 fs.append(&file_path, "Test").await.unwrap();
269
270 assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test");
271 teardown(test_dir);
272 }
273
274 #[tokio::test]
275 async fn append_should_updates_an_existing_file_by_appending_contents() {
276 let test_dir = setup();
277 let fs = LocalFileSystem;
278
279 let file_path = format!("{test_dir}/myfile");
280 std::fs::write(&file_path, "Test").unwrap();
281 assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "Test");
282 fs.append(&file_path, " updated").await.unwrap();
283
284 assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test updated");
285 teardown(test_dir);
286 }
287
288 #[tokio::test]
289 async fn append_should_bubble_up_error_if_some_happens() {
290 let test_dir = setup();
291 let fs = LocalFileSystem;
292
293 let file_path = format!("{test_dir}/myfile");
294 std::fs::create_dir(&file_path).unwrap();
296 let err = fs.append(&file_path, "Test").await.unwrap_err();
297
298 assert_eq!(err.to_string(), "Is a directory (os error 21)");
299 teardown(test_dir);
300 }
301
302 #[tokio::test]
303 async fn copy_should_create_a_duplicate_of_source() {
304 let test_dir = setup();
305 let fs = LocalFileSystem;
306
307 let from_path = format!("{test_dir}/myfile");
308 std::fs::write(&from_path, "Test").unwrap();
309 let to_path = format!("{test_dir}/mycopy");
310 fs.copy(&from_path, &to_path).await.unwrap();
311
312 assert_eq!(std::fs::read_to_string(to_path).unwrap(), "Test");
313 teardown(test_dir);
314 }
315
316 #[tokio::test]
317 async fn copy_should_ovewrite_destination_if_alread_exists() {
318 let test_dir = setup();
319 let fs = LocalFileSystem;
320
321 let from_path = format!("{test_dir}/myfile");
322 std::fs::write(&from_path, "Test").unwrap();
323 let to_path = format!("{test_dir}/mycopy");
324 std::fs::write(&from_path, "Some content").unwrap();
325 fs.copy(&from_path, &to_path).await.unwrap();
326
327 assert_eq!(std::fs::read_to_string(to_path).unwrap(), "Some content");
328 teardown(test_dir);
329 }
330
331 #[tokio::test]
332 async fn copy_should_bubble_up_error_if_some_happens() {
333 let test_dir = setup();
334 let fs = LocalFileSystem;
335
336 let from_path = format!("{test_dir}/nonexistentfile");
337 let to_path = format!("{test_dir}/mycopy");
338 let err = fs.copy(&from_path, &to_path).await.unwrap_err();
339
340 assert_eq!(err.to_string(), "No such file or directory (os error 2)");
341 teardown(test_dir);
342 }
343
344 #[tokio::test]
345 async fn set_mode_should_update_the_file_mode_at_path() {
346 let test_dir = setup();
347 let fs = LocalFileSystem;
348 let path = format!("{test_dir}/myfile");
349 std::fs::write(&path, "Test").unwrap();
350 assert!(std::fs::metadata(&path).unwrap().permissions().mode() != (FILE_BITS + 0o400));
351
352 fs.set_mode(&path, 0o400).await.unwrap();
353
354 assert_eq!(
355 std::fs::metadata(&path).unwrap().permissions().mode(),
356 FILE_BITS + 0o400
357 );
358 teardown(test_dir);
359 }
360
361 #[tokio::test]
362 async fn set_mode_should_update_the_directory_mode_at_path() {
363 let test_dir = setup();
364 let fs = LocalFileSystem;
365 let path = format!("{test_dir}/mydir");
366 std::fs::create_dir(&path).unwrap();
367 assert!(std::fs::metadata(&path).unwrap().permissions().mode() != (DIR_BITS + 0o700));
368
369 fs.set_mode(&path, 0o700).await.unwrap();
370
371 assert_eq!(
372 std::fs::metadata(&path).unwrap().permissions().mode(),
373 DIR_BITS + 0o700
374 );
375 teardown(test_dir);
376 }
377
378 #[tokio::test]
379 async fn set_mode_should_bubble_up_error_if_some_happens() {
380 let test_dir = setup();
381 let fs = LocalFileSystem;
382 let path = format!("{test_dir}/somemissingfile");
383 let err = fs.set_mode(&path, 0o400).await.unwrap_err();
386
387 assert_eq!(err.to_string(), "No such file or directory (os error 2)");
388 teardown(test_dir);
389 }
390}