Allow message specification for should_fail

The test harness will make sure that the panic message contains the
specified string. This is useful to help make `#[should_fail]` tests a
bit less brittle by decreasing the chance that the test isn't
"accidentally" passing due to a panic occurring earlier than expected.
The behavior is in some ways similar to JUnit's `expected` feature:
`@Test(expected=NullPointerException.class)`.

Without the message assertion, this test would pass even though it's not
actually reaching the intended part of the code:
```rust
 #[test]
 #[should_fail(message = "out of bounds")]
fn test_oob_array_access() {
    let idx: uint = from_str("13o").unwrap(); // oops, this will panic
    [1i32, 2, 3][idx];
}
```
This commit is contained in:
Steven Fackler 2014-12-04 23:02:36 -08:00
parent de83d7dd19
commit 616af6eb83
6 changed files with 150 additions and 29 deletions

View File

@ -346,7 +346,7 @@ pub fn make_test(config: &Config, testfile: &Path, f: || -> test::TestFn)
desc: test::TestDesc { desc: test::TestDesc {
name: make_test_name(config, testfile), name: make_test_name(config, testfile),
ignore: header::is_test_ignored(config, testfile), ignore: header::is_test_ignored(config, testfile),
should_fail: false should_fail: test::ShouldFail::No,
}, },
testfn: f(), testfn: f(),
} }

View File

