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}