Skip to main content

diskann_benchmark_runner/
checker.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use std::{
7    collections::HashSet,
8    path::{Path, PathBuf},
9};
10
11/// Shared context for resolving input and output files paths post deserialization.
12#[derive(Debug)]
13pub struct Checker {
14    /// Root directories in which to look for files.
15    ///
16    /// Loading input files will first look to see if the input file is an absolute path.
17    /// If so, the absolute path will be used.
18    ///
19    /// Otherwise, the search directories are traversed from beginning to end.
20    search_directories: Vec<PathBuf>,
21
22    /// Root directory (only one permitted) to write output files into
23    /// and check for output files
24    output_directory: Option<PathBuf>,
25
26    /// The collection of output directories registered so far with the checker.
27    ///
28    /// This ensures that each job uses a distinct output directory to avoid conflicts.
29    current_outputs: HashSet<PathBuf>,
30}
31
32impl Checker {
33    /// Create a new checker with the list of search directories..
34    pub(crate) fn new(search_directories: Vec<PathBuf>, output_directory: Option<PathBuf>) -> Self {
35        Self {
36            search_directories,
37            output_directory,
38            current_outputs: HashSet::new(),
39        }
40    }
41
42    /// Return the ordered list of search directories registered with the [`Checker`].
43    pub fn search_directories(&self) -> &[PathBuf] {
44        &self.search_directories
45    }
46
47    /// Return the output directory registered with the [`Checker`], if any.
48    pub fn output_directory(&self) -> Option<&PathBuf> {
49        self.output_directory.as_ref()
50    }
51
52    /// Register `save_path` as an output directory and resolve `save_path` to an absolute path.
53    ///
54    /// # NOTE
55    ///
56    /// The behavior of this function is expected to change in the near future.
57    pub fn register_output(&mut self, save_path: Option<&Path>) -> anyhow::Result<PathBuf> {
58        // Check if `save_path` is absolute or relative. If relative, resolve it to an absolute
59        // path using `self.output_directory.
60        let resolved_dir = match save_path {
61            None => {
62                if let Some(output_dir) = self.output_directory() {
63                    output_dir.clone()
64                } else {
65                    return Err(anyhow::Error::msg(
66                        "relative save path \"{}\" specified but no output directory was provided",
67                    ));
68                }
69            }
70            Some(save_path) => {
71                if save_path.is_absolute() {
72                    if !(save_path.is_dir()) {
73                        return Err(anyhow::Error::msg(format!(
74                            "absolute save path \"{}\" is not a valid directory",
75                            save_path.display()
76                        )));
77                    }
78                    save_path.to_path_buf()
79                } else {
80                    // relative path, we concatenate it with the output directory
81                    if let Some(output_dir) = self.output_directory() {
82                        let absolute = output_dir.join(save_path);
83                        if !absolute.is_dir() {
84                            return Err(anyhow::Error::msg(format!(
85                                "relative save path \"{}\" is not a valid directory when combined with output directory \"{}\"",
86                                save_path.display(),
87                                output_dir.display()
88                            )));
89                        }
90                        absolute
91                    } else {
92                        return Err(anyhow::Error::msg(format!(
93                            "relative save path \"{}\" specified but no output directory was provided",
94                            save_path.display()
95                        )));
96                    }
97                }
98            }
99        };
100
101        // If the resolved directory already exists - bail.
102        if !self.current_outputs.insert(resolved_dir.clone()) {
103            anyhow::bail!(
104                "output directory {} already being used by another job",
105                resolved_dir.display()
106            );
107        } else {
108            Ok(resolved_dir)
109        }
110    }
111
112    /// Try to resolve `path` using the following approach:
113    ///
114    /// 1. If `path` is absolute - check that it exists and is a valid file. If
115    ///    successful, return `path` unaltered.
116    ///
117    /// 2. If `path` is relative, work through `self.search_directories()` in order,
118    ///    returning the absolute path first existing file.
119    #[deprecated(since = "0.54.0", note = "please use `find_input_file` instead")]
120    pub fn check_path(&self, path: &Path) -> Result<PathBuf, anyhow::Error> {
121        self.find_input_file(path)
122    }
123
124    /// Try to resolve `path` as a file using the following approach:
125    ///
126    /// 1. If `path` is absolute - check that it exists and is a valid file. If
127    ///    successful, return `path` unaltered.
128    ///
129    /// 2. If `path` is relative, work through `self.search_directories()` in order,
130    ///    returning the absolute path first existing file.
131    ///
132    /// See also: [`Self::find_input_dir`].
133    pub fn find_input_file(&self, path: &Path) -> Result<PathBuf, anyhow::Error> {
134        self.check_input_path(path, Kind::File)
135    }
136
137    /// Try to resolve `path` as a directory using the following approach:
138    ///
139    /// 1. If `path` is absolute - check that it exists and is a valid directory. If
140    ///    successful, return `path` unaltered.
141    ///
142    /// 2. If `path` is relative, work through `self.search_directories()` in order,
143    ///    returning the absolute path first existing directory.
144    ///
145    /// See also: [`Self::find_input_file`].
146    pub fn find_input_dir(&self, path: &Path) -> Result<PathBuf, anyhow::Error> {
147        self.check_input_path(path, Kind::Dir)
148    }
149
150    fn check_input_path(&self, path: &Path, kind: Kind) -> Result<PathBuf, anyhow::Error> {
151        // Check if the path exists (allowing for relative paths with respect to checker's
152        // search directories).
153        //
154        // If the path is absolute - check if it exists and if it doesn't, we are done.
155        if path.is_absolute() {
156            if kind.check(path) {
157                return Ok(path.into());
158            } else {
159                let kind = kind.as_str();
160                return Err(anyhow::Error::msg(format!(
161                    "input {} with absolute path \"{}\" either does not exist or is not a {}",
162                    kind,
163                    path.display(),
164                    kind,
165                )));
166            }
167        };
168
169        // At this point, start searching in the provided directories.
170        for dir in self.search_directories() {
171            let absolute = dir.join(path);
172            if kind.check(&absolute) {
173                return Ok(absolute);
174            }
175        }
176
177        Err(anyhow::Error::msg(format!(
178            "could not find input {} \"{}\" in the search directories \"{:?}\"",
179            kind.as_str(),
180            path.display(),
181            self.search_directories(),
182        )))
183    }
184}
185
186#[derive(Debug, Clone, Copy)]
187enum Kind {
188    File,
189    Dir,
190}
191
192impl Kind {
193    fn check(self, path: &Path) -> bool {
194        match self {
195            Self::File => path.is_file(),
196            Self::Dir => path.is_dir(),
197        }
198    }
199
200    fn as_str(self) -> &'static str {
201        match self {
202            Self::File => "file",
203            Self::Dir => "directory",
204        }
205    }
206}
207
208///////////
209// Tests //
210///////////
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    use std::fs::{create_dir, File};
217
218    #[test]
219    fn test_constructor() {
220        let checker = Checker::new(Vec::new(), None);
221        assert!(checker.search_directories().is_empty());
222        assert!(checker.output_directory().is_none());
223
224        let dir_a: PathBuf = "directory/a".into();
225        let dir_b: PathBuf = "directory/another/b".into();
226
227        let checker = Checker::new(vec![dir_a.clone()], Some(dir_b.clone()));
228        assert_eq!(checker.search_directories(), vec![dir_a.clone()]);
229        assert_eq!(checker.output_directory(), Some(&dir_b));
230
231        let checker = Checker::new(vec![dir_a.clone(), dir_b.clone()], None);
232        assert_eq!(
233            checker.search_directories(),
234            vec![dir_a.clone(), dir_b.clone()]
235        );
236        assert!(checker.output_directory().is_none());
237    }
238
239    // Create a directory that looks like this:
240    //
241    // dir/
242    //     file_a.txt
243    //     dir0/
244    //        file_b.txt
245    //        dir2/
246    //     dir1/
247    //        file_c.txt
248    //        dir0/
249    //           file_c.txt
250    fn create_test_directory(dir: &Path) {
251        File::create(dir.join("file_a.txt")).unwrap();
252
253        create_dir(dir.join("dir0")).unwrap();
254        create_dir(dir.join("dir0/dir2")).unwrap();
255        create_dir(dir.join("dir1")).unwrap();
256        create_dir(dir.join("dir1/dir0")).unwrap();
257        File::create(dir.join("dir0/file_b.txt")).unwrap();
258        File::create(dir.join("dir1/file_c.txt")).unwrap();
259        File::create(dir.join("dir1/dir0/file_c.txt")).unwrap();
260    }
261
262    #[test]
263    fn test_find_input_file() {
264        let dir = tempfile::tempdir().unwrap();
265        let path = dir.path();
266        create_test_directory(path);
267
268        let make_checker = |paths: &[PathBuf]| -> Checker { Checker::new(paths.to_vec(), None) };
269
270        // Test absolute path success.
271        {
272            let checker = make_checker(&[]);
273            let absolute = path.join("file_a.txt");
274            assert_eq!(
275                checker.find_input_file(&absolute).unwrap(),
276                absolute,
277                "absolute paths should be unmodified if they exist",
278            );
279
280            let absolute = path.join("dir0/file_b.txt");
281            assert_eq!(
282                checker.find_input_file(&absolute).unwrap(),
283                absolute,
284                "absolute paths should be unmodified if they exist",
285            );
286        }
287
288        // Absolute path fail.
289        {
290            let checker = make_checker(&[]);
291            let absolute = path.join("dir0/file_c.txt");
292            let err = checker.find_input_file(&absolute).unwrap_err();
293            let message = err.to_string();
294            assert!(message.contains("input file with absolute path"));
295            assert!(message.contains("either does not exist or is not a file"));
296        }
297
298        // Directory search
299        {
300            let checker =
301                make_checker(&[path.join("dir1/dir0"), path.join("dir1"), path.join("dir0")]);
302
303            // Directories are searched in order.
304            let file = &Path::new("file_c.txt");
305            let resolved = checker.find_input_file(file).unwrap();
306            assert_eq!(resolved, path.join("dir1/dir0/file_c.txt"));
307
308            let file = &Path::new("file_b.txt");
309            let resolved = checker.find_input_file(file).unwrap();
310            assert_eq!(resolved, path.join("dir0/file_b.txt"));
311
312            // Directory search can fail.
313            let file = &Path::new("file_a.txt");
314            let err = checker.find_input_file(file).unwrap_err();
315            let message = err.to_string();
316            assert!(message.contains("could not find input file"));
317            assert!(message.contains("in the search directories"));
318
319            // If we give an absolute path, no directory search is performed.
320            let file = path.join("file_c.txt");
321            let err = checker.find_input_file(&file).unwrap_err();
322            let message = err.to_string();
323            assert!(message.starts_with("input file with absolute path"));
324        }
325    }
326
327    #[test]
328    fn test_find_input_dir() {
329        let dir = tempfile::tempdir().unwrap();
330        let path = dir.path();
331        create_test_directory(path);
332
333        let make_checker = |paths: &[PathBuf]| -> Checker { Checker::new(paths.to_vec(), None) };
334
335        // Test absolute path success.
336        {
337            let checker = make_checker(&[]);
338            let absolute = path.join("dir0");
339            assert_eq!(
340                checker.find_input_dir(&absolute).unwrap(),
341                absolute,
342                "absolute paths should be unmodified if they exist",
343            );
344
345            let absolute = path.join("dir1/dir0");
346            assert_eq!(
347                checker.find_input_dir(&absolute).unwrap(),
348                absolute,
349                "absolute paths should be unmodified if they exist",
350            );
351        }
352
353        // Absolute path fail.
354        {
355            let checker = make_checker(&[]);
356            let absolute = path.join("dir1/dir1");
357            let err = checker.find_input_dir(&absolute).unwrap_err();
358            let message = err.to_string();
359            assert!(message.contains("input directory with absolute path"));
360            assert!(message.contains("either does not exist or is not a directory"));
361
362            // Files are rejected.
363            let absolute = path.join("file_a.txt");
364            let err = checker.find_input_dir(&absolute).unwrap_err();
365            let message = err.to_string();
366            assert!(message.contains("input directory with absolute path"));
367            assert!(message.contains("either does not exist or is not a directory"));
368        }
369
370        // Directory search
371        {
372            let checker =
373                make_checker(&[path.join("dir1/dir0"), path.join("dir1"), path.join("dir0")]);
374
375            // Directories are searched in order.
376            let dir = &Path::new("dir0");
377            let resolved = checker.find_input_dir(dir).unwrap();
378            assert_eq!(resolved, path.join("dir1/dir0"));
379
380            let dir = &Path::new("dir2");
381            let resolved = checker.find_input_dir(dir).unwrap();
382            assert_eq!(resolved, path.join("dir0/dir2"));
383
384            // Directory search can fail.
385            let dir = &Path::new("nope");
386            let err = checker.find_input_dir(dir).unwrap_err();
387            let message = err.to_string();
388            assert!(message.contains("could not find input directory"));
389            assert!(message.contains("in the search directories"));
390
391            // If we give an absolute path, no directory search is performed.
392            let dir = path.join("dir2");
393            let err = checker.find_input_dir(&dir).unwrap_err();
394            let message = err.to_string();
395            assert!(message.starts_with("input directory with absolute path"));
396        }
397    }
398}