pacsea/index/
installed.rs

1use super::installed_lock;
2
3/// What: Refresh the process-wide cache of installed package names using `pacman -Qq`.
4///
5/// Inputs:
6/// - None (spawns a blocking task to run pacman)
7///
8/// Output:
9/// - Updates the global installed-name set; ignores errors.
10///
11/// Details:
12/// - Parses command stdout into a `HashSet` and swaps it into the shared cache under a write lock.
13pub async fn refresh_installed_cache() {
14    if let Ok(Ok(body)) =
15        tokio::task::spawn_blocking(|| crate::util::pacman::run_pacman(&["-Qq"])).await
16    {
17        let set: std::collections::HashSet<String> =
18            body.lines().map(|s| s.trim().to_string()).collect();
19        if let Ok(mut g) = installed_lock().write() {
20            *g = set;
21        }
22    }
23}
24
25/// What: Query whether `name` appears in the cached set of installed packages.
26///
27/// Inputs:
28/// - `name`: Package name
29///
30/// Output:
31/// - `true` if `name` is present; `false` when absent or if the cache is unavailable.
32///
33/// Details:
34/// - Acquires a read lock and defers to `HashSet::contains`, returning false on lock poisoning.
35#[must_use]
36pub fn is_installed(name: &str) -> bool {
37    installed_lock()
38        .read()
39        .ok()
40        .is_some_and(|s| s.contains(name))
41}
42
43#[cfg(test)]
44mod tests {
45    /// What: Return false when the cache is empty or the package is missing.
46    ///
47    /// Inputs:
48    /// - Clear `INSTALLED_SET` and query an unknown package name.
49    ///
50    /// Output:
51    /// - Boolean `false` result.
52    ///
53    /// Details:
54    /// - Confirms empty cache behaves as expected without panicking.
55    #[test]
56    fn is_installed_returns_false_when_uninitialized_or_missing() {
57        let _guard = crate::global_test_mutex()
58            .lock()
59            .unwrap_or_else(std::sync::PoisonError::into_inner);
60        if let Ok(mut g) = super::installed_lock().write() {
61            g.clear();
62        }
63        assert!(!super::is_installed("foo"));
64    }
65
66    /// What: Verify membership lookups return true only for cached names.
67    ///
68    /// Inputs:
69    /// - Insert `bar` into `INSTALLED_SET` before querying.
70    ///
71    /// Output:
72    /// - `true` for `bar` and `false` for `baz`.
73    ///
74    /// Details:
75    /// - Exercises both positive and negative membership checks.
76    #[test]
77    fn is_installed_checks_membership_in_cached_set() {
78        let _guard = crate::global_test_mutex()
79            .lock()
80            .unwrap_or_else(std::sync::PoisonError::into_inner);
81        if let Ok(mut g) = super::installed_lock().write() {
82            g.clear();
83            g.insert("bar".to_string());
84        }
85        assert!(super::is_installed("bar"));
86        assert!(!super::is_installed("baz"));
87    }
88
89    #[cfg(not(target_os = "windows"))]
90    #[allow(clippy::await_holding_lock)]
91    #[tokio::test]
92    /// What: Populate the installed cache from pacman output.
93    ///
94    /// Inputs:
95    /// - Override PATH with a fake pacman that emits installed package names before invoking the refresh.
96    ///
97    /// Output:
98    /// - Cache lookup succeeds for the emitted names after `refresh_installed_cache` completes.
99    ///
100    /// Details:
101    /// - Exercises the async refresh path, ensures PATH is restored, and verifies cache contents via helper accessors.
102    async fn refresh_installed_cache_populates_cache_from_pacman_output() {
103        struct PathGuard {
104            original: String,
105        }
106        impl Drop for PathGuard {
107            fn drop(&mut self) {
108                unsafe {
109                    std::env::set_var("PATH", &self.original);
110                }
111            }
112        }
113        let _guard = crate::global_test_mutex_lock();
114
115        if let Ok(mut g) = super::installed_lock().write() {
116            g.clear();
117        }
118
119        let original_path = std::env::var("PATH").unwrap_or_default();
120        let _path_guard = PathGuard {
121            original: original_path.clone(),
122        };
123
124        let mut root = std::env::temp_dir();
125        root.push(format!(
126            "pacsea_fake_pacman_qq_{}_{}",
127            std::process::id(),
128            std::time::SystemTime::now()
129                .duration_since(std::time::UNIX_EPOCH)
130                .expect("System time is before UNIX epoch")
131                .as_nanos()
132        ));
133        std::fs::create_dir_all(&root).expect("failed to create test root directory");
134        let mut bin = root.clone();
135        bin.push("bin");
136        std::fs::create_dir_all(&bin).expect("failed to create test bin directory");
137        let mut script = bin.clone();
138        script.push("pacman");
139        let body = r#"#!/usr/bin/env bash
140set -e
141if [[ "$1" == "-Qq" ]]; then
142  echo "alpha"
143  echo "beta"
144  exit 0
145fi
146exit 1
147"#;
148        std::fs::write(&script, body).expect("failed to write test pacman script");
149        #[cfg(unix)]
150        {
151            use std::os::unix::fs::PermissionsExt;
152            let mut perm = std::fs::metadata(&script)
153                .expect("failed to read test pacman script metadata")
154                .permissions();
155            perm.set_mode(0o755);
156            std::fs::set_permissions(&script, perm)
157                .expect("failed to set test pacman script permissions");
158        }
159        let new_path = format!("{}:{original_path}", bin.to_string_lossy());
160        unsafe {
161            std::env::set_var("PATH", &new_path);
162        }
163
164        super::refresh_installed_cache().await;
165
166        let _ = std::fs::remove_dir_all(&root);
167
168        assert!(super::is_installed("alpha"));
169        assert!(super::is_installed("beta"));
170        assert!(!super::is_installed("gamma"));
171    }
172}