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(®istry, &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: ®istry::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(|a, b| crate::input::internal::order(a, b));
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: ®istry::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(®istered.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(®istry, 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}