use super::{idx, save_to_disk};
pub fn request_enrich_for(
persist_path: std::path::PathBuf,
notify_tx: tokio::sync::mpsc::UnboundedSender<()>,
names: Vec<String>,
) {
tokio::spawn(async move {
use std::collections::HashSet;
const BATCH: usize = 100;
let set: HashSet<String> = names.into_iter().collect();
if set.is_empty() {
return;
}
let mut desc_map: std::collections::HashMap<String, (String, String, String, String)> =
std::collections::HashMap::new(); let all: Vec<String> = set.into_iter().collect();
for chunk in all.chunks(BATCH) {
let args_owned: Vec<String> = std::iter::once("-Si".to_string())
.chain(chunk.iter().cloned())
.collect();
let block = tokio::task::spawn_blocking(move || {
let args_ref: Vec<&str> = args_owned.iter().map(String::as_str).collect();
crate::util::pacman::run_pacman(&args_ref)
})
.await;
let Ok(Ok(out)) = block else { continue };
let mut cur_name: Option<String> = None;
let mut cur_desc: Option<String> = None;
let mut cur_arch: Option<String> = None;
let mut cur_repo: Option<String> = None;
let mut cur_ver: Option<String> = None;
#[allow(clippy::collection_is_never_read)]
let mut _cur_packager: Option<String> = None;
for line in out.lines().chain(std::iter::once("")) {
let line = line.trim_end();
if line.is_empty() {
if let Some(n) = cur_name.take() {
let d = cur_desc.take().unwrap_or_default();
let a = cur_arch.take().unwrap_or_default();
let r = cur_repo.take().unwrap_or_default();
let v = cur_ver.take().unwrap_or_default();
desc_map.insert(n, (d, a, r, v));
}
continue;
}
if let Some((k, v)) = line.split_once(':') {
let key = k.trim();
let val = v.trim();
match key {
"Name" => cur_name = Some(val.to_string()),
"Description" => cur_desc = Some(val.to_string()),
"Architecture" => cur_arch = Some(val.to_string()),
"Repository" => cur_repo = Some(val.to_string()),
"Packager" => _cur_packager = Some(val.to_string()),
"Version" => cur_ver = Some(val.to_string()),
_ => {}
}
}
}
}
if desc_map.is_empty() {
return;
}
if let Ok(mut g) = idx().write() {
for p in &mut g.pkgs {
if let Some((d, a, r, v)) = desc_map.get(&p.name) {
if p.description.is_empty() {
p.description = d.clone();
}
if !a.is_empty() {
p.arch = a.clone();
}
if !r.is_empty() {
p.repo = r.clone();
}
if !v.is_empty() {
p.version = v.clone();
}
}
}
}
save_to_disk(&persist_path);
let _ = notify_tx.send(());
});
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn index_enrich_noop_on_empty_names() {
use std::path::PathBuf;
let mut path: PathBuf = std::env::temp_dir();
path.push(format!(
"pacsea_idx_empty_enrich_{}_{}.json",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
let idx_json = serde_json::json!({ "pkgs": [] });
std::fs::write(
&path,
serde_json::to_string(&idx_json).expect("Failed to serialize test index JSON"),
)
.expect("Failed to write test index file");
crate::index::load_from_disk(&path);
let (notify_tx, mut notify_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
super::request_enrich_for(path.clone(), notify_tx, Vec::new());
let none = tokio::time::timeout(std::time::Duration::from_millis(200), notify_rx.recv())
.await
.ok()
.flatten();
assert!(none.is_none());
let _ = std::fs::remove_file(&path);
}
#[cfg(not(target_os = "windows"))]
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn enrich_updates_fields_and_notifies() {
let _guard = crate::global_test_mutex_lock();
if let Ok(mut g) = crate::index::idx().write() {
g.pkgs = vec![crate::index::OfficialPkg {
name: "foo".to_string(),
repo: String::new(),
arch: String::new(),
version: String::new(),
description: String::new(),
}];
}
let old_path = std::env::var("PATH").unwrap_or_default();
let mut root = std::env::temp_dir();
root.push(format!(
"pacsea_fake_pacman_si_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
std::fs::create_dir_all(&root).expect("Failed to create test root directory");
let mut bin = root.clone();
bin.push("bin");
std::fs::create_dir_all(&bin).expect("Failed to create test bin directory");
let mut script = bin.clone();
script.push("pacman");
let body = r#"#!/usr/bin/env bash
set -e
if [[ "$1" == "-Si" ]]; then
# Print two blocks, one for foo, one unrelated
cat <<EOF
Name : foo
Version : 1.2.3
Architecture : x86_64
Repository : core
Description : hello
Name : other
Version : 9.9.9
Architecture : any
Repository : extra
Description : nope
EOF
exit 0
fi
exit 0
"#;
std::fs::write(&script, body).expect("Failed to write test pacman script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(&script)
.expect("Failed to read test pacman script metadata")
.permissions();
perm.set_mode(0o755);
std::fs::set_permissions(&script, perm)
.expect("Failed to set test pacman script permissions");
}
let new_path = format!("{}:{old_path}", bin.to_string_lossy());
unsafe { std::env::set_var("PATH", &new_path) };
let mut path: std::path::PathBuf = std::env::temp_dir();
path.push("pacsea_enrich_test.json");
crate::index::save_to_disk(&path);
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<()>();
super::request_enrich_for(path.clone(), tx, vec!["foo".into(), "foo".into()]);
let notified = tokio::time::timeout(std::time::Duration::from_millis(500), rx.recv())
.await
.ok()
.flatten()
.is_some();
assert!(notified);
let all = crate::index::all_official();
let pkg = all
.iter()
.find(|p| p.name == "foo")
.expect("package 'foo' should exist in test data");
assert_eq!(pkg.version, "1.2.3");
assert_eq!(pkg.description, "hello");
match &pkg.source {
crate::state::Source::Official { repo, arch } => {
assert_eq!(repo, "core");
assert_eq!(arch, "x86_64");
}
crate::state::Source::Aur => panic!("expected official"),
}
unsafe { std::env::set_var("PATH", &old_path) };
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_dir_all(&root);
}
}