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