From 2f0f017d3e6a34a956c8d969b12a3905934a649b Mon Sep 17 00:00:00 2001 From: Huon Wilson Date: Mon, 5 May 2014 19:15:17 +1000 Subject: [PATCH 1/4] test: implement a no-alloc -> &str method for TestName. This is far cheaper than the `.to_str` technique that was used previously. --- src/libtest/lib.rs | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/libtest/lib.rs b/src/libtest/lib.rs index ba8b8f776d9..2a985fcab4d 100644 --- a/src/libtest/lib.rs +++ b/src/libtest/lib.rs @@ -53,6 +53,7 @@ use term::color::{Color, RED, YELLOW, GREEN, CYAN}; use std::cmp; use std::f64; use std::fmt; +use std::fmt::Show; use std::from_str::FromStr; use std::io::stdio::StdWriter; use std::io::{File, ChanReader, ChanWriter}; @@ -85,14 +86,19 @@ pub enum TestName { StaticTestName(&'static str), DynTestName(StrBuf) } -impl fmt::Show for TestName { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl TestName { + fn as_slice<'a>(&'a self) -> &'a str { match *self { - StaticTestName(s) => f.buf.write_str(s), - DynTestName(ref s) => f.buf.write_str(s.as_slice()), + StaticTestName(s) => s, + DynTestName(ref s) => s.as_slice() } } } +impl Show for TestName { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.as_slice().fmt(f) + } +} #[deriving(Clone)] enum NamePadding { PadNone, PadOnLeft, PadOnRight } @@ -100,7 +106,7 @@ enum NamePadding { PadNone, PadOnLeft, PadOnRight } impl TestDesc { fn padded_name(&self, column_count: uint, align: NamePadding) -> StrBuf { use std::num::Saturating; - let mut name = StrBuf::from_str(self.name.to_str()); + let mut name = StrBuf::from_str(self.name.as_slice()); let fill = column_count.saturating_sub(name.len()); let mut pad = StrBuf::from_owned_str(" ".repeat(fill)); match align { @@ -590,7 +596,7 @@ impl ConsoleTestState { TrIgnored => "ignored".to_strbuf(), TrMetrics(ref mm) => fmt_metrics(mm), TrBench(ref bs) => fmt_bench_samples(bs) - }, test.name.to_str()); + }, test.name.as_slice()); o.write(s.as_bytes()) } } @@ -604,7 +610,7 @@ impl ConsoleTestState { failures.push(f.name.to_str()); if stdout.len() > 0 { fail_out.push_str(format!("---- {} stdout ----\n\t", - f.name.to_str())); + f.name.as_slice())); let output = str::from_utf8_lossy(stdout.as_slice()); fail_out.push_str(output.as_slice().replace("\n", "\n\t")); fail_out.push_str("\n"); @@ -618,7 +624,7 @@ impl ConsoleTestState { try!(self.write_plain("\nfailures:\n")); failures.as_mut_slice().sort(); for name in failures.iter() { - try!(self.write_plain(format!(" {}\n", name.to_str()))); + try!(self.write_plain(format!(" {}\n", name.as_slice()))); } Ok(()) } @@ -753,7 +759,7 @@ pub fn run_tests_console(opts: &TestOpts, TrOk => st.passed += 1, TrIgnored => st.ignored += 1, TrMetrics(mm) => { - let tname = test.name.to_str(); + let tname = test.name.as_slice(); let MetricMap(mm) = mm; for (k,v) in mm.iter() { st.metrics @@ -764,7 +770,7 @@ pub fn run_tests_console(opts: &TestOpts, st.measured += 1 } TrBench(bs) => { - st.metrics.insert_metric(test.name.to_str(), + st.metrics.insert_metric(test.name.as_slice(), bs.ns_iter_summ.median, bs.ns_iter_summ.max - bs.ns_iter_summ.min); st.measured += 1 @@ -782,12 +788,12 @@ pub fn run_tests_console(opts: &TestOpts, fn len_if_padded(t: &TestDescAndFn) -> uint { match t.testfn.padding() { PadNone => 0u, - PadOnLeft | PadOnRight => t.desc.name.to_str().len(), + PadOnLeft | PadOnRight => t.desc.name.as_slice().len(), } } match tests.iter().max_by(|t|len_if_padded(*t)) { Some(t) => { - let n = t.desc.name.to_str(); + let n = t.desc.name.as_slice(); st.max_name_len = n.len(); }, None => {} @@ -980,7 +986,7 @@ pub fn filter_tests( }; // Sort the tests alphabetically - filtered.sort_by(|t1, t2| t1.desc.name.to_str().cmp(&t2.desc.name.to_str())); + filtered.sort_by(|t1, t2| t1.desc.name.as_slice().cmp(&t2.desc.name.as_slice())); // Shard the remaining tests, if sharding requested. match opts.test_shard { From 19f9181654ec71754c8528dd37075643d95947c4 Mon Sep 17 00:00:00 2001 From: Huon Wilson Date: Mon, 5 May 2014 22:19:38 +1000 Subject: [PATCH 2/4] test: allow the test filter to be a regex. This is fully backwards compatible, since test names are Rust identifiers + `:`, and hence not special regex characters. Fixes #2866. --- mk/crates.mk | 2 +- src/compiletest/common.rs | 3 +- src/compiletest/compiletest.rs | 26 +++++++---- src/doc/guide-testing.md | 36 +++++++++++---- src/libtest/lib.rs | 82 +++++++++++++++++++++------------- 5 files changed, 101 insertions(+), 48 deletions(-) diff --git a/mk/crates.mk b/mk/crates.mk index 0437e08de28..0b923cca7a2 100644 --- a/mk/crates.mk +++ b/mk/crates.mk @@ -80,7 +80,7 @@ DEPS_collections := std rand DEPS_fourcc := syntax std DEPS_hexfloat := syntax std DEPS_num := std rand -DEPS_test := std collections getopts serialize term time +DEPS_test := std collections getopts serialize term time regex DEPS_time := std serialize DEPS_rand := std DEPS_url := std collections diff --git a/src/compiletest/common.rs b/src/compiletest/common.rs index 9934a48c856..b1f1e69c5a1 100644 --- a/src/compiletest/common.rs +++ b/src/compiletest/common.rs @@ -10,6 +10,7 @@ use std::from_str::FromStr; use std::fmt; +use regex::Regex; #[deriving(Clone, Eq)] pub enum Mode { @@ -88,7 +89,7 @@ pub struct Config { pub run_ignored: bool, // Only run tests that match this filter - pub filter: Option<~str>, + pub filter: Option, // Write out a parseable log of tests that were run pub logfile: Option, diff --git a/src/compiletest/compiletest.rs b/src/compiletest/compiletest.rs index 32bd66c2004..3b57e3e98ca 100644 --- a/src/compiletest/compiletest.rs +++ b/src/compiletest/compiletest.rs @@ -23,6 +23,8 @@ extern crate log; extern crate green; extern crate rustuv; +extern crate regex; + use std::os; use std::io; use std::io::fs; @@ -113,6 +115,19 @@ pub fn parse_config(args: Vec<~str> ) -> Config { Path::new(m.opt_str(nm).unwrap()) } + let filter = if !matches.free.is_empty() { + let s = matches.free.get(0).as_slice(); + match regex::Regex::new(s) { + Ok(re) => Some(re), + Err(e) => { + println!("failed to parse filter /{}/: {}", s, e); + fail!() + } + } + } else { + None + }; + Config { compile_lib_path: matches.opt_str("compile-lib-path").unwrap(), run_lib_path: matches.opt_str("run-lib-path").unwrap(), @@ -125,12 +140,7 @@ pub fn parse_config(args: Vec<~str> ) -> Config { stage_id: matches.opt_str("stage-id").unwrap(), mode: FromStr::from_str(matches.opt_str("mode").unwrap()).expect("invalid mode"), run_ignored: matches.opt_present("ignored"), - filter: - if !matches.free.is_empty() { - Some((*matches.free.get(0)).clone()) - } else { - None - }, + filter: filter, logfile: matches.opt_str("logfile").map(|s| Path::new(s)), save_metrics: matches.opt_str("save-metrics").map(|s| Path::new(s)), ratchet_metrics: @@ -169,7 +179,7 @@ pub fn log_config(config: &Config) { logv(c, format!("stage_id: {}", config.stage_id)); logv(c, format!("mode: {}", config.mode)); logv(c, format!("run_ignored: {}", config.run_ignored)); - logv(c, format!("filter: {}", opt_str(&config.filter))); + logv(c, format!("filter: {}", opt_str(&config.filter.as_ref().map(|re| re.to_str())))); logv(c, format!("runtool: {}", opt_str(&config.runtool))); logv(c, format!("host-rustcflags: {}", opt_str(&config.host_rustcflags))); logv(c, format!("target-rustcflags: {}", opt_str(&config.target_rustcflags))); @@ -238,7 +248,7 @@ pub fn test_opts(config: &Config) -> test::TestOpts { test::TestOpts { filter: match config.filter { None => None, - Some(ref filter) => Some(filter.to_strbuf()), + Some(ref filter) => Some(filter.clone()), }, run_ignored: config.run_ignored, logfile: config.logfile.clone(), diff --git a/src/doc/guide-testing.md b/src/doc/guide-testing.md index 0be831c5132..057849f1bca 100644 --- a/src/doc/guide-testing.md +++ b/src/doc/guide-testing.md @@ -90,10 +90,15 @@ fn test_out_of_bounds_failure() { ~~~ A test runner built with the `--test` flag supports a limited set of -arguments to control which tests are run: the first free argument -passed to a test runner specifies a filter used to narrow down the set -of tests being run; the `--ignored` flag tells the test runner to run -only tests with the `ignore` attribute. +arguments to control which tests are run: + +- the first free argument passed to a test runner is interpreted as a + regular expression + ([syntax reference](regex/index.html#syntax)) + and is used to narrow down the set of tests being run. Note: a plain + string is a valid regular expression that matches itself. +- the `--ignored` flag tells the test runner to run only tests with the + `ignore` attribute. ## Parallelism @@ -146,16 +151,31 @@ result: FAILED. 1 passed; 1 failed; 0 ignored ### Running a subset of tests -~~~ {.notrust} -$ mytests mytest1 +Using a plain string: -running 11 tests +~~~ {.notrust} +$ mytests mytest23 + +running 1 tests +running driver::tests::mytest23 ... ok + +result: ok. 1 passed; 0 failed; 0 ignored +~~~ + +Using some regular expression features: + +~~~ {.notrust} +$ mytests 'mytest[145]' + +running 13 tests running driver::tests::mytest1 ... ok +running driver::tests::mytest4 ... ok +running driver::tests::mytest5 ... ok running driver::tests::mytest10 ... ignored ... snip ... running driver::tests::mytest19 ... ok -result: ok. 11 passed; 0 failed; 1 ignored +result: ok. 13 passed; 0 failed; 1 ignored ~~~ # Microbenchmarking diff --git a/src/libtest/lib.rs b/src/libtest/lib.rs index 2a985fcab4d..793657e7e88 100644 --- a/src/libtest/lib.rs +++ b/src/libtest/lib.rs @@ -37,6 +37,7 @@ extern crate collections; extern crate getopts; +extern crate regex; extern crate serialize; extern crate term; extern crate time; @@ -45,6 +46,7 @@ use collections::TreeMap; use stats::Stats; use time::precise_time_ns; use getopts::{OptGroup, optflag, optopt}; +use regex::Regex; use serialize::{json, Decodable}; use serialize::json::{Json, ToJson}; use term::Terminal; @@ -263,7 +265,7 @@ pub fn test_main_static_x(args: &[~str], tests: &[TestDescAndFn]) { } pub struct TestOpts { - pub filter: Option, + pub filter: Option, pub run_ignored: bool, pub run_tests: bool, pub run_benchmarks: bool, @@ -324,8 +326,8 @@ fn usage(binary: &str, helpstr: &str) { println!(""); if helpstr == "help" { println!("{}", "\ -The FILTER is matched against the name of all tests to run, and if any tests -have a substring match, only those tests are run. +The FILTER regex is matched against the name of all tests to run, and +only those tests that match are run. By default, all tests are run in parallel. This can be altered with the RUST_TEST_TASKS environment variable when running tests (set it to 1). @@ -372,12 +374,15 @@ pub fn parse_opts(args: &[StrBuf]) -> Option { return None; } - let filter = - if matches.free.len() > 0 { - Some((*matches.free.get(0)).to_strbuf()) - } else { - None - }; + let filter = if matches.free.len() > 0 { + let s = matches.free.get(0).as_slice(); + match Regex::new(s) { + Ok(re) => Some(re), + Err(e) => return Some(Err(format_strbuf!("could not parse /{}/: {}", s, e))) + } + } else { + None + }; let run_ignored = matches.opt_present("ignored"); @@ -945,26 +950,12 @@ pub fn filter_tests( let mut filtered = tests; // Remove tests that don't match the test filter - filtered = if opts.filter.is_none() { - filtered - } else { - let filter_str = match opts.filter { - Some(ref f) => (*f).clone(), - None => "".to_strbuf() - }; - - fn filter_fn(test: TestDescAndFn, filter_str: &str) -> - Option { - if test.desc.name.to_str().contains(filter_str) { - return Some(test); - } else { - return None; - } + filtered = match opts.filter { + None => filtered, + Some(ref re) => { + filtered.move_iter() + .filter(|test| re.is_match(test.desc.name.as_slice())).collect() } - - filtered.move_iter() - .filter_map(|x| filter_fn(x, filter_str.as_slice())) - .collect() }; // Maybe pull out the ignored test and unignore them @@ -1451,12 +1442,12 @@ mod tests { #[test] fn first_free_arg_should_be_a_filter() { - let args = vec!("progname".to_strbuf(), "filter".to_strbuf()); + let args = vec!("progname".to_strbuf(), "some_regex_filter".to_strbuf()); let opts = match parse_opts(args.as_slice()) { Some(Ok(o)) => o, _ => fail!("Malformed arg in first_free_arg_should_be_a_filter") }; - assert!("filter" == opts.filter.clone().unwrap().as_slice()); + assert!(opts.filter.expect("should've found filter").is_match("some_regex_filter")) } #[test] @@ -1555,6 +1546,37 @@ mod tests { } } + #[test] + pub fn filter_tests_regex() { + let mut opts = TestOpts::new(); + opts.filter = Some(::regex::Regex::new("a.*b.+c").unwrap()); + + let mut names = ["yes::abXc", "yes::aXXXbXXXXc", + "no::XYZ", "no::abc"]; + names.sort(); + + fn test_fn() {} + let tests = names.iter().map(|name| { + TestDescAndFn { + desc: TestDesc { + name: DynTestName(name.to_strbuf()), + ignore: false, + should_fail: false + }, + testfn: DynTestFn(test_fn) + } + }).collect(); + let filtered = filter_tests(&opts, tests); + + let expected: Vec<&str> = + names.iter().map(|&s| s).filter(|name| name.starts_with("yes")).collect(); + + assert_eq!(filtered.len(), expected.len()); + for (test, expected_name) in filtered.iter().zip(expected.iter()) { + assert_eq!(test.desc.name.as_slice(), *expected_name); + } + } + #[test] pub fn test_metricmap_compare() { let mut m1 = MetricMap::new(); From b1ee3200b52a87ba788a6edf3b14db9466f2cb7d Mon Sep 17 00:00:00 2001 From: Huon Wilson Date: Mon, 5 May 2014 22:44:07 +1000 Subject: [PATCH 3/4] test: ensure that the extended usage description gets printed. Previously the longer hand-written usage string was never being printed: theoretically it was trying to detect when precisely `--help` was passed (but not `-h`), but the getopts framework was considering a check for the presence of `-h` to be a check for that of `--help` too, i.e. the code was always going through the `-h` path. This changes it to print the extended usage for both `-h` and `--help`, meaning that it does actually appear correctly. --- src/libtest/lib.rs | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/libtest/lib.rs b/src/libtest/lib.rs index 793657e7e88..3273e53ed8a 100644 --- a/src/libtest/lib.rs +++ b/src/libtest/lib.rs @@ -320,13 +320,11 @@ fn optgroups() -> Vec { task, allow printing directly")) } -fn usage(binary: &str, helpstr: &str) { +fn usage(binary: &str) { let message = format!("Usage: {} [OPTIONS] [FILTER]", binary); - println!("{}", getopts::usage(message, optgroups().as_slice())); - println!(""); - if helpstr == "help" { - println!("{}", "\ -The FILTER regex is matched against the name of all tests to run, and + println!(r"{usage} + +The FILTER regex is tested against the name of all tests to run, and only those tests that match are run. By default, all tests are run in parallel. This can be altered with the @@ -338,18 +336,18 @@ environment variable. Logging is not captured by default. Test Attributes: - #[test] - Indicates a function is a test to be run. This function + \#[test] - Indicates a function is a test to be run. This function takes no arguments. - #[bench] - Indicates a function is a benchmark to be run. This + \#[bench] - Indicates a function is a benchmark to be run. This function takes one argument (test::Bencher). - #[should_fail] - This function (also labeled with #[test]) will only pass if + \#[should_fail] - This function (also labeled with \#[test]) will only pass if the code causes a failure (an assertion failure or fail!) - #[ignore] - When applied to a function which is already attributed as a + \#[ignore] - When applied to a function which is already attributed as a test, then the test runner will ignore these tests during normal test runs. Running with --ignored will run these - tests. This may also be written as #[ignore(cfg(...))] to - ignore the test on certain configurations."); - } + tests. This may also be written as \#[ignore(cfg(...))] to + ignore the test on certain configurations.", + usage = getopts::usage(message, optgroups().as_slice())); } // Parses command line arguments into test options @@ -365,14 +363,7 @@ pub fn parse_opts(args: &[StrBuf]) -> Option { Err(f) => return Some(Err(f.to_err_msg().to_strbuf())) }; - if matches.opt_present("h") { - usage(args[0].as_slice(), "h"); - return None; - } - if matches.opt_present("help") { - usage(args[0].as_slice(), "help"); - return None; - } + if matches.opt_present("h") { usage(args[0].as_slice()); return None; } let filter = if matches.free.len() > 0 { let s = matches.free.get(0).as_slice(); From 18c13de5e6dfd4631f9ed05e0ab49305bf0384ac Mon Sep 17 00:00:00 2001 From: Huon Wilson Date: Thu, 8 May 2014 23:33:22 +1000 Subject: [PATCH 4/4] std:: switch the order in which dynamic_lib adds search paths. The compiler needs to be opening e.g. libregex in the correct directory, which requires getting these in the right order. --- src/libstd/unstable/dynamic_lib.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libstd/unstable/dynamic_lib.rs b/src/libstd/unstable/dynamic_lib.rs index 87d531cc627..b2a912e65a8 100644 --- a/src/libstd/unstable/dynamic_lib.rs +++ b/src/libstd/unstable/dynamic_lib.rs @@ -16,8 +16,8 @@ A simple wrapper over the platform's dynamic library facilities */ + use c_str::ToCStr; -use iter::Iterator; use mem; use ops::*; use option::*; @@ -25,7 +25,7 @@ use os; use path::GenericPath; use path; use result::*; -use slice::{Vector,OwnedVector}; +use slice::Vector; use str; use vec::Vec; @@ -75,10 +75,12 @@ impl DynamicLibrary { } else { ("LD_LIBRARY_PATH", ':' as u8) }; - let newenv = os::getenv_as_bytes(envvar).unwrap_or(box []); - let mut newenv = newenv.move_iter().collect::>(); - newenv.push_all(&[sep]); - newenv.push_all(path.as_vec()); + let mut newenv = Vec::from_slice(path.as_vec()); + newenv.push(sep); + match os::getenv_as_bytes(envvar) { + Some(bytes) => newenv.push_all(bytes), + None => {} + } os::setenv(envvar, str::from_utf8(newenv.as_slice()).unwrap()); }