@ -280,7 +280,7 @@ impl Collector {
desc: testing::TestDesc { desc: testing::TestDesc {
name: testing::DynTestName(name), name: testing::DynTestName(name),
ignore: should_ignore, ignore: should_ignore,
should_fail: false, // compiler failures are test failures should_fail: testing::ShouldFail::No, // compiler failures are test failures
}, },
testfn: testing::DynTestFn(proc() { testfn: testing::DynTestFn(proc() {
runtest(test.as_slice(), runtest(test.as_slice(),

View File

@ -37,12 +37,17 @@ use {ast, ast_util};
use ptr::P; use ptr::P;
use util::small_vector::SmallVector; use util::small_vector::SmallVector;
enum ShouldFail {
No,
Yes(Option<InternedString>),
}
struct Test { struct Test {
span: Span, span: Span,
path: Vec<ast::Ident> , path: Vec<ast::Ident> ,
bench: bool, bench: bool,
ignore: bool, ignore: bool,
should_fail: bool should_fail: ShouldFail
} }
struct TestCtxt<'a> { struct TestCtxt<'a> {
@ -360,8 +365,16 @@ fn is_ignored(i: &ast::Item) -> bool {
i.attrs.iter().any(|attr| attr.check_name("ignore")) i.attrs.iter().any(|attr| attr.check_name("ignore"))
} }
fn should_fail(i: &ast::Item) -> bool { fn should_fail(i: &ast::Item) -> ShouldFail {
attr::contains_name(i.attrs.as_slice(), "should_fail") match i.attrs.iter().find(|attr| attr.check_name("should_fail")) {
Some(attr) => {
let msg = attr.meta_item_list()
.and_then(|list| list.iter().find(|mi| mi.check_name("message")))
.and_then(|mi| mi.value_str());
ShouldFail::Yes(msg)
}
None => ShouldFail::No,
}
} }
/* /*
@ -550,7 +563,20 @@ fn mk_test_desc_and_fn_rec(cx: &TestCtxt, test: &Test) -> P<ast::Expr> {
vec![name_expr]); vec![name_expr]);
let ignore_expr = ecx.expr_bool(span, test.ignore); let ignore_expr = ecx.expr_bool(span, test.ignore);
let fail_expr = ecx.expr_bool(span, test.should_fail); let should_fail_path = |name| {
ecx.path(span, vec![self_id, test_id, ecx.ident_of("ShouldFail"), ecx.ident_of(name)])
};
let fail_expr = match test.should_fail {
ShouldFail::No => ecx.expr_path(should_fail_path("No")),
ShouldFail::Yes(ref msg) => {
let path = should_fail_path("Yes");
let arg = match *msg {
Some(ref msg) => ecx.expr_some(span, ecx.expr_str(span, msg.clone())),
None => ecx.expr_none(span),
};
ecx.expr_call(span, ecx.expr_path(path), vec![arg])
}
};
// self::test::TestDesc { ... } // self::test::TestDesc { ... }
let desc_expr = ecx.expr_struct( let desc_expr = ecx.expr_struct(

View File

@ -47,6 +47,7 @@ use self::TestEvent::*;
use self::NamePadding::*; use self::NamePadding::*;
use self::OutputLocation::*; use self::OutputLocation::*;
use std::any::{Any, AnyRefExt};
use std::collections::TreeMap; use std::collections::TreeMap;
use stats::Stats; use stats::Stats;
use getopts::{OptGroup, optflag, optopt}; use getopts::{OptGroup, optflag, optopt};
@ -78,7 +79,7 @@ pub mod test {
MetricChange, Improvement, Regression, LikelyNoise, MetricChange, Improvement, Regression, LikelyNoise,
StaticTestFn, StaticTestName, DynTestName, DynTestFn, StaticTestFn, StaticTestName, DynTestName, DynTestFn,
run_test, test_main, test_main_static, filter_tests, run_test, test_main, test_main_static, filter_tests,
parse_opts, StaticBenchFn}; parse_opts, StaticBenchFn, ShouldFail};
} }
pub mod stats; pub mod stats;
@ -184,13 +185,19 @@ pub struct Bencher {
pub bytes: u64, pub bytes: u64,
} }
#[deriving(Clone, Show, PartialEq, Eq, Hash)]
pub enum ShouldFail {
No,
Yes(Option<&'static str>)
}
// The definition of a single test. A test runner will run a list of // The definition of a single test. A test runner will run a list of
// these. // these.
#[deriving(Clone, Show, PartialEq, Eq, Hash)] #[deriving(Clone, Show, PartialEq, Eq, Hash)]
pub struct TestDesc { pub struct TestDesc {
pub name: TestName, pub name: TestName,
pub ignore: bool, pub ignore: bool,
pub should_fail: bool, pub should_fail: ShouldFail,
} }
#[deriving(Show)] #[deriving(Show)]
@ -346,7 +353,7 @@ fn optgroups() -> Vec<getopts::OptGroup> {
fn usage(binary: &str) { fn usage(binary: &str) {
let message = format!("Usage: {} [OPTIONS] [FILTER]", binary); let message = format!("Usage: {} [OPTIONS] [FILTER]", binary);
println!(r"{usage} println!(r#"{usage}
The FILTER regex is tested against the name of all tests to run, and The FILTER regex is tested against the name of all tests to run, and
only those tests that match are run. only those tests that match are run.
@ -366,10 +373,12 @@ Test Attributes:
function takes one argument (test::Bencher). 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 panic!) the code causes a failure (an assertion failure or panic!)
A message may be provided, which the failure string must
contain: #[should_fail(message = "foo")].
#[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 test, then the test runner will ignore these tests during
normal test runs. Running with --ignored will run these normal test runs. Running with --ignored will run these
tests.", tests."#,
usage = getopts::usage(message.as_slice(), usage = getopts::usage(message.as_slice(),
optgroups().as_slice())); optgroups().as_slice()));
} }
@ -902,13 +911,13 @@ fn should_sort_failures_before_printing_them() {
let test_a = TestDesc { let test_a = TestDesc {
name: StaticTestName("a"), name: StaticTestName("a"),
ignore: false, ignore: false,
should_fail: false should_fail: ShouldFail::No
}; };
let test_b = TestDesc { let test_b = TestDesc {
name: StaticTestName("b"), name: StaticTestName("b"),
ignore: false, ignore: false,
should_fail: false should_fail: ShouldFail::No
}; };
let mut st = ConsoleTestState { let mut st = ConsoleTestState {
@ -1114,7 +1123,7 @@ pub fn run_test(opts: &TestOpts,
let stdout = reader.read_to_end().unwrap().into_iter().collect(); let stdout = reader.read_to_end().unwrap().into_iter().collect();
let task_result = result_future.into_inner(); let task_result = result_future.into_inner();
let test_result = calc_result(&desc, task_result.is_ok()); let test_result = calc_result(&desc, task_result);
monitor_ch.send((desc.clone(), test_result, stdout)); monitor_ch.send((desc.clone(), test_result, stdout));
}) })
} }
@ -1148,13 +1157,17 @@ pub fn run_test(opts: &TestOpts,
} }
} }
fn calc_result(desc: &TestDesc, task_succeeded: bool) -> TestResult { fn calc_result(desc: &TestDesc, task_result: Result<(), Box<Any+Send>>) -> TestResult {
if task_succeeded { match (&desc.should_fail, task_result) {
if desc.should_fail { TrFailed } (&ShouldFail::No, Ok(())) |
else { TrOk } (&ShouldFail::Yes(None), Err(_)) => TrOk,
} else { (&ShouldFail::Yes(Some(msg)), Err(ref err))
if desc.should_fail { TrOk } if err.downcast_ref::<String>()
else { TrFailed } .map(|e| &**e)
.or_else(|| err.downcast_ref::<&'static str>().map(|e| *e))
.map(|e| e.contains(msg))
.unwrap_or(false) => TrOk,
_ => TrFailed,
} }
} }
@ -1437,7 +1450,7 @@ mod tests {
TestDesc, TestDescAndFn, TestOpts, run_test, TestDesc, TestDescAndFn, TestOpts, run_test,
Metric, MetricMap, MetricAdded, MetricRemoved, Metric, MetricMap, MetricAdded, MetricRemoved,
Improvement, Regression, LikelyNoise, Improvement, Regression, LikelyNoise,
StaticTestName, DynTestName, DynTestFn}; StaticTestName, DynTestName, DynTestFn, ShouldFail};
use std::io::TempDir; use std::io::TempDir;
#[test] #[test]
@ -1447,7 +1460,7 @@ mod tests {
desc: TestDesc { desc: TestDesc {
name: StaticTestName("whatever"), name: StaticTestName("whatever"),
ignore: true, ignore: true,
should_fail: false should_fail: ShouldFail::No,
}, },
testfn: DynTestFn(proc() f()), testfn: DynTestFn(proc() f()),
}; };
@ -1464,7 +1477,7 @@ mod tests {
desc: TestDesc { desc: TestDesc {
name: StaticTestName("whatever"), name: StaticTestName("whatever"),
ignore: true, ignore: true,
should_fail: false should_fail: ShouldFail::No,
}, },
testfn: DynTestFn(proc() f()), testfn: DynTestFn(proc() f()),
}; };
@ -1481,7 +1494,7 @@ mod tests {
desc: TestDesc { desc: TestDesc {
name: StaticTestName("whatever"), name: StaticTestName("whatever"),
ignore: false, ignore: false,
should_fail: true should_fail: ShouldFail::Yes(None)
}, },
testfn: DynTestFn(proc() f()), testfn: DynTestFn(proc() f()),
}; };
@ -1491,6 +1504,40 @@ mod tests {
assert!(res == TrOk); assert!(res == TrOk);
} }
#[test]
fn test_should_fail_good_message() {
fn f() { panic!("an error message"); }
let desc = TestDescAndFn {
desc: TestDesc {
name: StaticTestName("whatever"),
ignore: false,
should_fail: ShouldFail::Yes(Some("error message"))
},
testfn: DynTestFn(proc() f()),
};
let (tx, rx) = channel();
run_test(&TestOpts::new(), false, desc, tx);
let (_, res, _) = rx.recv();
assert!(res == TrOk);
}
#[test]
fn test_should_fail_bad_message() {
fn f() { panic!("an error message"); }
let desc = TestDescAndFn {
desc: TestDesc {
name: StaticTestName("whatever"),
ignore: false,
should_fail: ShouldFail::Yes(Some("foobar"))
},
testfn: DynTestFn(proc() f()),
};
let (tx, rx) = channel();
run_test(&TestOpts::new(), false, desc, tx);
let (_, res, _) = rx.recv();
assert!(res == TrFailed);
}
#[test] #[test]
fn test_should_fail_but_succeeds() { fn test_should_fail_but_succeeds() {
fn f() { } fn f() { }
@ -1498,7 +1545,7 @@ mod tests {
desc: TestDesc { desc: TestDesc {
name: StaticTestName("whatever"), name: StaticTestName("whatever"),
ignore: false, ignore: false,
should_fail: true should_fail: ShouldFail::Yes(None)
}, },
testfn: DynTestFn(proc() f()), testfn: DynTestFn(proc() f()),
}; };
@ -1544,7 +1591,7 @@ mod tests {
desc: TestDesc { desc: TestDesc {
name: StaticTestName("1"), name: StaticTestName("1"),
ignore: true, ignore: true,
should_fail: false, should_fail: ShouldFail::No,
}, },
testfn: DynTestFn(proc() {}), testfn: DynTestFn(proc() {}),
}, },
@ -1552,7 +1599,7 @@ mod tests {
desc: TestDesc { desc: TestDesc {
name: StaticTestName("2"), name: StaticTestName("2"),
ignore: false, ignore: false,
should_fail: false should_fail: ShouldFail::No,
}, },
testfn: DynTestFn(proc() {}), testfn: DynTestFn(proc() {}),
}); });
@ -1588,7 +1635,7 @@ mod tests {
desc: TestDesc { desc: TestDesc {
name: DynTestName((*name).clone()), name: DynTestName((*name).clone()),
ignore: false, ignore: false,
should_fail: false should_fail: ShouldFail::No,
}, },
testfn: DynTestFn(testfn), testfn: DynTestFn(testfn),
}; };
@ -1629,7 +1676,7 @@ mod tests {
desc: TestDesc { desc: TestDesc {
name: DynTestName(name.to_string()), name: DynTestName(name.to_string()),
ignore: false, ignore: false,
should_fail: false should_fail: ShouldFail::No,
}, },
testfn: DynTestFn(test_fn) testfn: DynTestFn(test_fn)
} }

View File

@ -0,0 +1,22 @@
// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// check-stdout
// error-pattern:task 'test_foo' panicked at
// compile-flags: --test
// ignore-pretty: does not work well with `--test`
#[test]
#[should_fail(message = "foobar")]
fn test_foo() {
panic!("blah")
}

View File

@ -0,0 +1,26 @@
// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// compile-flags: --test
// ignore-pretty: does not work well with `--test`
#[test]
#[should_fail(message = "foo")]
fn test_foo() {
panic!("foo bar")
}
#[test]
#[should_fail(message = "foo")]
fn test_foo_dynamic() {
panic!("{} bar", "foo")
}