Skip to content

Commit e76ca4b

Browse files
committed
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.
1 parent bae3134 commit e76ca4b

File tree

5 files changed

+100
-44
lines changed

5 files changed

+100
-44
lines changed

mk/crates.mk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ DEPS_collections := std rand
7878
DEPS_fourcc := syntax std
7979
DEPS_hexfloat := syntax std
8080
DEPS_num := std rand
81-
DEPS_test := std collections getopts serialize term time
81+
DEPS_test := std collections getopts serialize term time regex
8282
DEPS_time := std serialize
8383
DEPS_rand := std
8484
DEPS_url := std collections

src/compiletest/common.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
// option. This file may not be copied, modified, or distributed
99
// except according to those terms.
1010

11+
use regex::Regex;
12+
1113
#[deriving(Clone, Eq)]
1214
pub enum mode {
1315
mode_compile_fail,
@@ -54,7 +56,7 @@ pub struct config {
5456
pub run_ignored: bool,
5557

5658
// Only run tests that match this filter
57-
pub filter: Option<~str>,
59+
pub filter: Option<Regex>,
5860

5961
// Write out a parseable log of tests that were run
6062
pub logfile: Option<Path>,

src/compiletest/compiletest.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ extern crate log;
2424
extern crate green;
2525
extern crate rustuv;
2626

27+
extern crate regex;
28+
2729
use std::os;
2830
use std::io;
2931
use std::io::fs;
@@ -117,6 +119,19 @@ pub fn parse_config(args: Vec<~str> ) -> config {
117119
Path::new(m.opt_str(nm).unwrap())
118120
}
119121

122+
let filter = if !matches.free.is_empty() {
123+
let s = matches.free.get(0).as_slice();
124+
match regex::Regex::new(s) {
125+
Ok(re) => Some(re),
126+
Err(e) => {
127+
println!("failed to parse filter /{}/: {}", s, e);
128+
fail!()
129+
}
130+
}
131+
} else {
132+
None
133+
};
134+
120135
config {
121136
compile_lib_path: matches.opt_str("compile-lib-path").unwrap(),
122137
run_lib_path: matches.opt_str("run-lib-path").unwrap(),
@@ -129,12 +144,7 @@ pub fn parse_config(args: Vec<~str> ) -> config {
129144
stage_id: matches.opt_str("stage-id").unwrap(),
130145
mode: str_mode(matches.opt_str("mode").unwrap()),
131146
run_ignored: matches.opt_present("ignored"),
132-
filter:
133-
if !matches.free.is_empty() {
134-
Some((*matches.free.get(0)).clone())
135-
} else {
136-
None
137-
},
147+
filter: filter,
138148
logfile: matches.opt_str("logfile").map(|s| Path::new(s)),
139149
save_metrics: matches.opt_str("save-metrics").map(|s| Path::new(s)),
140150
ratchet_metrics:
@@ -170,7 +180,7 @@ pub fn log_config(config: &config) {
170180
logv(c, format!("stage_id: {}", config.stage_id));
171181
logv(c, format!("mode: {}", mode_str(config.mode)));
172182
logv(c, format!("run_ignored: {}", config.run_ignored));
173-
logv(c, format!("filter: {}", opt_str(&config.filter)));
183+
logv(c, format!("filter: {}", if config.filter.is_some() { "(regex)" } else { "(none)" }));
174184
logv(c, format!("runtool: {}", opt_str(&config.runtool)));
175185
logv(c, format!("host-rustcflags: {}", opt_str(&config.host_rustcflags)));
176186
logv(c, format!("target-rustcflags: {}", opt_str(&config.target_rustcflags)));

src/doc/guide-testing.md

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,15 @@ fn test_out_of_bounds_failure() {
9090
~~~
9191

9292
A test runner built with the `--test` flag supports a limited set of
93-
arguments to control which tests are run: the first free argument
94-
passed to a test runner specifies a filter used to narrow down the set
95-
of tests being run; the `--ignored` flag tells the test runner to run
96-
only tests with the `ignore` attribute.
93+
arguments to control which tests are run:
94+
95+
- the first free argument passed to a test runner is interpreted as a
96+
regular expression
97+
([syntax reference](regex/index.html#syntax))
98+
and is used to narrow down the set of tests being run. Note: a plain
99+
string is a valid regular expression that matches itself.
100+
- the `--ignored` flag tells the test runner to run only tests with the
101+
`ignore` attribute.
97102

98103
## Parallelism
99104

@@ -146,16 +151,31 @@ result: FAILED. 1 passed; 1 failed; 0 ignored
146151

147152
### Running a subset of tests
148153

154+
Using a plain string:
155+
156+
~~~ {.notrust}
157+
$ mytests mytest23
158+
159+
running 1 tests
160+
running driver::tests::mytest23 ... ok
161+
162+
result: ok. 1 passed; 0 failed; 0 ignored
163+
~~~
164+
165+
Using some regular expression features:
166+
149167
~~~ {.notrust}
150-
$ mytests mytest1
168+
$ mytests 'mytest[145]'
151169
152-
running 11 tests
170+
running 13 tests
153171
running driver::tests::mytest1 ... ok
172+
running driver::tests::mytest4 ... ok
173+
running driver::tests::mytest5 ... ok
154174
running driver::tests::mytest10 ... ignored
155175
... snip ...
156176
running driver::tests::mytest19 ... ok
157177
158-
result: ok. 11 passed; 0 failed; 1 ignored
178+
result: ok. 13 passed; 0 failed; 1 ignored
159179
~~~
160180

161181
# Microbenchmarking

src/libtest/lib.rs

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
extern crate collections;
3939
extern crate getopts;
40+
extern crate regex;
4041
extern crate serialize;
4142
extern crate term;
4243
extern crate time;
@@ -45,6 +46,7 @@ use collections::TreeMap;
4546
use stats::Stats;
4647
use time::precise_time_ns;
4748
use getopts::{OptGroup, optflag, optopt};
49+
use regex::Regex;
4850
use serialize::{json, Decodable};
4951
use serialize::json::{Json, ToJson};
5052
use term::Terminal;
@@ -255,7 +257,7 @@ pub fn test_main_static(args: &[~str], tests: &[TestDescAndFn]) {
255257
}
256258

257259
pub struct TestOpts {
258-
pub filter: Option<~str>,
260+
pub filter: Option<Regex>,
259261
pub run_ignored: bool,
260262
pub run_tests: bool,
261263
pub run_benchmarks: bool,
@@ -316,8 +318,8 @@ fn usage(binary: &str, helpstr: &str) {
316318
println!("");
317319
if helpstr == "help" {
318320
println!("{}", "\
319-
The FILTER is matched against the name of all tests to run, and if any tests
320-
have a substring match, only those tests are run.
321+
The FILTER regex is matched against the name of all tests to run, and
322+
only those tests that match are run.
321323
322324
By default, all tests are run in parallel. This can be altered with the
323325
RUST_TEST_TASKS environment variable when running tests (set it to 1).
@@ -354,12 +356,15 @@ pub fn parse_opts(args: &[~str]) -> Option<OptRes> {
354356
if matches.opt_present("h") { usage(args[0], "h"); return None; }
355357
if matches.opt_present("help") { usage(args[0], "help"); return None; }
356358

357-
let filter =
358-
if matches.free.len() > 0 {
359-
Some((*matches.free.get(0)).clone())
360-
} else {
361-
None
362-
};
359+
let filter = if matches.free.len() > 0 {
360+
let s = matches.free.get(0).as_slice();
361+
match Regex::new(s) {
362+
Ok(re) => Some(re),
363+
Err(e) => return Some(Err(format!("could not parse /{}/: {}", s, e)))
364+
}
365+
} else {
366+
None
367+
};
363368

364369
let run_ignored = matches.opt_present("ignored");
365370

@@ -925,24 +930,12 @@ pub fn filter_tests(
925930
let mut filtered = tests;
926931

927932
// Remove tests that don't match the test filter
928-
filtered = if opts.filter.is_none() {
929-
filtered
930-
} else {
931-
let filter_str = match opts.filter {
932-
Some(ref f) => (*f).clone(),
933-
None => "".to_owned()
934-
};
935-
936-
fn filter_fn(test: TestDescAndFn, filter_str: &str) ->
937-
Option<TestDescAndFn> {
938-
if test.desc.name.to_str().contains(filter_str) {
939-
return Some(test);
940-
} else {
941-
return None;
942-
}
933+
filtered = match opts.filter {
934+
None => filtered,
935+
Some(ref re) => {
936+
filtered.move_iter()
937+
.filter(|test| re.is_match(test.desc.name.as_slice())).collect()
943938
}
944-
945-
filtered.move_iter().filter_map(|x| filter_fn(x, filter_str)).collect()
946939
};
947940

948941
// Maybe pull out the ignored test and unignore them
@@ -1422,12 +1415,12 @@ mod tests {
14221415

14231416
#[test]
14241417
fn first_free_arg_should_be_a_filter() {
1425-
let args = vec!("progname".to_owned(), "filter".to_owned());
1418+
let args = vec!("progname".to_owned(), "some_regex_filter".to_owned());
14261419
let opts = match parse_opts(args.as_slice()) {
14271420
Some(Ok(o)) => o,
14281421
_ => fail!("Malformed arg in first_free_arg_should_be_a_filter")
14291422
};
1430-
assert!("filter" == opts.filter.clone().unwrap());
1423+
assert!(opts.filter.expect("should've found filter").is_match("some_regex_filter"))
14311424
}
14321425

14331426
#[test]
@@ -1518,6 +1511,37 @@ mod tests {
15181511
}
15191512
}
15201513

1514+
#[test]
1515+
pub fn filter_tests_regex() {
1516+
let mut opts = TestOpts::new();
1517+
opts.filter = Some(::regex::Regex::new("a.*b.+c").unwrap());
1518+
1519+
let mut names = ["yes::abXc", "yes::aXXXbXXXXc",
1520+
"no::XYZ", "no::abc"];
1521+
names.sort();
1522+
1523+
fn test_fn() {}
1524+
let tests = names.iter().map(|name| {
1525+
TestDescAndFn {
1526+
desc: TestDesc {
1527+
name: DynTestName(name.to_owned()),
1528+
ignore: false,
1529+
should_fail: false
1530+
},
1531+
testfn: DynTestFn(test_fn)
1532+
}
1533+
}).collect();
1534+
let filtered = filter_tests(&opts, tests);
1535+
1536+
let expected: Vec<&str> =
1537+
names.iter().map(|&s| s).filter(|name| name.starts_with("yes")).collect();
1538+
1539+
assert_eq!(filtered.len(), expected.len());
1540+
for (test, expected_name) in filtered.iter().zip(expected.iter()) {
1541+
assert_eq!(test.desc.name.as_slice(), *expected_name);
1542+
}
1543+
}
1544+
15211545
#[test]
15221546
pub fn test_metricmap_compare() {
15231547
let mut m1 = MetricMap::new();

0 commit comments

Comments
 (0)