1use crate::state::{PackageItem, Source};
4use crate::util::{percent_encode, s};
5
6pub async fn fetch_all_with_errors(query: String) -> (Vec<PackageItem>, Vec<String>) {
17 let q = percent_encode(query.trim());
18 let aur_url = format!("https://aur.archlinux.org/rpc/v5/search?by=name&arg={q}");
19
20 let mut items: Vec<PackageItem> = Vec::new();
21
22 let ret = tokio::task::spawn_blocking(move || crate::util::curl::curl_json(&aur_url)).await;
23 let mut errors = Vec::new();
24 match ret {
25 Ok(Ok(resp)) => {
26 if let Some(arr) = resp.get("results").and_then(|v| v.as_array()) {
27 for pkg in arr.iter().take(200) {
28 let name = s(pkg, "Name");
29 let version = s(pkg, "Version");
30 let description = s(pkg, "Description");
31 let popularity = pkg.get("Popularity").and_then(serde_json::Value::as_f64);
32 if name.is_empty() {
33 continue;
34 }
35 let out_of_date = pkg
37 .get("OutOfDate")
38 .and_then(serde_json::Value::as_i64)
39 .and_then(|ts| u64::try_from(ts).ok())
40 .filter(|&ts| ts > 0);
41 let maintainer = s(pkg, "Maintainer");
43 let orphaned = maintainer.is_empty();
44 items.push(PackageItem {
45 name,
46 version,
47 description,
48 source: Source::Aur,
49 popularity,
50 out_of_date,
51 orphaned,
52 });
53 }
54 }
55 }
56 Ok(Err(e)) => errors.push(format!("AUR search unavailable: {e}")),
57 Err(e) => errors.push(format!("AUR search failed: {e}")),
58 }
59
60 (items, errors)
61}
62
63#[cfg(not(target_os = "windows"))]
64#[cfg(test)]
65mod tests {
66 #[tokio::test]
67 #[allow(clippy::await_holding_lock, clippy::all)] async fn search_returns_items_on_success_and_error_on_failure() {
69 let _guard = crate::global_test_mutex_lock();
70 let old_path = std::env::var("PATH").unwrap_or_default();
72 let mut root = std::env::temp_dir();
73 root.push(format!(
74 "pacsea_fake_curl_search_{}_{}",
75 std::process::id(),
76 std::time::SystemTime::now()
77 .duration_since(std::time::UNIX_EPOCH)
78 .expect("System time is before UNIX epoch")
79 .as_nanos()
80 ));
81 std::fs::create_dir_all(&root).expect("failed to create test root directory");
82 let mut bin = root.clone();
83 bin.push("bin");
84 std::fs::create_dir_all(&bin).expect("failed to create test bin directory");
85 let mut curl = bin.clone();
86 curl.push("curl");
87 #[allow(clippy::all, clippy::literal_string_with_formatting_args)]
89 let script = r#"#!/bin/sh
90set -e
91state_dir="${PACSEA_FAKE_STATE_DIR:-.}"
92if [ ! -f "$state_dir/pacsea_search_called" ]; then
93 : > "$state_dir/pacsea_search_called"
94 echo '{"results":[{"Name":"yay","Version":"12","Description":"AUR helper","Popularity":3.14,"OutOfDate":null,"Maintainer":"someuser"}]}'
95else
96 exit 22
97fi
98"#;
99 std::fs::write(&curl, script.as_bytes()).expect("failed to write test curl script");
100 #[cfg(unix)]
101 {
102 use std::os::unix::fs::PermissionsExt;
103 let mut perm = std::fs::metadata(&curl)
104 .expect("failed to read test curl script metadata")
105 .permissions();
106 perm.set_mode(0o755);
107 std::fs::set_permissions(&curl, perm)
108 .expect("failed to set test curl script permissions");
109 }
110 let new_path = format!("{}:{old_path}", bin.to_string_lossy());
111 unsafe {
112 std::env::set_var("PATH", &new_path);
113 std::env::set_var("PACSEA_FAKE_STATE_DIR", bin.to_string_lossy().to_string());
114 std::env::set_var("PACSEA_CURL_PATH", "1");
116 }
117 std::thread::sleep(std::time::Duration::from_millis(10));
119
120 let (items, errs) = super::fetch_all_with_errors("yay".into()).await;
121 assert_eq!(
122 items.len(),
123 1,
124 "Expected 1 item, got {} items. Errors: {:?}",
125 items.len(),
126 errs
127 );
128 assert!(errs.is_empty());
129 assert_eq!(items[0].out_of_date, None);
131 assert!(!items[0].orphaned);
132
133 let (_items2, errs2) = super::fetch_all_with_errors("yay".into()).await;
135 assert!(!errs2.is_empty());
136
137 unsafe {
138 std::env::set_var("PATH", &old_path);
139 std::env::remove_var("PACSEA_CURL_PATH");
140 }
141 let _ = std::fs::remove_dir_all(&root);
142 }
143}