Skip to content

Commit c7a9b49

Browse files
committed
auto merge of #19560 : sfackler/rust/should-fail-reason, r=alexcrichton
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]; } ```
2 parents cf0b4e0 + a20926a commit c7a9b49

File tree

7 files changed

+165
-29
lines changed

7 files changed

+165
-29
lines changed

src/compiletest/compiletest.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ pub fn make_test(config: &Config, testfile: &Path, f: || -> test::TestFn)
346346
desc: test::TestDesc {
347347
name: make_test_name(config, testfile),
348348
ignore: header::is_test_ignored(config, testfile),
349-
should_fail: false
349+
should_fail: test::ShouldFail::No,
350350
},
351351
testfn: f(),
352352
}

src/doc/guide-testing.md

+15
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ fn test_out_of_bounds_failure() {
8989
}
9090
~~~
9191

92+
`#[should_fail]` tests can be fragile as it's hard to guarantee that the test
93+
didn't fail for an unexpected reason. To help with this, an optional `expected`
94+
parameter can be added to the `should_fail` attribute. The test harness will
95+
make sure that the failure message contains the provided text. A safer version
96+
of the example above would be:
97+
98+
~~~test_harness
99+
#[test]
100+
#[should_fail(expected = "index out of bounds")]
101+
fn test_out_of_bounds_failure() {
102+
let v: &[int] = &[];
103+
v[0];
104+
}
105+
~~~
106+
92107
A test runner built with the `--test` flag supports a limited set of
93108
arguments to control which tests are run:
94109

