1#[cfg(not(target_os = "windows"))]
2use super::fetch::fetch_official_pkg_names;
3#[cfg(not(target_os = "windows"))]
4use super::{OfficialPkg, idx, save_to_disk};
5
6#[cfg(not(target_os = "windows"))]
21pub async fn update_in_background(
22 persist_path: std::path::PathBuf,
23 net_err_tx: tokio::sync::mpsc::UnboundedSender<String>,
24 notify_tx: tokio::sync::mpsc::UnboundedSender<()>,
25) {
26 tokio::spawn(async move {
27 tracing::info!("refreshing official index in background");
28 match fetch_official_pkg_names().await {
29 Ok(new_pkgs) => {
30 let new_count = new_pkgs.len();
31 let (different, merged): (bool, Vec<OfficialPkg>) = {
32 let guard = idx().read().ok();
33 if let Some(g) = guard {
34 use std::collections::{HashMap, HashSet};
35 let pkg_key =
36 |p: &OfficialPkg| (p.repo.to_lowercase(), p.name.to_lowercase());
37 let old_keys: HashSet<(String, String)> =
38 g.pkgs.iter().map(pkg_key).collect();
39 let new_keys: HashSet<(String, String)> =
40 new_pkgs.iter().map(pkg_key).collect();
41 let different = old_keys != new_keys;
42 let mut old_map: HashMap<(String, String), &OfficialPkg> = HashMap::new();
44 for p in &g.pkgs {
45 old_map.insert(pkg_key(p), p);
46 }
47 let mut merged = Vec::with_capacity(new_pkgs.len());
48 for mut p in new_pkgs {
49 if let Some(old) = old_map.get(&pkg_key(&p)) {
50 p.repo.clone_from(&old.repo);
52 p.arch.clone_from(&old.arch);
53 p.version.clone_from(&old.version);
54 p.description.clone_from(&old.description);
55 }
56 merged.push(p);
57 }
58 (different, merged)
59 } else {
60 (true, new_pkgs)
61 }
62 };
63 if different {
64 if let Ok(mut g) = idx().write() {
65 g.pkgs = merged;
66 g.rebuild_name_index();
67 }
68 save_to_disk(&persist_path);
69 let _ = notify_tx.send(());
70 tracing::info!(count = new_count, "official index updated (names changed)");
71 } else {
72 tracing::info!(
73 count = new_count,
74 "official index up-to-date (no name changes)"
75 );
76 }
77 }
78 Err(e) => {
79 let _ = net_err_tx.send(format!("Failed to refresh official index: {e}"));
80 tracing::warn!(error = %e, "failed to refresh official index");
81 }
82 }
83 });
84}
85
86#[cfg(not(target_os = "windows"))]
87#[cfg(test)]
88mod tests {
89 #[tokio::test]
90 #[allow(clippy::await_holding_lock)]
91 async fn update_merges_preserving_enriched_fields_and_notifies_on_name_changes() {
102 let _guard = crate::global_test_mutex_lock();
103
104 seed_enriched_index();
106
107 let (old_path, root, tmp) = setup_fake_pacman_for_update();
109
110 let (err_tx, mut err_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
112 let (notify_tx, mut notify_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
113 super::update_in_background(tmp.clone(), err_tx, notify_tx).await;
114
115 verify_update_notification(&mut notify_rx, &mut err_rx).await;
117
118 verify_enriched_fields_preserved();
120
121 teardown_test_env(&old_path, &tmp, &root);
123 }
124
125 fn seed_enriched_index() {
134 if let Ok(mut g) = super::idx().write() {
135 g.pkgs = vec![super::OfficialPkg {
136 name: "foo".to_string(),
137 repo: "core".to_string(),
138 arch: "x86_64".to_string(),
139 version: "0.9".to_string(),
140 description: "old".to_string(),
141 }];
142 }
143 }
144
145 fn setup_fake_pacman_for_update() -> (String, std::path::PathBuf, std::path::PathBuf) {
155 let old_path = std::env::var("PATH").unwrap_or_default();
156 let mut root = std::env::temp_dir();
157 root.push(format!(
158 "pacsea_fake_pacman_update_{}_{}",
159 std::process::id(),
160 std::time::SystemTime::now()
161 .duration_since(std::time::UNIX_EPOCH)
162 .expect("System time is before UNIX epoch")
163 .as_nanos()
164 ));
165 std::fs::create_dir_all(&root).expect("failed to create test root directory");
166 let mut bin = root.clone();
167 bin.push("bin");
168 std::fs::create_dir_all(&bin).expect("failed to create test bin directory");
169 let mut script = bin.clone();
170 script.push("pacman");
171 let body = r#"#!/usr/bin/env bash
172set -e
173if [[ "$1" == "-Sl" ]]; then
174 repo="$2"
175 case "$repo" in
176 core)
177 echo "core foo 1.0"
178 ;;
179 extra)
180 echo "extra bar 2.0"
181 ;;
182 esac
183 exit 0
184fi
185exit 0
186"#;
187 std::fs::write(&script, body).expect("failed to write test pacman script");
188 #[cfg(unix)]
189 {
190 use std::os::unix::fs::PermissionsExt;
191 let mut perm = std::fs::metadata(&script)
192 .expect("failed to read test pacman script metadata")
193 .permissions();
194 perm.set_mode(0o755);
195 std::fs::set_permissions(&script, perm)
196 .expect("failed to set test pacman script permissions");
197 }
198 let new_path = format!("{}:{old_path}", bin.to_string_lossy());
199 unsafe { std::env::set_var("PATH", &new_path) };
200 let mut tmp = std::env::temp_dir();
201 tmp.push("pacsea_update_merge.json");
202 (old_path, root, tmp)
203 }
204
205 async fn verify_update_notification(
216 notify_rx: &mut tokio::sync::mpsc::UnboundedReceiver<()>,
217 err_rx: &mut tokio::sync::mpsc::UnboundedReceiver<String>,
218 ) {
219 let notified =
220 tokio::time::timeout(std::time::Duration::from_millis(500), notify_rx.recv())
221 .await
222 .ok()
223 .flatten()
224 .is_some();
225 assert!(notified);
226 let none = tokio::time::timeout(std::time::Duration::from_millis(200), err_rx.recv())
227 .await
228 .ok()
229 .flatten();
230 assert!(none.is_none());
231 }
232
233 fn verify_enriched_fields_preserved() {
242 let items = crate::index::all_official();
243 let foo = items
244 .iter()
245 .find(|p| p.name == "foo")
246 .expect("package 'foo' should exist in test data");
247 match &foo.source {
248 crate::state::Source::Official { repo, arch } => {
249 assert_eq!(repo, "core");
250 assert_eq!(arch, "x86_64");
251 }
252 crate::state::Source::Aur => panic!("expected official"),
253 }
254 assert_eq!(foo.version, "0.9"); }
256
257 fn teardown_test_env(old_path: &str, tmp: &std::path::PathBuf, root: &std::path::PathBuf) {
269 unsafe { std::env::set_var("PATH", old_path) };
270 let _ = std::fs::remove_file(tmp);
271 let _ = std::fs::remove_dir_all(root);
272 }
273}