Skip to main content

cli/
test_support.rs

1//! Shared test-only utilities.
2//!
3//! Multiple modules' tests mutate process-global environment variables
4//! (`HOME`, `SHINE_CONFIG_DIR`, `SHINE_PRESETS`) to control config/preset
5//! resolution. A single crate-wide lock serialises these mutations so
6//! tests in different modules don't race on the shared process environment
7//! when `cargo test` runs unit tests in parallel.
8
9use std::path::{Path, PathBuf};
10use std::sync::{Mutex, MutexGuard, OnceLock};
11use std::time::{Duration, Instant};
12
13/// Creates and returns a uniquely-named temp directory under the OS temp dir.
14///
15/// `prefix` should identify the calling module (e.g. `"shine-fileops"`) so
16/// leftover directories from a failed test run are easy to trace back to
17/// their source.
18pub async fn make_temp_dir(prefix: &str) -> PathBuf {
19    let dir = std::env::temp_dir().join(format!("{prefix}-{}", uuid::Uuid::new_v4()));
20    tokio::fs::create_dir_all(&dir).await.unwrap();
21    dir
22}
23
24/// A `Config` rooted at `dir`, for tests that don't need a separate `home`
25/// subdirectory. `config::test_util::config_in` is a distinct homed variant
26/// (it additionally roots `home_dir` under `dir.join("home")`) and stays
27/// separate from this one.
28pub fn test_config(dir: &Path) -> crate::config::Config {
29    crate::config::Config::new_for_test(dir)
30}
31
32/// Restores the process's current directory, for tests that temporarily
33/// `set_current_dir` to exercise relative-path resolution.
34pub fn restore_current_dir(dir: &Path) {
35    std::env::set_current_dir(dir).expect("restore current dir");
36}
37
38pub fn env_lock() -> MutexGuard<'static, ()> {
39    static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
40    ENV_LOCK
41        .get_or_init(|| Mutex::new(()))
42        .lock()
43        .unwrap_or_else(|poisoned| poisoned.into_inner())
44}
45
46/// Some embedded app categories (e.g. `docker-engine`) install to a real,
47/// absolute system path (`/etc/docker/daemon.json`) rather than one scoped
48/// under the test's temporary `HOME`. `cargo nextest` runs each test in its
49/// own OS process, so `env_lock()` — a single-process `Mutex` — cannot
50/// prevent two such test processes from racing on that one real, shared
51/// file. Tests that install/uninstall the full embedded category set must
52/// hold this cross-process lock for their entire body.
53pub struct AdminCategoryTestLockGuard {
54    path: PathBuf,
55}
56
57impl Drop for AdminCategoryTestLockGuard {
58    fn drop(&mut self) {
59        let _ = std::fs::remove_dir(&self.path);
60    }
61}
62
63pub async fn admin_category_test_lock() -> AdminCategoryTestLockGuard {
64    let path = std::env::temp_dir().join("shine-admin-category-test.lock");
65    let deadline = Instant::now() + Duration::from_secs(60);
66    loop {
67        match tokio::fs::create_dir(&path).await {
68            Ok(()) => return AdminCategoryTestLockGuard { path },
69            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
70                if Instant::now() >= deadline {
71                    // Stale lock from a crashed process: reclaim it.
72                    let _ = tokio::fs::remove_dir(&path).await;
73                    continue;
74                }
75                tokio::time::sleep(Duration::from_millis(50)).await;
76            }
77            Err(_) => return AdminCategoryTestLockGuard { path },
78        }
79    }
80}