src/librustdoc/test.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ impl Collector {
280280
desc: testing::TestDesc {
281281
name: testing::DynTestName(name),
282282
ignore: should_ignore,
283-
should_fail: false, // compiler failures are test failures
283+
should_fail: testing::ShouldFail::No, // compiler failures are test failures
284284
},
285285
testfn: testing::DynTestFn(proc() {
286286
runtest(test.as_slice(),

src/libsyntax/test.rs

+30-4
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,17 @@ use {ast, ast_util};
3737
use ptr::P;
3838
use util::small_vector::SmallVector;
3939

40+
enum ShouldFail {
41+
No,
42+
Yes(Option<InternedString>),
43+
}
44+
4045
struct Test {
4146
span: Span,
4247
path: Vec<ast::Ident> ,
4348
bench: bool,
4449
ignore: bool,
45-
should_fail: bool
50+
should_fail: ShouldFail
4651
}
4752

4853
struct TestCtxt<'a> {
@@ -360,8 +365,16 @@ fn is_ignored(i: &ast::Item) -> bool {
360365
i.attrs.iter().any(|attr| attr.check_name("ignore"))
361366
}
362367

363-
fn should_fail(i: &ast::Item) -> bool {
364-
attr::contains_name(i.attrs.as_slice(), "should_fail")
368+
fn should_fail(i: &ast::Item) -> ShouldFail {
369+
match i.attrs.iter().find(|attr| attr.check_name("should_fail")) {
370+
Some(attr) => {
371+
let msg = attr.meta_item_list()
372+
.and_then(|list| list.iter().find(|mi| mi.check_name("expected")))
373+
.and_then(|mi| mi.value_str());
374+
ShouldFail::Yes(msg)
375+
}
376+
None => ShouldFail::No,
377+
}
365378
}
366379

367380
/*
@@ -550,7 +563,20 @@ fn mk_test_desc_and_fn_rec(cx: &TestCtxt, test: &Test) -> P<ast::Expr> {
550563
vec![name_expr]);
551564

552565
let ignore_expr = ecx.expr_bool(span, test.ignore);
553-
let fail_expr = ecx.expr_bool(span, test.should_fail);
566+
let should_fail_path = |name| {
567+
ecx.path(span, vec![self_id, test_id, ecx.ident_of("ShouldFail"), ecx.ident_of(name)])
568+
};
569+
let fail_expr = match test.should_fail {
570+
ShouldFail::No => ecx.expr_path(should_fail_path("No")),
571+
ShouldFail::Yes(ref msg) => {
572+
let path = should_fail_path("Yes");
573+
let arg = match *msg {
574+
Some(ref msg) => ecx.expr_some(span, ecx.expr_str(span, msg.clone())),
575+
None => ecx.expr_none(span),
576+
};
577+
ecx.expr_call(span, ecx.expr_path(path), vec![arg])
578+
}
579+
};
554580

555581
// self::test::TestDesc { ... }
556582
let desc_expr = ecx.expr_struct(

src/libtest/lib.rs

+70-23
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use self::TestEvent::*;
4747
use self::NamePadding::*;
4848
use self::OutputLocation::*;
4949

50+
use std::any::{Any, AnyRefExt};
5051
use std::collections::TreeMap;
5152
use stats::Stats;
5253
use getopts::{OptGroup, optflag, optopt};
@@ -78,7 +79,7 @@ pub mod test {
7879
MetricChange, Improvement, Regression, LikelyNoise,
7980
StaticTestFn, StaticTestName, DynTestName, DynTestFn,
8081
run_test, test_main, test_main_static, filter_tests,
81-
parse_opts, StaticBenchFn};
82+
parse_opts, StaticBenchFn, ShouldFail};
8283
}
8384

8485
pub mod stats;
@@ -184,13 +185,19 @@ pub struct Bencher {
184185
pub bytes: u64,
185186
}
186187

188+
#[deriving(Clone, Show, PartialEq, Eq, Hash)]
189+
pub enum ShouldFail {
190+
No,
191+
Yes(Option<&'static str>)
192+
}
193+
187194
// The definition of a single test. A test runner will run a list of
188195
// these.
189196
#[deriving(Clone, Show, PartialEq, Eq, Hash)]
190197
pub struct TestDesc {
191198
pub name: TestName,
192199
pub ignore: bool,
193-
pub should_fail: bool,
200+
pub should_fail: ShouldFail,
194201
}
195202

196203
#[deriving(Show)]
@@ -346,7 +353,7 @@ fn optgroups() -> Vec<getopts::OptGroup> {
346353

347354
fn usage(binary: &str) {
348355
let message = format!("Usage: {} [OPTIONS] [FILTER]", binary);
349-
println!(r"{usage}
356+
println!(r#"{usage}
350357
351358
The FILTER regex is tested against the name of all tests to run, and
352359
only those tests that match are run.
@@ -366,10 +373,12 @@ Test Attributes:
366373
function takes one argument (test::Bencher).
367374
#[should_fail] - This function (also labeled with #[test]) will only pass if
368375
the code causes a failure (an assertion failure or panic!)
376+
A message may be provided, which the failure string must
377+
contain: #[should_fail(expected = "foo")].
369378
#[ignore] - When applied to a function which is already attributed as a
370379
test, then the test runner will ignore these tests during
371380
normal test runs. Running with --ignored will run these
372-
tests.",
381+
tests."#,
373382
usage = getopts::usage(message.as_slice(),
374383
optgroups().as_slice()));
375384
}
@@ -902,13 +911,13 @@ fn should_sort_failures_before_printing_them() {
902911
let test_a = TestDesc {
903912
name: StaticTestName("a"),
904913
ignore: false,
905-
should_fail: false
914+
should_fail: ShouldFail::No
906915
};
907916

908917
let test_b = TestDesc {
909918
name: StaticTestName("b"),
910919
ignore: false,
911-
should_fail: false
920+
should_fail: ShouldFail::No
912921
};
913922

914923
let mut st = ConsoleTestState {
@@ -1114,7 +1123,7 @@ pub fn run_test(opts: &TestOpts,
11141123

11151124
let stdout = reader.read_to_end().unwrap().into_iter().collect();
11161125
let task_result = result_future.into_inner();
1117-
let test_result = calc_result(&desc, task_result.is_ok());
1126+
let test_result = calc_result(&desc, task_result);
11181127
monitor_ch.send((desc.clone(), test_result, stdout));
11191128
})
11201129
}
@@ -1148,13 +1157,17 @@ pub fn run_test(opts: &TestOpts,
11481157
}
11491158
}
11501159

1151-
fn calc_result(desc: &TestDesc, task_succeeded: bool) -> TestResult {
1152-
if task_succeeded {
1153-
if desc.should_fail { TrFailed }
1154-
else { TrOk }
1155-
} else {
1156-
if desc.should_fail { TrOk }
1157-
else { TrFailed }
1160+
fn calc_result(desc: &TestDesc, task_result: Result<(), Box<Any+Send>>) -> TestResult {
1161+
match (&desc.should_fail, task_result) {
1162+
(&ShouldFail::No, Ok(())) |
1163+
(&ShouldFail::Yes(None), Err(_)) => TrOk,
1164+
(&ShouldFail::Yes(Some(msg)), Err(ref err))
1165+
if err.downcast_ref::<String>()
1166+
.map(|e| &**e)
1167+
.or_else(|| err.downcast_ref::<&'static str>().map(|e| *e))
1168+
.map(|e| e.contains(msg))
1169+
.unwrap_or(false) => TrOk,
1170+
_ => TrFailed,
11581171
}
11591172
}
11601173

@@ -1437,7 +1450,7 @@ mod tests {
14371450
TestDesc, TestDescAndFn, TestOpts, run_test,
14381451
Metric, MetricMap, MetricAdded, MetricRemoved,
14391452
Improvement, Regression, LikelyNoise,
1440-
StaticTestName, DynTestName, DynTestFn};
1453+
StaticTestName, DynTestName, DynTestFn, ShouldFail};
14411454
use std::io::TempDir;
14421455

14431456
#[test]
@@ -1447,7 +1460,7 @@ mod tests {
14471460
desc: TestDesc {
14481461
name: StaticTestName("whatever"),
14491462
ignore: true,
1450-
should_fail: false
1463+
should_fail: ShouldFail::No,
14511464
},
14521465
testfn: DynTestFn(proc() f()),
14531466
};
@@ -1464,7 +1477,7 @@ mod tests {
14641477
desc: TestDesc {
14651478
name: StaticTestName("whatever"),
14661479
ignore: true,
1467-
should_fail: false
1480+
should_fail: ShouldFail::No,
14681481
},
14691482
testfn: DynTestFn(proc() f()),
14701483
};
@@ -1481,7 +1494,24 @@ mod tests {
14811494
desc: TestDesc {
14821495
name: StaticTestName("whatever"),
14831496
ignore: false,
1484-
should_fail: true
1497+
should_fail: ShouldFail::Yes(None)
1498+
},
1499+
testfn: DynTestFn(proc() f()),
1500+
};
1501+
let (tx, rx) = channel();
1502+
run_test(&TestOpts::new(), false, desc, tx);
1503+
let (_, res, _) = rx.recv();
1504+
assert!(res == TrOk);
1505+
}
1506+
1507+
#[test]
1508+
fn test_should_fail_good_message() {
1509+
fn f() { panic!("an error message"); }
1510+
let desc = TestDescAndFn {
1511+
desc: TestDesc {
1512+
name: StaticTestName("whatever"),
1513+
ignore: false,
1514+
should_fail: ShouldFail::Yes(Some("error message"))
14851515
},
14861516
testfn: DynTestFn(proc() f()),
14871517
};
@@ -1491,14 +1521,31 @@ mod tests {
14911521
assert!(res == TrOk);
14921522
}
14931523

1524+
#[test]
1525+
fn test_should_fail_bad_message() {
1526+
fn f() { panic!("an error message"); }
1527+
let desc = TestDescAndFn {
1528+
desc: TestDesc {
1529+
name: StaticTestName("whatever"),
1530+
ignore: false,
1531+
should_fail: ShouldFail::Yes(Some("foobar"))
1532+
},
1533+
testfn: DynTestFn(proc() f()),
1534+
};
1535+
let (tx, rx) = channel();
1536+
run_test(&TestOpts::new(), false, desc, tx);
1537+
let (_, res, _) = rx.recv();
1538+
assert!(res == TrFailed);
1539+
}
1540+
14941541
#[test]
14951542
fn test_should_fail_but_succeeds() {
14961543
fn f() { }
14971544
let desc = TestDescAndFn {
14981545
desc: TestDesc {
14991546
name: StaticTestName("whatever"),
15001547
ignore: false,
1501-
should_fail: true
1548+
should_fail: ShouldFail::Yes(None)
15021549
},
15031550
testfn: DynTestFn(proc() f()),
15041551
};
@@ -1544,15 +1591,15 @@ mod tests {
15441591
desc: TestDesc {
15451592
name: StaticTestName("1"),
15461593
ignore: true,
1547-
should_fail: false,
1594+
should_fail: ShouldFail::No,
15481595
},
15491596
testfn: DynTestFn(proc() {}),
15501597
},
15511598
TestDescAndFn {
15521599
desc: TestDesc {
15531600
name: StaticTestName("2"),
15541601
ignore: false,
1555-
should_fail: false
1602+
should_fail: ShouldFail::No,
15561603
},
15571604
testfn: DynTestFn(proc() {}),
15581605
});
@@ -1588,7 +1635,7 @@ mod tests {
15881635
desc: TestDesc {
15891636
name: DynTestName((*name).clone()),
15901637
ignore: false,
1591-
should_fail: false
1638+
should_fail: ShouldFail::No,
15921639
},
15931640
testfn: DynTestFn(testfn),
15941641
};
@@ -1629,7 +1676,7 @@ mod tests {
16291676
desc: TestDesc {
16301677
name: DynTestName(name.to_string()),
16311678
ignore: false,
1632-
should_fail: false
1679+
should_fail: ShouldFail::No,
16331680
},
16341681
testfn: DynTestFn(test_fn)
16351682
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
2+
// file at the top-level directory of this distribution and at
3+
// http://rust-lang.org/COPYRIGHT.
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
// check-stdout
12+
// error-pattern:task 'test_foo' panicked at
13+
// compile-flags: --test
14+
// ignore-pretty: does not work well with `--test`
15+
16+
#[test]
17+
#[should_fail(expected = "foobar")]
18+
fn test_foo() {
19+
panic!("blah")
20+
}
21+
22+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
2+
// file at the top-level directory of this distribution and at
3+
// http://rust-lang.org/COPYRIGHT.
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
// compile-flags: --test
12+
// ignore-pretty: does not work well with `--test`
13+
14+
#[test]
15+
#[should_fail(expected = "foo")]
16+
fn test_foo() {
17+
panic!("foo bar")
18+
}
19+
20+
#[test]
21+
#[should_fail(expected = "foo")]
22+
fn test_foo_dynamic() {
23+
panic!("{} bar", "foo")
24+
}
25+
26+

0 commit comments

Comments
 (0)