diff --git a/cranelift/src/tools/filetest/mod.rs b/cranelift/src/tools/filetest/mod.rs new file mode 100644 index 0000000000..078e287dde --- /dev/null +++ b/cranelift/src/tools/filetest/mod.rs @@ -0,0 +1,33 @@ +//! File tests. +//! +//! This module contains the main driver for `cton-util test` as well as implementations of the +//! available test commands. + +use std::path::Path; +use CommandResult; +use filetest::runner::TestRunner; + +mod runner; + +/// Main entry point for `cton-util test`. +/// +/// Take a list of filenames which can be either `.cton` files or directories. +/// +/// Files are interpreted as test cases and executed immediately. +/// +/// Directories are scanned recursively for test cases ending in `.cton`. These test cases are +/// executed on background threads. +/// +pub fn run(files: Vec) -> CommandResult { + let mut runner = TestRunner::new(); + + for path in files.iter().map(Path::new) { + if path.is_file() { + runner.push_test(path); + } else { + runner.push_dir(path); + } + } + + runner.run() +} diff --git a/cranelift/src/tools/filetest/runner.rs b/cranelift/src/tools/filetest/runner.rs new file mode 100644 index 0000000000..7f39ec6490 --- /dev/null +++ b/cranelift/src/tools/filetest/runner.rs @@ -0,0 +1,112 @@ +//! Test runner. +//! +//! This module implements the `TestRunner` struct which manages executing tests as well as +//! scanning directories for tests. + +use std::ffi::OsStr; +use std::path::PathBuf; +use std::error::Error; +use CommandResult; + +pub struct TestRunner { + // Directories that have not yet been scanned. + dir_stack: Vec, + + // Filenames of tests to run. + test_list: Vec, + + errors: usize, +} + +impl TestRunner { + /// Create a new blank TrstRunner. + pub fn new() -> TestRunner { + TestRunner { + dir_stack: Vec::new(), + test_list: Vec::new(), + errors: 0, + } + } + + /// Add a directory path to be scanned later. + /// + /// If `dir` turns out to be a regular file, it is silently ignored. + /// Otherwise, any problems reading the directory are reported. + pub fn push_dir>(&mut self, dir: P) { + self.dir_stack.push(dir.into()); + } + + /// Add a test to be executed later. + /// + /// Any problems reading `file` as a test case file will be reported as a test failure. + pub fn push_test>(&mut self, file: P) { + self.test_list.push(file.into()); + } + + /// Scan any directories pushed so far. + /// Push any potential test cases found. + pub fn scan_dirs(&mut self) { + // This recursive search tries to minimize statting in a directory hierarchy containing + // mostly test cases. + // + // - Directory entries with a "cton" extension are presumed to be test case files. + // - Directory entries with no extension are presumed to be subdirectories. + // - Anything else is ignored. + // + while let Some(dir) = self.dir_stack.pop() { + match dir.read_dir() { + Err(err) => { + // Fail silently if `dir` was actually a regular file. + // This lets us skip spurious extensionless files without statting everything + // needlessly. + if !dir.is_file() { + self.path_error(dir, err); + } + } + Ok(entries) => { + // Read all directory entries. Avoid statting. + for entry_result in entries { + match entry_result { + Err(err) => { + // Not sure why this would happen. `read_dir` succeeds, but there's + // a problem with an entry. I/O error during a getdirentries + // syscall seems to be the reason. The implementation in + // libstd/sys/unix/fs.rs seems to suggest that breaking now would + // be a good idea, or the iterator could keep returning the same + // error forever. + self.path_error(dir, err); + break; + } + Ok(entry) => { + let path = entry.path(); + // Recognize directories and tests by extension. + // Yes, this means we ignore directories with '.' in their name. + match path.extension().and_then(OsStr::to_str) { + Some("cton") => self.push_test(path), + Some(_) => {} + None => self.push_dir(path), + } + } + } + } + } + } + } + } + + /// Report an error related to a path. + fn path_error(&mut self, path: PathBuf, err: E) { + self.errors += 1; + println!("path-error {}: {}", path.to_string_lossy(), err); + } + + /// Scan pushed directories for tests and run them. + pub fn run(&mut self) -> CommandResult { + self.scan_dirs(); + match self.errors { + 0 => Ok(()), + 1 => Err("1 failure".to_string()), + n => Err(format!("{} failures", n)), + } + } +} diff --git a/cranelift/src/tools/main.rs b/cranelift/src/tools/main.rs index bd0999cd4e..5abfa89462 100644 --- a/cranelift/src/tools/main.rs +++ b/cranelift/src/tools/main.rs @@ -10,7 +10,7 @@ use docopt::Docopt; use std::io::{self, Write}; use std::process; - +mod filetest; mod cat; mod print_cfg; mod rsfilecheck; @@ -19,6 +19,7 @@ const USAGE: &'static str = " Cretonne code generator utility Usage: + cton-util test ... cton-util cat ... cton-util filecheck [-v] cton-util print-cfg ... @@ -33,6 +34,7 @@ Options: #[derive(RustcDecodable, Debug)] struct Args { + cmd_test: bool, cmd_cat: bool, cmd_filecheck: bool, cmd_print_cfg: bool, @@ -55,7 +57,9 @@ fn cton_util() -> CommandResult { .unwrap_or_else(|e| e.exit()); // Find the sub-command to execute. - if args.cmd_cat { + if args.cmd_test { + filetest::run(args.arg_file) + } else if args.cmd_cat { cat::run(args.arg_file) } else if args.cmd_filecheck { rsfilecheck::run(args.arg_file, args.flag_verbose)