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