4 4 | */
|
5 5 |
|
6 6 | //! Abstractions for testing code that interacts with the operating system:
|
7 7 | //! - Reading environment variables
|
8 8 | //! - Reading from the file system
|
9 9 |
|
10 10 | use std::collections::HashMap;
|
11 11 | use std::env::VarError;
|
12 12 | use std::ffi::OsString;
|
13 13 | use std::fmt::Debug;
|
14 - | use std::future::Future;
|
15 - | use std::panic::{RefUnwindSafe, UnwindSafe};
|
16 14 | use std::path::{Path, PathBuf};
|
17 - | use std::pin::Pin;
|
18 15 | use std::sync::{Arc, Mutex};
|
19 16 |
|
20 17 | use crate::os_shim_internal::fs::Fake;
|
21 18 |
|
22 - | /// Trait for custom environment variable providers.
|
23 - | pub trait ProvideEnv: Debug + Send + Sync + UnwindSafe + RefUnwindSafe {
|
24 - | /// Get the value of environment variable `k`.
|
25 - | fn get(&self, k: &str) -> Result<String, VarError>;
|
26 - | }
|
27 - |
|
28 - | /// Trait for custom filesystem providers.
|
29 - | pub trait ProvideFs: Debug + Send + Sync + UnwindSafe + RefUnwindSafe {
|
30 - | /// Read the entire contents of the file at `path`.
|
31 - | fn read_to_end(
|
32 - | &self,
|
33 - | path: &Path,
|
34 - | ) -> Pin<Box<dyn Future<Output = std::io::Result<Vec<u8>>> + Send + '_>>;
|
35 - |
|
36 - | /// Write `contents` to the file at `path`.
|
37 - | fn write(
|
38 - | &self,
|
39 - | path: &Path,
|
40 - | contents: &[u8],
|
41 - | ) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>>;
|
42 - | }
|
43 - |
|
44 19 | /// File system abstraction
|
45 20 | ///
|
46 21 | /// Simple abstraction enabling in-memory mocking of the file system
|
47 22 | ///
|
48 23 | /// # Examples
|
49 24 | /// Construct a file system which delegates to `std::fs`:
|
50 25 | /// ```rust
|
51 26 | /// let fs = aws_types::os_shim_internal::Fs::real();
|
52 27 | /// ```
|
53 28 | ///
|
54 29 | /// Construct an in-memory file system for testing:
|
55 30 | /// ```rust
|
56 31 | /// use std::collections::HashMap;
|
57 32 | /// let fs = aws_types::os_shim_internal::Fs::from_map({
|
58 33 | /// let mut map = HashMap::new();
|
59 34 | /// map.insert("/home/.aws/config".to_string(), "[default]\nregion = us-east-1");
|
60 35 | /// map
|
61 36 | /// });
|
62 37 | /// ```
|
63 38 | #[derive(Clone, Debug)]
|
64 39 | pub struct Fs(fs::Inner);
|
65 40 |
|
66 41 | impl Default for Fs {
|
67 42 | fn default() -> Self {
|
68 43 | Fs::real()
|
69 44 | }
|
70 45 | }
|
71 46 |
|
72 47 | impl Fs {
|
73 48 | /// Create `Fs` representing a real file system.
|
74 49 | pub fn real() -> Self {
|
75 50 | Fs(fs::Inner::Real)
|
76 51 | }
|
77 52 |
|
78 - | /// Create an `Fs` backed by a custom `ProvideFs` implementation.
|
79 - | pub fn from_custom(provider: impl ProvideFs + 'static) -> Self {
|
80 - | Self(fs::Inner::Custom(Arc::new(provider)))
|
81 - | }
|
82 - |
|
83 53 | /// Create `Fs` from a map of `OsString` to `Vec<u8>`.
|
84 54 | pub fn from_raw_map(fs: HashMap<OsString, Vec<u8>>) -> Self {
|
85 55 | Fs(fs::Inner::Fake(Arc::new(Fake::MapFs(Mutex::new(fs)))))
|
86 56 | }
|
87 57 |
|
88 58 | /// Create `Fs` from a map of `String` to `Vec<u8>`.
|
89 59 | pub fn from_map(data: HashMap<String, impl Into<Vec<u8>>>) -> Self {
|
90 60 | let fs = data
|
91 61 | .into_iter()
|
92 62 | .map(|(k, v)| (k.into(), v.into()))
|
170 140 | Fake::NamespacedFs {
|
171 141 | real_path,
|
172 142 | namespaced_to,
|
173 143 | } => {
|
174 144 | let actual_path = path
|
175 145 | .strip_prefix(namespaced_to)
|
176 146 | .map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound))?;
|
177 147 | std::fs::read(real_path.join(actual_path))
|
178 148 | }
|
179 149 | },
|
180 - | Inner::Custom(provider) => provider.read_to_end(path).await,
|
181 150 | }
|
182 151 | }
|
183 152 |
|
184 153 | /// Write a slice as the entire contents of a file.
|
185 154 | ///
|
186 155 | /// This is equivalent to `std::fs::write`.
|
187 156 | pub async fn write(
|
188 157 | &self,
|
189 158 | path: impl AsRef<Path>,
|
190 159 | contents: impl AsRef<[u8]>,
|
191 160 | ) -> std::io::Result<()> {
|
192 161 | use fs::Inner;
|
193 162 | match &self.0 {
|
194 163 | // TODO(https://github.com/awslabs/aws-sdk-rust/issues/867): Use async IO below
|
195 164 | Inner::Real => {
|
196 165 | std::fs::write(&path, contents)?;
|
197 166 |
|
198 167 | #[cfg(unix)]
|
199 168 | {
|
200 169 | use std::os::unix::fs::PermissionsExt;
|
201 170 | std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
|
202 171 | }
|
203 172 | }
|
204 173 | Inner::Fake(fake) => match fake.as_ref() {
|
205 174 | Fake::MapFs(fs) => {
|
206 175 | fs.lock()
|
207 176 | .unwrap()
|
208 177 | .insert(path.as_ref().as_os_str().into(), contents.as_ref().to_vec());
|
209 178 | }
|
210 179 | Fake::NamespacedFs {
|
211 180 | real_path,
|
212 181 | namespaced_to,
|
213 182 | } => {
|
214 183 | let actual_path = path
|
215 184 | .as_ref()
|
216 185 | .strip_prefix(namespaced_to)
|
217 186 | .map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound))?;
|
218 187 | std::fs::write(real_path.join(actual_path), contents)?;
|
219 188 | }
|
220 189 | },
|
221 - | Inner::Custom(provider) => {
|
222 - | return provider.write(path.as_ref(), contents.as_ref()).await
|
223 - | }
|
224 190 | }
|
225 191 | Ok(())
|
226 192 | }
|
227 193 | }
|
228 194 |
|
229 195 | mod fs {
|
230 196 | use std::collections::HashMap;
|
231 197 | use std::ffi::OsString;
|
232 198 | use std::path::PathBuf;
|
233 199 | use std::sync::{Arc, Mutex};
|
234 200 |
|
235 - | use super::ProvideFs;
|
236 - |
|
237 201 | #[derive(Clone, Debug)]
|
238 202 | pub(super) enum Inner {
|
239 203 | Real,
|
240 204 | Fake(Arc<Fake>),
|
241 - | Custom(Arc<dyn ProvideFs>),
|
242 205 | }
|
243 206 |
|
244 207 | #[derive(Debug)]
|
245 208 | pub(super) enum Fake {
|
246 209 | MapFs(Mutex<HashMap<OsString, Vec<u8>>>),
|
247 210 | NamespacedFs {
|
248 211 | real_path: PathBuf,
|
249 212 | namespaced_to: PathBuf,
|
250 213 | },
|
251 214 | }
|
252 215 | }
|
253 216 |
|
254 217 | /// Environment variable abstraction
|
255 218 | ///
|
256 219 | /// Environment variables are global to a process, and, as such, are difficult to test with a multi-
|
257 220 | /// threaded test runner like Rust's. This enables loading environment variables either from the
|
258 221 | /// actual process environment ([`std::env::var`]) or from a hash map.
|
259 222 | ///
|
260 223 | /// Process environments are cheap to clone:
|
261 224 | /// - Faked process environments are wrapped in an internal Arc
|
262 225 | /// - Real process environments are pointer-sized
|
263 226 | #[derive(Clone, Debug)]
|
264 227 | pub struct Env(env::Inner);
|
265 228 |
|
266 229 | impl Default for Env {
|
267 230 | fn default() -> Self {
|
268 231 | Self::real()
|
269 232 | }
|
270 233 | }
|
271 234 |
|
272 235 | impl Env {
|
273 236 | /// Retrieve a value for the given `k` and return `VarError` is that key is not present.
|
274 237 | pub fn get(&self, k: &str) -> Result<String, VarError> {
|
275 238 | use env::Inner;
|
276 239 | match &self.0 {
|
277 240 | Inner::Real => std::env::var(k),
|
278 241 | Inner::Fake(map) => map.get(k).cloned().ok_or(VarError::NotPresent),
|
279 - | Inner::Custom(provider) => provider.get(k),
|
280 242 | }
|
281 243 | }
|
282 244 |
|
283 245 | /// Create a fake process environment from a slice of tuples.
|
284 246 | ///
|
285 247 | /// # Examples
|
286 248 | /// ```rust
|
287 249 | /// use aws_types::os_shim_internal::Env;
|
288 250 | /// let mock_env = Env::from_slice(&[
|
289 251 | /// ("HOME", "/home/myname"),
|
290 252 | /// ("AWS_REGION", "us-west-2")
|
291 253 | /// ]);
|
292 254 | /// assert_eq!(mock_env.get("HOME").unwrap(), "/home/myname");
|
293 255 | /// ```
|
294 256 | pub fn from_slice<'a>(vars: &[(&'a str, &'a str)]) -> Self {
|
295 257 | let map: HashMap<_, _> = vars
|
296 258 | .iter()
|
297 259 | .map(|(k, v)| (k.to_string(), v.to_string()))
|
298 260 | .collect();
|
299 261 | Self::from(map)
|
300 262 | }
|
301 263 |
|
302 264 | /// Create a process environment that uses the real process environment
|
303 265 | ///
|
304 266 | /// Calls will be delegated to [`std::env::var`].
|
305 267 | pub fn real() -> Self {
|
306 268 | Self(env::Inner::Real)
|
307 269 | }
|
308 - |
|
309 - | /// Create an `Env` backed by a custom `ProvideEnv` implementation.
|
310 - | pub fn from_custom(provider: impl ProvideEnv + 'static) -> Self {
|
311 - | Self(env::Inner::Custom(Arc::new(provider)))
|
312 - | }
|
313 270 | }
|
314 271 |
|
315 272 | impl From<HashMap<String, String>> for Env {
|
316 273 | fn from(hash_map: HashMap<String, String>) -> Self {
|
317 274 | Self(env::Inner::Fake(Arc::new(hash_map)))
|
318 275 | }
|
319 276 | }
|
320 277 |
|
321 278 | mod env {
|
322 279 | use std::collections::HashMap;
|
323 280 | use std::sync::Arc;
|
324 281 |
|
325 - | use super::ProvideEnv;
|
326 - |
|
327 282 | #[derive(Clone, Debug)]
|
328 283 | pub(super) enum Inner {
|
329 284 | Real,
|
330 285 | Fake(Arc<HashMap<String, String>>),
|
331 - | Custom(Arc<dyn ProvideEnv>),
|
332 286 | }
|
333 287 | }
|
334 288 |
|
335 289 | #[cfg(test)]
|
336 290 | mod test {
|
337 - | use std::collections::HashMap;
|
338 291 | use std::env::VarError;
|
339 - | use std::future::Future;
|
340 - | use std::path::{Path, PathBuf};
|
341 - | use std::pin::Pin;
|
342 - | use std::sync::Mutex;
|
343 292 |
|
344 - | use crate::os_shim_internal::{Env, Fs, ProvideEnv, ProvideFs};
|
293 + | use crate::os_shim_internal::{Env, Fs};
|
345 294 |
|
346 295 | #[test]
|
347 296 | fn env_works() {
|
348 297 | let env = Env::from_slice(&[("FOO", "BAR")]);
|
349 298 | assert_eq!(env.get("FOO").unwrap(), "BAR");
|
350 299 | assert_eq!(
|
351 300 | env.get("OTHER").expect_err("no present"),
|
352 301 | VarError::NotPresent
|
353 302 | )
|
354 303 | }
|
355 304 |
|
356 305 | #[tokio::test]
|
357 306 | async fn fs_from_test_dir_works() {
|
358 307 | let fs = Fs::from_test_dir(".", "/users/test-data");
|
359 308 | let _ = fs
|
360 309 | .read_to_end("/users/test-data/Cargo.toml")
|
361 310 | .await
|
362 311 | .expect("file exists");
|
363 312 |
|
364 313 | let _ = fs
|
365 314 | .read_to_end("doesntexist")
|
366 315 | .await
|
367 316 | .expect_err("file doesnt exists");
|
368 317 | }
|
369 318 |
|
370 319 | #[tokio::test]
|
371 320 | async fn fs_round_trip_file_with_real() {
|
372 321 | let temp = tempfile::tempdir().unwrap();
|
373 322 | let path = temp.path().join("test-file");
|
374 323 |
|
375 324 | let fs = Fs::real();
|
376 325 | fs.read_to_end(&path)
|
377 326 | .await
|
378 327 | .expect_err("file doesn't exist yet");
|
379 328 |
|
380 329 | fs.write(&path, b"test").await.expect("success");
|
381 330 |
|
382 331 | let result = fs.read_to_end(&path).await.expect("success");
|
383 332 | assert_eq!(b"test", &result[..]);
|
384 333 | }
|
385 334 |
|
386 - | #[test]
|
387 - | fn custom_env_works() {
|
388 - | #[derive(Debug)]
|
389 - | struct CustomEnvProvider {
|
390 - | vars: HashMap<String, String>,
|
391 - | }
|
392 - |
|
393 - | impl ProvideEnv for CustomEnvProvider {
|
394 - | fn get(&self, k: &str) -> Result<String, VarError> {
|
395 - | self.vars.get(k).cloned().ok_or(VarError::NotPresent)
|
396 - | }
|
397 - | }
|
398 - |
|
399 - | let mut vars = HashMap::new();
|
400 - | vars.insert("FOO".to_string(), "BAR".to_string());
|
401 - | let env = Env::from_custom(CustomEnvProvider { vars });
|
402 - | assert_eq!(env.get("FOO").unwrap(), "BAR");
|
403 - | assert_eq!(
|
404 - | env.get("OTHER").expect_err("not present"),
|
405 - | VarError::NotPresent
|
406 - | );
|
407 - | }
|
408 - |
|
409 - | #[tokio::test]
|
410 - | async fn custom_fs_round_trip() {
|
411 - | #[derive(Debug)]
|
412 - | struct InMemoryFs {
|
413 - | files: Mutex<HashMap<PathBuf, Vec<u8>>>,
|
414 - | }
|
415 - |
|
416 - | impl ProvideFs for InMemoryFs {
|
417 - | fn read_to_end(
|
418 - | &self,
|
419 - | path: &Path,
|
420 - | ) -> Pin<Box<dyn Future<Output = std::io::Result<Vec<u8>>> + Send + '_>> {
|
421 - | let path = path.to_path_buf();
|
422 - | Box::pin(async move {
|
423 - | self.files
|
424 - | .lock()
|
425 - | .unwrap()
|
426 - | .get(&path)
|
427 - | .cloned()
|
428 - | .ok_or_else(|| std::io::ErrorKind::NotFound.into())
|
429 - | })
|
430 - | }
|
431 - |
|
432 - | fn write(
|
433 - | &self,
|
434 - | path: &Path,
|
435 - | contents: &[u8],
|
436 - | ) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>> {
|
437 - | let path = path.to_path_buf();
|
438 - | let contents = contents.to_vec();
|
439 - | Box::pin(async move {
|
440 - | self.files.lock().unwrap().insert(path, contents);
|
441 - | Ok(())
|
442 - | })
|
443 - | }
|
444 - | }
|
445 - |
|
446 - | let provider = InMemoryFs {
|
447 - | files: Mutex::new(HashMap::new()),
|
448 - | };
|
449 - | let fs = Fs::from_custom(provider);
|
450 - |
|
451 - | fs.read_to_end("/missing")
|
452 - | .await
|
453 - | .expect_err("file doesn't exist yet");
|
454 - |
|
455 - | fs.write("/test-file", b"hello")
|
456 - | .await
|
457 - | .expect("write succeeds");
|
458 - |
|
459 - | let result = fs.read_to_end("/test-file").await.expect("read succeeds");
|
460 - | assert_eq!(result, b"hello");
|
461 - | }
|
462 - |
|
463 335 | #[cfg(unix)]
|
464 336 | #[tokio::test]
|
465 337 | async fn real_fs_write_sets_owner_only_permissions_on_unix() {
|
466 338 | use std::os::unix::fs::PermissionsExt;
|
467 339 |
|
468 340 | let dir = tempfile::tempdir().expect("create temp dir");
|
469 341 | let path = dir.path().join("secret.txt");
|
470 342 | let fs = Fs::real();
|
471 343 |
|
472 344 | fs.write(&path, b"sensitive").await.expect("write succeeds");
|