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