Skip to main content

diskann_benchmark_runner/
app.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! The CLI frontend for benchmark applications built with this crate.
7//!
8//! [`App`] provides a [`clap`]-based command line interface that handles input parsing,
9//! benchmark dispatch, and regression checking. Consumers build a binary by registering
10//! [`Input`](crate::Input)s and [`Benchmark`](crate::Benchmark)s, then forwarding to
11//! [`App::parse`] and [`App::run`].
12//!
13//! # Subcommands
14//!
15//! ## Standard Workflow
16//!
17//! * `inputs [NAME]`: List available input kinds, or describe one by name.
18//! * `benchmarks`: List registered benchmarks and their descriptions.
19//! * `skeleton`: Print a skeleton input JSON file.
20//! * `run --input-file <FILE> --output-file <FILE> [--dry-run]`: Run benchmarks.
21//!
22//! ## Regression Checks
23//!
24//! These are accessed via `check <SUBCOMMAND>`:
25//!
26//! * `check skeleton`: Print a skeleton tolerance JSON file.
27//! * `check tolerances [NAME]`: List tolerance kinds, or describe one by name.
28//! * `check verify --tolerances <FILE> --input-file <FILE>`: Validate a tolerance file
29//!   against an input file.
30//! * `check run --tolerances <FILE> --input-file <FILE> --before <FILE> --after <FILE> [--output-file <FILE>]`:
31//!   Run regression checks.
32//!
33//! # Example
34//!
35//! A typical binary using this crate:
36//!
37//! ```rust,no_run
38//! use diskann_benchmark_runner::{App, registry};
39//!
40//! fn main() -> anyhow::Result<()> {
41//!     let mut registry = registry::Registry::new();
42//!     // registry.register("my-bench", MyBenchmark::default())?;
43//!     // registry.register_regression("my-regression", MyRegressionBenchmark::default())?;
44//!
45//!     let app = App::parse();
46//!     let mut output = diskann_benchmark_runner::output::default();
47//!     app.run(&registry, &mut output)
48//! }
49//! ```
50//!
51//! # Regression Workflow
52//!
53//! 1. Run benchmarks twice (e.g. before and after a code change) with `run`, producing
54//!    two output files.
55//! 2. Author a tolerance file describing acceptable variation (use `check skeleton` and
56//!    `check tolerances` for guidance).
57//! 3. Validate the tolerance file with `check verify`.
58//! 4. Compare the two output files with `check run`.
59
60use std::{io::Write, path::PathBuf};
61
62use clap::{Parser, Subcommand};
63
64use crate::{
65    internal::{self, visibility::Filter as VisibilityFilter},
66    jobs::{self, Jobs},
67    output::Output,
68    registry,
69    result::Checkpoint,
70    utils::fmt::{Banner, Indent},
71};
72
73/// Check if we're running in debug mode and error if not allowed.
74fn check_debug_mode(allow_debug: bool) -> anyhow::Result<()> {
75    // Unit tests are treated as debug mode to ensure consistent behavior across builds.
76    if cfg!(any(test, debug_assertions)) && !allow_debug {
77        anyhow::bail!(
78            "Benchmarking in debug mode produces misleading performance results.\n\
79             Please compile in release mode or use the --allow-debug flag to bypass this check."
80        );
81    }
82    Ok(())
83}
84
85/// Parsed command line options.
86#[derive(Debug, Subcommand)]
87pub enum Commands {
88    /// List the kinds of input formats available for ingestion.
89    Inputs {
90        /// Describe the layout of the named input kind.
91        #[arg(conflicts_with = "all")]
92        describe: Option<String>,
93
94        /// Print all inputs, including those that are feature gated.
95        #[arg(long, conflicts_with = "describe")]
96        all: bool,
97    },
98    /// List the available benchmarks.
99    Benchmarks {
100        /// Print all benchmarks, including those that are feature gated.
101        #[arg(long)]
102        all: bool,
103    },
104    /// Provide a skeleton JSON file for running a set of benchmarks.
105    Skeleton,
106    /// Run a list of benchmarks.
107    Run {
108        /// The input file to run.
109        #[arg(long = "input-file")]
110        input_file: PathBuf,
111        /// The path where the output file should reside.
112        #[arg(long = "output-file")]
113        output_file: PathBuf,
114        /// Parse an input file and perform all validation checks, but don't actually run any
115        /// benchmarks.
116        #[arg(long, action)]
117        dry_run: bool,
118        /// Allow running benchmarks in debug mode (not recommended).
119        #[arg(long, action)]
120        allow_debug: bool,
121    },
122    #[command(subcommand)]
123    Check(Check),
124}
125
126/// Subcommands for regression check operations.
127#[derive(Debug, Subcommand)]
128pub enum Check {
129    /// Provide a skeleton of the overall tolerance files.
130    Skeleton,
131    /// List all the tolerance inputs accepted by the benchmark executable.
132    Tolerances {
133        /// Describe the layout for the named tolerance kind.
134        describe: Option<String>,
135    },
136    /// Verify the tolerance file with the accompanying input file.
137    Verify {
138        /// The tolerance file to check.
139        #[arg(long = "tolerances")]
140        tolerances: PathBuf,
141        /// The benchmark input file used to generate the data that will be compared.
142        #[arg(long = "input-file")]
143        input_file: PathBuf,
144    },
145    /// Run regression checks against before/after output files.
146    Run {
147        /// The tolerance file to check.
148        #[arg(long = "tolerances")]
149        tolerances: PathBuf,
150        /// The benchmark input file used to generate the data that will be compared.
151        #[arg(long = "input-file")]
152        input_file: PathBuf,
153        /// The `--output-file` from a benchmark to use as a baseline.
154        #[arg(long = "before")]
155        before: PathBuf,
156        /// The `--output-file` that will be checked for regression against `before`.
157        #[arg(long = "after")]
158        after: PathBuf,
159        /// Optional path to write the JSON check results.
160        #[arg(long = "output-file")]
161        output_file: Option<PathBuf>,
162    },
163}
164
165/// The CLI used to drive a benchmark application.
166#[derive(Debug, Parser)]
167pub struct App {
168    #[command(subcommand)]
169    command: Commands,
170}
171
172impl App {
173    /// Construct [`Self`] by parsing commandline arguments from [`std::env::args`].
174    ///
175    /// This simply redirects to [`clap::Parser::parse`] and is provided to allow parsing
176    /// without the [`clap::Parser`] trait in scope.
177    pub fn parse() -> Self {
178        <Self as clap::Parser>::parse()
179    }
180
181    /// Construct [`Self`] by parsing command line arguments from the iterator.
182    ///
183    /// This simply redirects to [`clap::Parser::try_parse_from`] and is provided to allow
184    /// parsing without the [`clap::Parser`] trait in scope.
185    pub fn try_parse_from<I, T>(itr: I) -> anyhow::Result<Self>
186    where
187        I: IntoIterator<Item = T>,
188        T: Into<std::ffi::OsString> + Clone,
189    {
190        Ok(<Self as clap::Parser>::try_parse_from(itr)?)
191    }
192
193    /// Construct [`Self`] directly from a [`Commands`] enum.
194    pub fn from_commands(command: Commands) -> Self {
195        Self { command }
196    }
197
198    /// Run the application using the registered `inputs` and `benchmarks`.
199    pub fn run(
200        &self,
201        registry: &registry::Registry,
202        mut output: &mut dyn Output,
203    ) -> anyhow::Result<()> {
204        match &self.command {
205            // If a named benchmark isn't given, then list the available benchmarks.
206            Commands::Inputs { describe, all } => {
207                if let Some(describe) = describe {
208                    if let Some(input) = registry.input(describe) {
209                        let repr = jobs::Unprocessed::format_input(input)?;
210                        writeln!(
211                            output,
212                            "The example JSON representation for \"{}\" is:",
213                            describe
214                        )?;
215                        writeln!(output, "{}", serde_json::to_string_pretty(&repr)?)?;
216                        return Ok(());
217                    } else {
218                        writeln!(output, "No input found for \"{}\"", describe)?;
219                    }
220
221                    return Ok(());
222                }
223
224                // The algorithm below does a number of things. It:
225                //
226                // 1. Gathers all registered inputs.
227                // 2. Filters out inputs by visibility (but notes if any *were* filtered out)
228                // 3. Sorts and displays the inputs.
229                // 4. Notifies the user if additional unprinted inputs remain.
230                writeln!(output, "Available input kinds are listed below:")?;
231
232                let mut has_hidden = false;
233                let filter = if *all {
234                    VisibilityFilter::All
235                } else {
236                    VisibilityFilter::OnlyAvailable
237                };
238
239                let mut inputs: Vec<_> = registry
240                    .inputs()
241                    .filter(|i| {
242                        let vis = i.visibility();
243                        if !vis.is_available() {
244                            has_hidden = true;
245                        }
246
247                        filter.matches(&vis)
248                    })
249                    .collect();
250
251                inputs.sort_by(crate::input::order_inputs);
252
253                for i in inputs.iter() {
254                    writeln!(output, "    {}", i.display())?;
255                }
256
257                // If we did not print all inputs and there are hidden inputs - inform the user.
258                let should_notify_of_hidden = !*all && has_hidden;
259                if should_notify_of_hidden {
260                    writeln!(
261                        output,
262                        "\nOne or more inputs are hidden, use \"inputs --all\" to list all inputs"
263                    )?;
264                }
265            }
266            // List the available benchmarks.
267            Commands::Benchmarks { all } => {
268                writeln!(output, "Registered Benchmarks:")?;
269
270                let mut has_hidden = false;
271                let filter = if *all {
272                    VisibilityFilter::All
273                } else {
274                    VisibilityFilter::OnlyAvailable
275                };
276
277                let mut benchmarks: Vec<_> = registry
278                    .benchmarks()
279                    .iter()
280                    .filter(|b| {
281                        let vis = b.visibility();
282                        if !vis.is_available() {
283                            has_hidden = true;
284                        }
285
286                        filter.matches(&vis)
287                    })
288                    .collect();
289
290                benchmarks.sort_by(registry::RegisteredBenchmark::order);
291
292                for b in benchmarks {
293                    writeln!(output, "{}", Indent::new(&b.display().to_string(), 4))?;
294                }
295
296                // If we did not print all inputs and there are hidden inputs - inform the user.
297                let should_notify_of_hidden = !*all && has_hidden;
298                if should_notify_of_hidden {
299                    writeln!(
300                        output,
301                        "\nOne or more benchmarks are hidden, use \"benchmarks --all\" to list all benchmarks"
302                    )?;
303                }
304            }
305            Commands::Skeleton => {
306                writeln!(output, "Skeleton input file:")?;
307                writeln!(output, "{}", Jobs::example()?)?;
308            }
309            // Run the benchmarks
310            Commands::Run {
311                input_file,
312                output_file,
313                dry_run,
314                allow_debug,
315            } => {
316                // Parse and validate the input.
317                let run = Jobs::load(input_file, registry)?;
318                // Check if we have a match for each benchmark.
319                for job in run.jobs().iter() {
320                    const MAX_METHODS: usize = 3;
321                    if let Err(mismatches) = registry.debug(job, MAX_METHODS) {
322                        let repr = serde_json::to_string_pretty(&job.serialize()?)?;
323
324                        writeln!(
325                            output,
326                            "Could not find a match for the following input:\n\n{}\n",
327                            repr
328                        )?;
329                        writeln!(output, "{}", mismatches)?;
330
331                        return Err(anyhow::Error::msg(
332                            "could not find a benchmark for all inputs",
333                        ));
334                    }
335                }
336
337                if *dry_run {
338                    writeln!(
339                        output,
340                        "Success - skipping running benchmarks because \"--dry-run\" was used."
341                    )?;
342                    return Ok(());
343                }
344
345                // Check for debug mode before running benchmarks.
346                // This check is placed after the dry-run early return since dry-run doesn't
347                // actually execute benchmarks and thus won't produce misleading performance results.
348                check_debug_mode(*allow_debug)?;
349
350                // The collection of output results for each run.
351                let mut results = Vec::<serde_json::Value>::new();
352
353                // Now - we've verified the integrity of all the jobs we want to run and that
354                // each job can match an associated benchmark.
355                //
356                // All that's left is to actually run the benchmarks.
357                let jobs = run.jobs();
358                let serialized = jobs
359                    .iter()
360                    .map(|job| {
361                        Ok(serde_json::to_value(jobs::Unprocessed::new(
362                            job.tag().into(),
363                            job.serialize()?,
364                        ))?)
365                    })
366                    .collect::<anyhow::Result<Vec<_>>>()?;
367                for (i, job) in jobs.iter().enumerate() {
368                    let prefix: &str = if i != 0 { "\n\n" } else { "" };
369                    writeln!(
370                        output,
371                        "{}{}",
372                        prefix,
373                        Banner::new(&format!("Running Job {} of {}", i + 1, jobs.len()))
374                    )?;
375
376                    // Run the specified job.
377                    let checkpoint = Checkpoint::new(&serialized, &results, output_file)?;
378                    let r = registry.call(job, checkpoint, output)?;
379
380                    // Collect the results
381                    results.push(r);
382
383                    // Save everything.
384                    Checkpoint::new(&serialized, &results, output_file)?.save()?;
385                }
386            }
387            // Extensions
388            Commands::Check(check) => return self.check(check, registry, output),
389        };
390        Ok(())
391    }
392
393    // Extensions
394    fn check(
395        &self,
396        check: &Check,
397        registry: &registry::Registry,
398        mut output: &mut dyn Output,
399    ) -> anyhow::Result<()> {
400        match check {
401            Check::Skeleton => {
402                let message = "Skeleton tolerance file.\n\n\
403                               Each tolerance is paired with an input that is structurally\n\
404                               matched with an entry in the corresponding `--input-file`.\n\n\
405                               This allow a single tolerance entry to be applied to multiple\n\
406                               benchmark runs as long as this structural mapping is unambiguous.\n";
407
408                writeln!(output, "{}", message)?;
409                writeln!(output, "{}", internal::regression::Raw::example())?;
410                Ok(())
411            }
412            Check::Tolerances { describe } => {
413                let tolerances = registry.tolerances();
414
415                match describe {
416                    Some(name) => match tolerances.get(&**name) {
417                        Some(registered) => {
418                            let repr = internal::regression::RawInner::new(
419                                jobs::Unprocessed::new(
420                                    "".to_string(),
421                                    serde_json::Value::Object(Default::default()),
422                                ),
423                                jobs::Unprocessed::format_input(registered.tolerance)?,
424                            );
425
426                            write!(
427                                output,
428                                "The example JSON representation for \"{}\" is shown below.\n\
429                                 Populate the \"input\" field with a compatible benchmark input.\n\
430                                 Matching will be performed by partial structural map on the input.\n\n",
431                                name
432                            )?;
433                            writeln!(output, "{}", serde_json::to_string_pretty(&repr)?)?;
434                            Ok(())
435                        }
436                        None => {
437                            writeln!(output, "No tolerance input found for \"{}\"", name)?;
438                            Ok(())
439                        }
440                    },
441                    None => {
442                        writeln!(output, "Available tolerance kinds are listed below.")?;
443
444                        // Print the registered tolerance files in alphabetical order.
445                        let mut keys: Vec<_> = tolerances.keys().collect();
446                        keys.sort();
447                        for k in keys {
448                            // This access should not panic - we just obtained all the keys.
449                            let registered = &tolerances[k];
450                            writeln!(output, "    {}", registered.tolerance.tag())?;
451                            for pair in registered.regressions.iter() {
452                                writeln!(
453                                    output,
454                                    "    - \"{}\" => \"{}\"",
455                                    pair.input_tag(),
456                                    pair.name(),
457                                )?;
458                            }
459                        }
460                        Ok(())
461                    }
462                }
463            }
464            Check::Verify {
465                tolerances,
466                input_file,
467            } => {
468                let _ = internal::regression::Checks::new(tolerances, input_file, registry)?;
469                Ok(())
470            }
471            Check::Run {
472                tolerances,
473                input_file,
474                before,
475                after,
476                output_file,
477            } => {
478                let checks = internal::regression::Checks::new(tolerances, input_file, registry)?;
479                let jobs = checks.jobs(before, after)?;
480                jobs.run(output, output_file.as_deref())?;
481                Ok(())
482            }
483        }
484    }
485}
486
487///////////
488// Tests //
489///////////
490
491/// The integration test below look inside the `tests` directory for folders.
492///
493/// ## Input Files
494///
495/// Each folder should have at least a `stdin.txt` file specifying the command line to give
496/// to the `App` parser.
497///
498/// Within the `stdin.txt` command line, there are several special symbols:
499///
500/// * $INPUT - Resolves to `input.json` in the same directory as the `stdin.txt` file.
501/// * $OUTPUT - Resolves to `output.json` in a temporary directory.
502/// * $TOLERANCES - Resolves to `tolerances.json` in the test directory.
503/// * $REGRESSION_INPUT - Resolves to `regression_input.json` in the test directory.
504/// * $CHECK_OUTPUT - Resolves to `checks.json` in a temporary directory.
505///
506/// As mentioned - an input JSON file can be included and must be named "input.json" to be
507/// discoverable.
508///
509/// ## Output Files
510///
511/// Tests should have at least a `stdout.txt` file with the expected outputs for running the
512/// command in `stdin.txt`. If an output JSON file is expected, it should be named `output.json`.
513///
514/// ## Test Discovery and Running
515///
516/// The unit test will visit each folder in `tests` and run the outlined scenario. The
517/// `stdout.txt` expected output is compared to the actual output and if they do not match,
518/// the test fails.
519///
520/// Additionally, if `output.json` is present, the unit test will verify that (1) the command
521/// did in fact produce an output JSON file and (2) the generated file matches the expected file.
522///
523/// ## Regenerating Expected Results
524///
525/// The benchmark output will naturally change over time. Running the unit tests with the
526/// environment variable
527/// ```text
528/// DISKANN_TEST=overwrite
529/// ```
530/// will replace the `stdout.txt` (and `output.json` if one was generated) for each test
531/// scenario. Developers should then consult `git diff` to ensure that major regressions
532/// to the output did not occur.
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    use std::{
538        ffi::OsString,
539        path::{Path, PathBuf},
540    };
541
542    use crate::{registry, test::TestConfig, ux};
543
544    const ENV: &str = "DISKANN_TEST";
545
546    // Expected I/O files.
547    const STDIN: &str = "stdin.txt";
548    const STDOUT: &str = "stdout.txt";
549    const INPUT_FILE: &str = "input.json";
550    const OUTPUT_FILE: &str = "output.json";
551
552    // Optional list of enabled features, one feature name per line. When absent, no
553    // features are enabled.
554    const FEATURES_FILE: &str = "features.txt";
555
556    // Regression Extension
557    const TOLERANCES_FILE: &str = "tolerances.json";
558    const REGRESSION_INPUT_FILE: &str = "regression_input.json";
559    const CHECK_OUTPUT_FILE: &str = "checks.json";
560
561    const ALL_GENERATED_OUTPUTS: [&str; 2] = [OUTPUT_FILE, CHECK_OUTPUT_FILE];
562
563    // Read the entire contents of a file to a string.
564    fn read_to_string<P: AsRef<Path>>(path: P, ctx: &str) -> String {
565        match std::fs::read_to_string(path.as_ref()) {
566            Ok(s) => ux::normalize(s),
567            Err(err) => panic!(
568                "failed to read {} {:?} with error: {}",
569                ctx,
570                path.as_ref(),
571                err
572            ),
573        }
574    }
575
576    // Check if `DISKANN_TEST=overwrite` is configured. Return `true` if so - otherwise
577    // return `false`.
578    //
579    // If `DISKANN_TEST` is set but its value is not `overwrite` - panic.
580    fn overwrite() -> bool {
581        match std::env::var(ENV) {
582            Ok(v) => {
583                if v == "overwrite" {
584                    true
585                } else {
586                    panic!(
587                        "Unknown value for {}: \"{}\". Expected \"overwrite\"",
588                        ENV, v
589                    );
590                }
591            }
592            Err(std::env::VarError::NotPresent) => false,
593            Err(std::env::VarError::NotUnicode(_)) => {
594                panic!("Value for {} is not unicode", ENV);
595            }
596        }
597    }
598
599    // Test Runner
600    struct Test {
601        dir: PathBuf,
602        overwrite: bool,
603    }
604
605    impl Test {
606        fn new(dir: &Path) -> Self {
607            Self {
608                dir: dir.into(),
609                overwrite: overwrite(),
610            }
611        }
612
613        fn parse_stdin(&self, tempdir: &Path) -> Vec<App> {
614            let path = self.dir.join(STDIN);
615
616            // Read the standard input file to a string.
617            let stdin = read_to_string(&path, "standard input");
618
619            let output: Vec<App> = stdin
620                .lines()
621                .filter_map(|line| {
622                    if line.starts_with('#') || line.is_empty() {
623                        None
624                    } else {
625                        Some(self.parse_line(line, tempdir))
626                    }
627                })
628                .collect();
629
630            if output.is_empty() {
631                panic!("File \"{}/stdin.txt\" has no command!", self.dir.display());
632            }
633
634            output
635        }
636
637        fn parse_line(&self, line: &str, tempdir: &Path) -> App {
638            // Split and resolve special symbols
639            let args: Vec<OsString> = line
640                .split_whitespace()
641                .map(|v| -> OsString { self.resolve(v, tempdir).into() })
642                .collect();
643
644            App::try_parse_from(std::iter::once(OsString::from("test-app")).chain(args)).unwrap()
645        }
646
647        fn resolve(&self, s: &str, tempdir: &Path) -> PathBuf {
648            match s {
649                // Standard workflow
650                "$INPUT" => self.dir.join(INPUT_FILE),
651                "$OUTPUT" => tempdir.join(OUTPUT_FILE),
652                // Regression extension
653                "$TOLERANCES" => self.dir.join(TOLERANCES_FILE),
654                "$REGRESSION_INPUT" => self.dir.join(REGRESSION_INPUT_FILE),
655                "$CHECK_OUTPUT" => tempdir.join(CHECK_OUTPUT_FILE),
656
657                // Catch-all: no interpolation
658                _ => s.into(),
659            }
660        }
661
662        // Build the registration config from the fixture's `features.txt`, if present. Each
663        // non-empty line names one enabled feature.
664        fn config(&self) -> TestConfig {
665            let path = self.dir.join(FEATURES_FILE);
666            match std::fs::read_to_string(&path) {
667                Ok(contents) => TestConfig::with_features(
668                    contents
669                        .lines()
670                        .map(str::trim)
671                        .filter(|line| !line.is_empty())
672                        .map(str::to_owned),
673                ),
674                Err(err) if err.kind() == std::io::ErrorKind::NotFound => TestConfig::new(),
675                Err(err) => panic!("failed to read {:?}: {}", path, err),
676            }
677        }
678
679        fn run(&self, tempdir: &Path) {
680            let apps = self.parse_stdin(tempdir);
681
682            // Register outputs
683            let mut registry = registry::Registry::new();
684            crate::test::register_benchmarks(&mut registry, &self.config()).unwrap();
685
686            // Run each app invocation - collecting the last output into a buffer.
687            //
688            // Only the last run is allowed to return an error - if it does, format the
689            // error to the output buffer as well using the debug formatting option.
690            let mut buffer = crate::output::Memory::new();
691            for (i, app) in apps.iter().enumerate() {
692                let is_last = i + 1 == apps.len();
693
694                // Select where to route the test output.
695                //
696                // Only the last run gets saved. Setup output is discarded — if a setup
697                // command fails, the panic message includes the error.
698                let mut b: &mut dyn crate::Output = if is_last {
699                    &mut buffer
700                } else {
701                    &mut crate::output::Sink::new()
702                };
703
704                if let Err(err) = app.run(&registry, b) {
705                    if is_last {
706                        write!(b, "{:?}", err).unwrap();
707                    } else {
708                        panic!(
709                            "App {} of {} failed with error: {:?}",
710                            i + 1,
711                            apps.len(),
712                            err
713                        );
714                    }
715                }
716            }
717
718            // Check that `stdout` matches
719            let stdout: String =
720                ux::normalize(ux::strip_backtrace(buffer.into_inner().try_into().unwrap()));
721            let stdout = ux::scrub_path(stdout, tempdir, "$TEMPDIR");
722            let output = self.dir.join(STDOUT);
723            if self.overwrite {
724                std::fs::write(output, stdout).unwrap();
725            } else {
726                let expected = read_to_string(&output, "expected standard output");
727                if stdout != expected {
728                    panic!("Got:\n--\n{}\n--\nExpected:\n--\n{}\n--", stdout, expected);
729                }
730            }
731
732            // Check that the output files match.
733            for file in ALL_GENERATED_OUTPUTS {
734                self.check_output_file(tempdir, file);
735            }
736        }
737
738        fn check_output_file(&self, tempdir: &Path, filename: &str) {
739            let generated_path = tempdir.join(filename);
740            let was_generated = generated_path.is_file();
741
742            let expected_path = self.dir.join(filename);
743            let is_expected = expected_path.is_file();
744
745            if self.overwrite {
746                // Copy the output file to the destination.
747                if was_generated {
748                    println!(
749                        "Moving generated file {:?} to {:?}",
750                        generated_path, expected_path
751                    );
752
753                    if let Err(err) = std::fs::rename(&generated_path, &expected_path) {
754                        panic!(
755                            "Moving generated file {:?} to expected location {:?} failed: {}",
756                            generated_path, expected_path, err
757                        );
758                    }
759                } else if is_expected {
760                    println!("Removing outdated file {:?}", expected_path);
761                    if let Err(err) = std::fs::remove_file(&expected_path) {
762                        panic!("Failed removing outdated file {:?}: {}", expected_path, err);
763                    }
764                }
765            } else {
766                match (was_generated, is_expected) {
767                    (true, true) => {
768                        let output_contents = read_to_string(generated_path, "generated");
769
770                        let expected_contents = read_to_string(expected_path, "expected");
771
772                        if output_contents != expected_contents {
773                            panic!(
774                                "{}: Got:\n\n{}\n\nExpected:\n\n{}\n",
775                                filename, output_contents, expected_contents
776                            );
777                        }
778                    }
779                    (true, false) => {
780                        let output_contents = read_to_string(generated_path, "generated");
781
782                        panic!(
783                            "{} was generated when none was expected. Contents:\n\n{}",
784                            filename, output_contents
785                        );
786                    }
787                    (false, true) => {
788                        panic!("{} was not generated when it was expected", filename);
789                    }
790                    (false, false) => { /* this is okay */ }
791                }
792            }
793        }
794    }
795
796    fn run_specific_test(test_dir: &Path) {
797        println!("running test in {:?}", test_dir);
798        let temp_dir = tempfile::tempdir().unwrap();
799        Test::new(test_dir).run(temp_dir.path());
800    }
801
802    fn run_all_tests_in(dir: &str) {
803        let dir: PathBuf = format!("{}/tests/{}", env!("CARGO_MANIFEST_DIR"), dir).into();
804        for entry in std::fs::read_dir(dir).unwrap() {
805            let entry = entry.unwrap();
806            if let Ok(file_type) = entry.file_type() {
807                if file_type.is_dir() {
808                    run_specific_test(&entry.path());
809                }
810            } else {
811                panic!("couldn't get file type for {:?}", entry.path());
812            }
813        }
814    }
815
816    #[test]
817    fn benchmark_tests() {
818        run_all_tests_in("benchmark");
819    }
820
821    #[test]
822    fn regression_tests() {
823        run_all_tests_in("regression");
824    }
825}