dev_scripts/
testing.rs

1//! Tests against downloaded artifacts.
2
3use 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/// This is the entry point for running validation tests of parsers on ALPM metadata files.
18#[derive(Clone, Debug)]
19pub struct TestRunner {
20    /// The directory in which test data is stored.
21    pub test_data_dir: PathBuf,
22    /// The type of file that is targeted in the test.
23    pub file_type: TestFileType,
24    /// The list of repositories against which the test runs.
25    pub repositories: Vec<PackageRepositories>,
26}
27
28impl TestRunner {
29    /// Run validation on all local test files that have been downloaded via the
30    /// `test-files download` command.
31    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        // Run the validate subcommand for all files in parallel.
45        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        // Finish the progress_bar
75        progress_bar.finish_with_message("Validation run finished.");
76
77        // Get all files and the respective error for which validation failed.
78        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    /// Searches the download directory for all files of the given type.
104    ///
105    /// Returns a list of Paths that were found in the process.
106    pub fn find_files_of_type(&self) -> Result<Vec<PathBuf>> {
107        let mut files = Vec::new();
108
109        // First up, determine which folders we should look at while searching for files.
110        let type_folders = match self.file_type {
111            // All package related file types are nested in the subdirectories of the respective
112            // package's package repository.
113            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            // The `desc` and `files` file types are nested in the subdirectories of the respective
120            // package's package repository.
121            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            // Each top-level folder contains a number of sub-folders where each sub-folder
138            // represents a single package. Check if the file we're interested in exists
139            // for said package. If so, add it to the list
140            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    /// Ensure that files can be found in case they're nested inside
175    /// sub-subdirectories if the directory structure is:
176    /// `target-dir/packages/${pacman-repo}/${package-name}`
177    #[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        // Create a temporary directory for testing.
183        let tmp_dir = tempfile::tempdir()?;
184        let packages_dir = tmp_dir.path().join(PACKAGES_DIR);
185        create_dir(&packages_dir)?;
186
187        // The list of files we're expecting to find.
188        let mut expected_files = HashSet::new();
189
190        // Create a test file for each repo.
191        for (index, repo) in PackageRepositories::iter().enumerate() {
192            // Create the repository folder
193            let repo_dir = packages_dir.join(repo.to_string());
194            create_dir(&repo_dir)?;
195
196            // Create a package subfolder inside that repository folder.
197            let pkg = PKG_NAMES[index];
198            let pkg_dir = repo_dir.join(pkg);
199            create_dir(&pkg_dir)?;
200
201            // Touch the file inside the package folder.
202            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        // Run the logic to find the files in question.
212        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    /// Ensure that files can be found in case they're nested inside
228    /// sub-subdirectories if the directory structure is:
229    /// `target-dir/databases/${pacman-repo}/${package-name}`
230    #[rstest]
231    #[case(TestFileType::RemoteFiles)]
232    #[case(TestFileType::RemoteDesc)]
233    fn test_find_files_for_databases(#[case] file_type: TestFileType) -> Result<()> {
234        // Create a temporary directory for testing.
235        let tmp_dir = tempfile::tempdir()?;
236        let databases_dir = tmp_dir.path().join(DATABASES_DIR);
237        create_dir(&databases_dir)?;
238
239        // The list of files we're expecting to find.
240        let mut expected_files = HashSet::new();
241
242        // Create a test file for each repo.
243        for (index, repo) in PackageRepositories::iter().enumerate() {
244            // Create the repository folder
245            let repo_dir = databases_dir.join(repo.to_string());
246            create_dir(&repo_dir)?;
247
248            // Create a package subfolder inside that repository folder.
249            let pkg = PKG_NAMES[index];
250            let pkg_dir = repo_dir.join(pkg);
251            create_dir(&pkg_dir)?;
252
253            // Touch the file inside the package folder.
254            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        // Run the logic to find the files in question.
264        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    /// Ensure that files can be found in case they're nested inside
280    /// sub-subdirectories if the directory structure is:
281    /// `target-dir/pkgsrc/${package-name}`
282    #[rstest]
283    #[case(TestFileType::SrcInfo)]
284    fn test_find_files_for_pkgsrc(#[case] file_type: TestFileType) -> Result<()> {
285        // Create a temporary directory for testing.
286        let tmp_dir = tempfile::tempdir()?;
287        let pkgsrc_dir = tmp_dir.path().join(PKGSRC_DIR);
288        create_dir(&pkgsrc_dir)?;
289
290        // The list of files we're expecting to find.
291        let mut expected_files = HashSet::new();
292
293        // Create one subdirectory for each package name.
294        // Then create the file in question for that package.
295        for pkg in PKG_NAMES {
296            let pkg_dir = pkgsrc_dir.join(pkg);
297            create_dir(&pkg_dir)?;
298
299            // Touch the file inside the package folder.
300            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        // Run the logic to find the files in question.
310        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}