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