aws_types/
os_shim_internal.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Abstractions for testing code that interacts with the operating system:
7//! - Reading environment variables
8//! - Reading from the file system
9
10use std::collections::HashMap;
11use std::env::VarError;
12use std::ffi::OsString;
13use std::fmt::Debug;
14use std::path::{Path, PathBuf};
15use std::sync::{Arc, Mutex};
16
17use crate::os_shim_internal::fs::Fake;
18
19/// File system abstraction
20///
21/// Simple abstraction enabling in-memory mocking of the file system
22///
23/// # Examples
24/// Construct a file system which delegates to `std::fs`:
25/// ```rust
26/// let fs = aws_types::os_shim_internal::Fs::real();
27/// ```
28///
29/// Construct an in-memory file system for testing:
30/// ```rust
31/// use std::collections::HashMap;
32/// let fs = aws_types::os_shim_internal::Fs::from_map({
33///     let mut map = HashMap::new();
34///     map.insert("/home/.aws/config".to_string(), "[default]\nregion = us-east-1");
35///     map
36/// });
37/// ```
38#[derive(Clone, Debug)]
39pub struct Fs(fs::Inner);
40
41impl Default for Fs {
42    fn default() -> Self {
43        Fs::real()
44    }
45}
46
47impl Fs {
48    /// Create `Fs` representing a real file system.
49    pub fn real() -> Self {
50        Fs(fs::Inner::Real)
51    }
52
53    /// Create `Fs` from a map of `OsString` to `Vec<u8>`.
54    pub fn from_raw_map(fs: HashMap<OsString, Vec<u8>>) -> Self {
55        Fs(fs::Inner::Fake(Arc::new(Fake::MapFs(Mutex::new(fs)))))
56    }
57
58    /// Create `Fs` from a map of `String` to `Vec<u8>`.
59    pub fn from_map(data: HashMap<String, impl Into<Vec<u8>>>) -> Self {
60        let fs = data
61            .into_iter()
62            .map(|(k, v)| (k.into(), v.into()))
63            .collect();
64        Self::from_raw_map(fs)
65    }
66
67    /// Create a test filesystem rooted in real files
68    ///
69    /// Creates a test filesystem from the contents of `test_directory` rooted into `namespaced_to`.
70    ///
71    /// Example:
72    /// Given:
73    /// ```bash
74    /// $ ls
75    /// ./my-test-dir/aws-config
76    /// ./my-test-dir/aws-config/config
77    /// $ cat ./my-test-dir/aws-config/config
78    /// test-config
79    /// ```
80    /// ```rust,no_run
81    /// # async fn docs() {
82    /// use aws_types::os_shim_internal::{Env, Fs};
83    /// let env = Env::from_slice(&[("HOME", "/Users/me")]);
84    /// let fs = Fs::from_test_dir("my-test-dir/aws-config", "/Users/me/.aws/config");
85    /// assert_eq!(fs.read_to_end("/Users/me/.aws/config").await.unwrap(), b"test-config");
86    /// # }
87    pub fn from_test_dir(
88        test_directory: impl Into<PathBuf>,
89        namespaced_to: impl Into<PathBuf>,
90    ) -> Self {
91        Self(fs::Inner::Fake(Arc::new(Fake::NamespacedFs {
92            real_path: test_directory.into(),
93            namespaced_to: namespaced_to.into(),
94        })))
95    }
96
97    /// Create a fake process environment from a slice of tuples.
98    ///
99    /// # Examples
100    /// ```rust
101    /// # async fn example() {
102    /// use aws_types::os_shim_internal::Fs;
103    /// let mock_fs = Fs::from_slice(&[
104    ///     ("config", "[default]\nretry_mode = \"standard\""),
105    /// ]);
106    /// assert_eq!(mock_fs.read_to_end("config").await.unwrap(), b"[default]\nretry_mode = \"standard\"");
107    /// # }
108    /// ```
109    pub fn from_slice<'a>(files: &[(&'a str, &'a str)]) -> Self {
110        let fs: HashMap<String, Vec<u8>> = files
111            .iter()
112            .map(|(k, v)| {
113                let k = (*k).to_owned();
114                let v = v.as_bytes().to_vec();
115                (k, v)
116            })
117            .collect();
118
119        Self::from_map(fs)
120    }
121
122    /// Read the entire contents of a file
123    ///
124    /// _Note: This function is currently `async` primarily for forward compatibility. Currently,
125    /// this function does not use Tokio (or any other runtime) to perform IO, the IO is performed
126    /// directly within the function._
127    pub async fn read_to_end(&self, path: impl AsRef<Path>) -> std::io::Result<Vec<u8>> {
128        use fs::Inner;
129        let path = path.as_ref();
130        match &self.0 {
131            // TODO(https://github.com/awslabs/aws-sdk-rust/issues/867): Use async IO below
132            Inner::Real => std::fs::read(path),
133            Inner::Fake(fake) => match fake.as_ref() {
134                Fake::MapFs(fs) => fs
135                    .lock()
136                    .unwrap()
137                    .get(path.as_os_str())
138                    .cloned()
139                    .ok_or_else(|| std::io::ErrorKind::NotFound.into()),
140                Fake::NamespacedFs {
141                    real_path,
142                    namespaced_to,
143                } => {
144                    let actual_path = path
145                        .strip_prefix(namespaced_to)
146                        .map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound))?;
147                    std::fs::read(real_path.join(actual_path))
148                }
149            },
150        }
151    }
152
153    /// Write a slice as the entire contents of a file.
154    ///
155    /// This is equivalent to `std::fs::write`.
156    pub async fn write(
157        &self,
158        path: impl AsRef<Path>,
159        contents: impl AsRef<[u8]>,
160    ) -> std::io::Result<()> {
161        use fs::Inner;
162        match &self.0 {
163            // TODO(https://github.com/awslabs/aws-sdk-rust/issues/867): Use async IO below
164            Inner::Real => {
165                std::fs::write(path, contents)?;
166            }
167            Inner::Fake(fake) => match fake.as_ref() {
168                Fake::MapFs(fs) => {
169                    fs.lock()
170                        .unwrap()
171                        .insert(path.as_ref().as_os_str().into(), contents.as_ref().to_vec());
172                }
173                Fake::NamespacedFs {
174                    real_path,
175                    namespaced_to,
176                } => {
177                    let actual_path = path
178                        .as_ref()
179                        .strip_prefix(namespaced_to)
180                        .map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound))?;
181                    std::fs::write(real_path.join(actual_path), contents)?;
182                }
183            },
184        }
185        Ok(())
186    }
187}
188
189mod fs {
190    use std::collections::HashMap;
191    use std::ffi::OsString;
192    use std::path::PathBuf;
193    use std::sync::{Arc, Mutex};
194
195    #[derive(Clone, Debug)]
196    pub(super) enum Inner {
197        Real,
198        Fake(Arc<Fake>),
199    }
200
201    #[derive(Debug)]
202    pub(super) enum Fake {
203        MapFs(Mutex<HashMap<OsString, Vec<u8>>>),
204        NamespacedFs {
205            real_path: PathBuf,
206            namespaced_to: PathBuf,
207        },
208    }
209}
210
211/// Environment variable abstraction
212///
213/// Environment variables are global to a process, and, as such, are difficult to test with a multi-
214/// threaded test runner like Rust's. This enables loading environment variables either from the
215/// actual process environment ([`std::env::var`]) or from a hash map.
216///
217/// Process environments are cheap to clone:
218/// - Faked process environments are wrapped in an internal Arc
219/// - Real process environments are pointer-sized
220#[derive(Clone, Debug)]
221pub struct Env(env::Inner);
222
223impl Default for Env {
224    fn default() -> Self {
225        Self::real()
226    }
227}
228
229impl Env {
230    /// Retrieve a value for the given `k` and return `VarError` is that key is not present.
231    pub fn get(&self, k: &str) -> Result<String, VarError> {
232        use env::Inner;
233        match &self.0 {
234            Inner::Real => std::env::var(k),
235            Inner::Fake(map) => map.get(k).cloned().ok_or(VarError::NotPresent),
236        }
237    }
238
239    /// Create a fake process environment from a slice of tuples.
240    ///
241    /// # Examples
242    /// ```rust
243    /// use aws_types::os_shim_internal::Env;
244    /// let mock_env = Env::from_slice(&[
245    ///     ("HOME", "/home/myname"),
246    ///     ("AWS_REGION", "us-west-2")
247    /// ]);
248    /// assert_eq!(mock_env.get("HOME").unwrap(), "/home/myname");
249    /// ```
250    pub fn from_slice<'a>(vars: &[(&'a str, &'a str)]) -> Self {
251        let map: HashMap<_, _> = vars
252            .iter()
253            .map(|(k, v)| (k.to_string(), v.to_string()))
254            .collect();
255        Self::from(map)
256    }
257
258    /// Create a process environment that uses the real process environment
259    ///
260    /// Calls will be delegated to [`std::env::var`].
261    pub fn real() -> Self {
262        Self(env::Inner::Real)
263    }
264}
265
266impl From<HashMap<String, String>> for Env {
267    fn from(hash_map: HashMap<String, String>) -> Self {
268        Self(env::Inner::Fake(Arc::new(hash_map)))
269    }
270}
271
272mod env {
273    use std::collections::HashMap;
274    use std::sync::Arc;
275
276    #[derive(Clone, Debug)]
277    pub(super) enum Inner {
278        Real,
279        Fake(Arc<HashMap<String, String>>),
280    }
281}
282
283#[cfg(test)]
284mod test {
285    use std::env::VarError;
286
287    use crate::os_shim_internal::{Env, Fs};
288
289    #[test]
290    fn env_works() {
291        let env = Env::from_slice(&[("FOO", "BAR")]);
292        assert_eq!(env.get("FOO").unwrap(), "BAR");
293        assert_eq!(
294            env.get("OTHER").expect_err("no present"),
295            VarError::NotPresent
296        )
297    }
298
299    #[tokio::test]
300    async fn fs_from_test_dir_works() {
301        let fs = Fs::from_test_dir(".", "/users/test-data");
302        let _ = fs
303            .read_to_end("/users/test-data/Cargo.toml")
304            .await
305            .expect("file exists");
306
307        let _ = fs
308            .read_to_end("doesntexist")
309            .await
310            .expect_err("file doesnt exists");
311    }
312
313    #[tokio::test]
314    async fn fs_round_trip_file_with_real() {
315        let temp = tempfile::tempdir().unwrap();
316        let path = temp.path().join("test-file");
317
318        let fs = Fs::real();
319        fs.read_to_end(&path)
320            .await
321            .expect_err("file doesn't exist yet");
322
323        fs.write(&path, b"test").await.expect("success");
324
325        let result = fs.read_to_end(&path).await.expect("success");
326        assert_eq!(b"test", &result[..]);
327    }
328}