dev_scripts/
testing.rs

1use std::path::PathBuf;
2
3use alpm_buildinfo::cli::ValidateArgs;
4use anyhow::{Context, Result};
5use colored::Colorize;
6use log::{debug, info};
7use rayon::iter::{IntoParallelIterator, ParallelIterator};
8
9use crate::{cli::TestFileType, sync::PackageRepositories, ui::get_progress_bar};
10
11static PKGSRC_DIR: &str = "pkgsrc";
12static PACKAGES_DIR: &str = "packages";
13static DATABASES_DIR: &str = "databases";
14
15/// This is the entry point for running validation tests of parsers on ALPM metadata files.
16pub struct TestRunner {
17    pub test_data_dir: PathBuf,
18    pub file_type: TestFileType,
19    pub repositories: Vec<PackageRepositories>,
20}
21
22impl TestRunner {
23    /// Run validation on all local test files that have been downloaded via the
24    /// `test-files download` command.
25    pub fn run_tests(&self) -> Result<()> {
26        let test_files = self.find_files_of_type().context(format!(
27            "Failed to detect files for type {}",
28            self.file_type
29        ))?;
30        info!(
31            "Found {} {} files for testing",
32            test_files.len(),
33            self.file_type
34        );
35
36        let progress_bar = get_progress_bar(test_files.len() as u64);
37
38        // Run the validate subcommand for all files in parallel.
39        let asserts: Vec<(PathBuf, Result<()>)> = test_files
40            .into_par_iter()
41            .map(|file| {
42                let result = match self.file_type {
43                    TestFileType::BuildInfo => alpm_buildinfo::commands::validate(ValidateArgs {
44                        file: Some(file.clone()),
45                        schema: None,
46                    })
47                    .map_err(|err| err.into()),
48                    TestFileType::SrcInfo => alpm_srcinfo::commands::validate(Some(&file), None)
49                        .map_err(|err| err.into()),
50                    TestFileType::MTree => {
51                        alpm_mtree::commands::validate(Some(&file), None).map_err(|err| err.into())
52                    }
53                    TestFileType::PackageInfo => {
54                        alpm_pkginfo::commands::validate(Some(file.clone()), None)
55                            .map_err(|err| err.into())
56                    }
57                    TestFileType::RemoteDesc => unimplemented!(),
58                    TestFileType::RemoteFiles => unimplemented!(),
59                    TestFileType::LocalDesc => unimplemented!(),
60                    TestFileType::LocalFiles => unimplemented!(),
61                };
62
63                progress_bar.inc(1);
64                (file, result)
65            })
66            .collect();
67
68        // Finish the progress_bar
69        progress_bar.finish_with_message("Validation run finished.");
70
71        // Get all files and the respective error for which validation failed.
72        let failures: Vec<(PathBuf, anyhow::Error)> = asserts
73            .into_iter()
74            .filter_map(|(path, result)| {
75                if let Err(err) = result {
76                    Some((path, err))
77                } else {
78                    None
79                }
80            })
81            .collect();
82
83        if !failures.is_empty() {
84            for (index, failure) in failures.into_iter().enumerate() {
85                let index = format!("[{index}]").bold().red();
86                info!(
87                    "{index} {} with error:\n {}\n",
88                    failure.0.to_string_lossy().bold(),
89                    failure.1
90                );
91            }
92        }
93
94        Ok(())
95    }
96
97    /// Searches the download directory for all files of the given type.
98    ///
99    /// Returns a list of Paths that were found in the process.
100    pub fn find_files_of_type(&self) -> Result<Vec<PathBuf>> {
101        let mut files = Vec::new();
102
103        // First up, determine which folders we should look at while searching for files.
104        let type_folders = match self.file_type {
105            // All package related file types are nested in the subdirectories of the respective
106            // package's package repository.
107            TestFileType::BuildInfo | TestFileType::PackageInfo | TestFileType::MTree => self
108                .repositories
109                .iter()
110                .map(|repo| self.test_data_dir.join(PACKAGES_DIR).join(repo.to_string()))
111                .collect(),
112            TestFileType::SrcInfo => vec![self.test_data_dir.join(PKGSRC_DIR)],
113            // The `desc` and `files` file types are nested in the subdirectories of the respective
114            // package's package repository.
115            TestFileType::RemoteDesc | TestFileType::RemoteFiles => self
116                .repositories
117                .iter()
118                .map(|repo| {
119                    self.test_data_dir
120                        .join(DATABASES_DIR)
121                        .join(repo.to_string())
122                })
123                .collect(),
124            TestFileType::LocalDesc | TestFileType::LocalFiles => {
125                unimplemented!();
126            }
127        };
128
129        for folder in type_folders {
130            debug!("Looking for files in {folder:?}");
131            // Each top-level folder contains a number of sub-folders where each sub-folder
132            // represents a single package. Check if the file we're interested in exists
133            // for said package. If so, add it to the list
134            for pkg_folder in
135                std::fs::read_dir(&folder).context(format!("Failed to read folder {folder:?}"))?
136            {
137                let pkg_folder = pkg_folder?;
138                let file_path = pkg_folder.path().join(self.file_type.to_string());
139                if file_path.exists() {
140                    files.push(file_path);
141                }
142            }
143        }
144
145        Ok(files)
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use std::{
152        collections::HashSet,
153        fs::{OpenOptions, create_dir},
154    };
155
156    use rstest::rstest;
157    use strum::IntoEnumIterator;
158
159    use super::*;
160
161    const PKG_NAMES: &[&str] = &[
162        "pipewire-alsa-1:1.0.7-1-x86_64",
163        "xorg-xvinfo-1.1.5-1-x86_64",
164        "acl-2.3.2-1-x86_64",
165        "archlinux-keyring-20240520-1-any",
166    ];
167
168    /// Ensure that files can be found in case they're nested inside
169    /// sub-subdirectories if the directory structure is:
170    /// `target-dir/packages/${pacman-repo}/${package-name}`
171    #[rstest]
172    #[case(TestFileType::BuildInfo)]
173    #[case(TestFileType::PackageInfo)]
174    #[case(TestFileType::MTree)]
175    fn test_find_files_for_packages(#[case] file_type: TestFileType) -> Result<()> {
176        // Create a temporary directory for testing.
177        let tmp_dir = tempfile::tempdir()?;
178        let packages_dir = tmp_dir.path().join(PACKAGES_DIR);
179        create_dir(&packages_dir)?;
180
181        // The list of files we're expecting to find.
182        let mut expected_files = HashSet::new();
183
184        // Create a test file for each repo.
185        for (index, repo) in PackageRepositories::iter().enumerate() {
186            // Create the repository folder
187            let repo_dir = packages_dir.join(repo.to_string());
188            create_dir(&repo_dir)?;
189
190            // Create a package subfolder inside that repository folder.
191            let pkg = PKG_NAMES[index];
192            let pkg_dir = repo_dir.join(pkg);
193            create_dir(&pkg_dir)?;
194
195            // Touch the file inside the package folder.
196            let file_path = pkg_dir.join(file_type.to_string());
197            OpenOptions::new()
198                .create(true)
199                .write(true)
200                .truncate(true)
201                .open(&file_path)?;
202            expected_files.insert(file_path);
203        }
204
205        // Run the logic to find the files in question.
206        let runner = TestRunner {
207            test_data_dir: tmp_dir.path().to_owned(),
208            file_type,
209            repositories: PackageRepositories::iter().collect(),
210        };
211        let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
212
213        assert_eq!(
214            found_files, expected_files,
215            "Expected that all created package files are also found."
216        );
217
218        Ok(())
219    }
220
221    /// Ensure that files can be found in case they're nested inside
222    /// sub-subdirectories if the directory structure is:
223    /// `target-dir/databases/${pacman-repo}/${package-name}`
224    #[rstest]
225    #[case(TestFileType::RemoteFiles)]
226    #[case(TestFileType::RemoteDesc)]
227    fn test_find_files_for_databases(#[case] file_type: TestFileType) -> Result<()> {
228        // Create a temporary directory for testing.
229        let tmp_dir = tempfile::tempdir()?;
230        let databases_dir = tmp_dir.path().join(DATABASES_DIR);
231        create_dir(&databases_dir)?;
232
233        // The list of files we're expecting to find.
234        let mut expected_files = HashSet::new();
235
236        // Create a test file for each repo.
237        for (index, repo) in PackageRepositories::iter().enumerate() {
238            // Create the repository folder
239            let repo_dir = databases_dir.join(repo.to_string());
240            create_dir(&repo_dir)?;
241
242            // Create a package subfolder inside that repository folder.
243            let pkg = PKG_NAMES[index];
244            let pkg_dir = repo_dir.join(pkg);
245            create_dir(&pkg_dir)?;
246
247            // Touch the file inside the package folder.
248            let file_path = pkg_dir.join(file_type.to_string());
249            OpenOptions::new()
250                .create(true)
251                .write(true)
252                .truncate(true)
253                .open(&file_path)?;
254            expected_files.insert(file_path);
255        }
256
257        // Run the logic to find the files in question.
258        let runner = TestRunner {
259            test_data_dir: tmp_dir.path().to_owned(),
260            file_type,
261            repositories: PackageRepositories::iter().collect(),
262        };
263        let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
264
265        assert_eq!(
266            found_files, expected_files,
267            "Expected that all created databases files are also found."
268        );
269
270        Ok(())
271    }
272
273    /// Ensure that files can be found in case they're nested inside
274    /// sub-subdirectories if the directory structure is:
275    /// `target-dir/pkgsrc/${package-name}`
276    #[rstest]
277    #[case(TestFileType::SrcInfo)]
278    fn test_find_files_for_pkgsrc(#[case] file_type: TestFileType) -> Result<()> {
279        // Create a temporary directory for testing.
280        let tmp_dir = tempfile::tempdir()?;
281        let pkgsrc_dir = tmp_dir.path().join(PKGSRC_DIR);
282        create_dir(&pkgsrc_dir)?;
283
284        // The list of files we're expecting to find.
285        let mut expected_files = HashSet::new();
286
287        // Create one subdirectory for each package name.
288        // Then create the file in question for that package.
289        for pkg in PKG_NAMES {
290            let pkg_dir = pkgsrc_dir.join(pkg);
291            create_dir(&pkg_dir)?;
292
293            // Touch the file inside the package folder.
294            let file_path = pkg_dir.join(file_type.to_string());
295            OpenOptions::new()
296                .create(true)
297                .write(true)
298                .truncate(true)
299                .open(&file_path)?;
300            expected_files.insert(file_path);
301        }
302
303        // Run the logic to find the files in question.
304        let runner = TestRunner {
305            test_data_dir: tmp_dir.path().to_owned(),
306            file_type,
307            repositories: PackageRepositories::iter().collect(),
308        };
309        let found_files = HashSet::from_iter(runner.find_files_of_type()?.into_iter());
310
311        assert_eq!(
312            found_files, expected_files,
313            "Expected that all created pkgsrc files are also found."
314        );
315
316        Ok(())
317    }
318}