1use std::path::PathBuf;
4
5use alpm_buildinfo::cli::ValidateArgs;
6use anyhow::{Context, Result};
7use colored::Colorize;
8use log::{debug, info};
9use rayon::iter::{IntoParallelIterator, ParallelIterator};
10
11use crate::{cli::TestFileType, sync::PackageRepositories, ui::get_progress_bar};
12
13static PKGSRC_DIR: &str = "pkgsrc";
14static PACKAGES_DIR: &str = "packages";
15static DATABASES_DIR: &str = "databases";
16
17#[derive(Clone, Debug)]
19pub struct TestRunner {
20 pub test_data_dir: PathBuf,
22 pub file_type: TestFileType,
24 pub repositories: Vec<PackageRepositories>,
26}
27
28impl TestRunner {
29 pub fn run_tests(&self) -> Result<()> {
32 let test_files = self.find_files_of_type().context(format!(
33 "Failed to detect files for type {}",
34 self.file_type
35 ))?;
36 info!(
37 "Found {} {} files for testing",
38 test_files.len(),
39 self.file_type
40 );
41
42 let progress_bar = get_progress_bar(test_files.len() as u64);
43
44 let asserts: Vec<(PathBuf, Result<()>)> = test_files
46 .into_par_iter()
47 .map(|file| {
48 let result = match self.file_type {
49 TestFileType::BuildInfo => alpm_buildinfo::commands::validate(ValidateArgs {
50 file: Some(file.clone()),
51 schema: None,
52 })
53 .map_err(|err| err.into()),
54 TestFileType::SrcInfo => alpm_srcinfo::commands::validate(Some(&file), None)
55 .map_err(|err| err.into()),
56 TestFileType::MTree => {
57 alpm_mtree::commands::validate(Some(&file), None).map_err(|err| err.into())
58 }
59 TestFileType::PackageInfo => {
60 alpm_pkginfo::commands::validate(Some(file.clone()), None)
61 .map_err(|err| err.into())
62 }
63 TestFileType::RemoteDesc => unimplemented!(),
64 TestFileType::RemoteFiles => unimplemented!(),
65 TestFileType::LocalDesc => unimplemented!(),
66 TestFileType::LocalFiles => unimplemented!(),
67 };
68
69 progress_bar.inc(1);
70 (file, result)
71 })
72 .collect();
73
74 progress_bar.finish_with_message("Validation run finished.");
76
77 let failures: Vec<(PathBuf, anyhow::Error)> = asserts
79 .into_iter()
80 .filter_map(|(path, result)| {
81 if let Err(err) = result {
82 Some((path, err))
83 } else {
84 None
85 }
86 })
87 .collect();
88
89 if !failures.is_empty() {
90 for (index, failure) in failures.into_iter().enumerate() {
91 let index = format!("[{index}]").bold().red();
92 info!(
93 "{index} {} with error:\n {}\n",
94 failure.0.to_string_lossy().bold(),
95 failure.1
96 );
97 }
98 }
99
100 Ok(())
101 }
102
103 pub fn find_files_of_type(&self) -> Result<Vec<PathBuf>> {
107 let mut files = Vec::new();
108
109 let type_folders = match self.file_type {
111 TestFileType::BuildInfo | TestFileType::PackageInfo | TestFileType::MTree => self
114 .repositories
115 .iter()
116 .map(|repo| self.test_data_dir.join(PACKAGES_DIR).join(repo.to_string()))
117 .collect(),
118 TestFileType::SrcInfo => vec![self.test_data_dir.join(PKGSRC_DIR)],
119 TestFileType::RemoteDesc | TestFileType::RemoteFiles => self
122 .repositories
123 .iter()
124 .map(|repo| {
125 self.test_data_dir
126 .join(DATABASES_DIR)
127 .join(repo.to_string())
128 })
129 .collect(),
130 TestFileType::LocalDesc | TestFileType::LocalFiles => {
131 unimplemented!();
132 }
133 };
134
135 for folder in type_folders {
136 debug!("Looking for files in {folder:?}");
137 for pkg_folder in
141 std::fs::read_dir(&folder).context(format!("Failed to read folder {folder:?}"))?
142 {
143 let pkg_folder = pkg_folder?;
144 let file_path = pkg_folder.path().join(self.file_type.to_string());
145 if file_path.exists() {
146 files.push(file_path);
147 }
148 }
149 }
150
151 Ok(files)
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use std::{
158 collections::HashSet,
159 fs::{OpenOptions, create_dir},
160 };
161
162 use rstest::rstest;
163 use strum::IntoEnumIterator;
164
165 use super::*;
166
167 const PKG_NAMES: &[&str] = &[
168 "pipewire-alsa-1:1.0.7-1-x86_64",
169 "xorg-xvinfo-1.1.5-1-x86_64",
170 "acl-2.3.2-1-x86_64",
171 "archlinux-keyring-20240520-1-any",
172 ];
173
174 #[rstest]
178 #[case(TestFileType::BuildInfo)]
179 #[case(TestFileType::PackageInfo)]
180 #[case(TestFileType::MTree)]
181 fn test_find_files_for_packages(#[case] file_type: TestFileType) -> Result<()> {
182 let tmp_dir = tempfile::tempdir()?;
184 let packages_dir = tmp_dir.path().join(PACKAGES_DIR);
185 create_dir(&packages_dir)?;
186
187 let mut expected_files = HashSet::new();
189
190 for (index, repo) in PackageRepositories::iter().enumerate() {
192 let repo_dir = packages_dir.join(repo.to_string());
194 create_dir(&repo_dir)?;
195
196 let pkg = PKG_NAMES[index];
198 let pkg_dir = repo_dir.join(pkg);
199 create_dir(&pkg_dir)?;
200
201 let file_path = pkg_dir.join(file_type.to_string());
203 OpenOptions::new()
204 .create(true)
205 .write(true)
206 .truncate(true)
207 .open(&file_path)?;
208 expected_files.insert(file_path);
209 }
210
211 let runner = TestRunner {
213 test_data_dir: tmp_dir.path().to_owned(),
214 file_type,
215 repositories: PackageRepositories::iter().collect(),
216 };
217 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
218
219 assert_eq!(
220 found_files, expected_files,
221 "Expected that all created package files are also found."
222 );
223
224 Ok(())
225 }
226
227 #[rstest]
231 #[case(TestFileType::RemoteFiles)]
232 #[case(TestFileType::RemoteDesc)]
233 fn test_find_files_for_databases(#[case] file_type: TestFileType) -> Result<()> {
234 let tmp_dir = tempfile::tempdir()?;
236 let databases_dir = tmp_dir.path().join(DATABASES_DIR);
237 create_dir(&databases_dir)?;
238
239 let mut expected_files = HashSet::new();
241
242 for (index, repo) in PackageRepositories::iter().enumerate() {
244 let repo_dir = databases_dir.join(repo.to_string());
246 create_dir(&repo_dir)?;
247
248 let pkg = PKG_NAMES[index];
250 let pkg_dir = repo_dir.join(pkg);
251 create_dir(&pkg_dir)?;
252
253 let file_path = pkg_dir.join(file_type.to_string());
255 OpenOptions::new()
256 .create(true)
257 .write(true)
258 .truncate(true)
259 .open(&file_path)?;
260 expected_files.insert(file_path);
261 }
262
263 let runner = TestRunner {
265 test_data_dir: tmp_dir.path().to_owned(),
266 file_type,
267 repositories: PackageRepositories::iter().collect(),
268 };
269 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
270
271 assert_eq!(
272 found_files, expected_files,
273 "Expected that all created databases files are also found."
274 );
275
276 Ok(())
277 }
278
279 #[rstest]
283 #[case(TestFileType::SrcInfo)]
284 fn test_find_files_for_pkgsrc(#[case] file_type: TestFileType) -> Result<()> {
285 let tmp_dir = tempfile::tempdir()?;
287 let pkgsrc_dir = tmp_dir.path().join(PKGSRC_DIR);
288 create_dir(&pkgsrc_dir)?;
289
290 let mut expected_files = HashSet::new();
292
293 for pkg in PKG_NAMES {
296 let pkg_dir = pkgsrc_dir.join(pkg);
297 create_dir(&pkg_dir)?;
298
299 let file_path = pkg_dir.join(file_type.to_string());
301 OpenOptions::new()
302 .create(true)
303 .write(true)
304 .truncate(true)
305 .open(&file_path)?;
306 expected_files.insert(file_path);
307 }
308
309 let runner = TestRunner {
311 test_data_dir: tmp_dir.path().to_owned(),
312 file_type,
313 repositories: PackageRepositories::iter().collect(),
314 };
315 let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
316
317 assert_eq!(
318 found_files, expected_files,
319 "Expected that all created pkgsrc files are also found."
320 );
321
322 Ok(())
323 }
324}