diff options
| author | John Renner <john@jrenner.net> | 2018-07-20 18:04:02 -0700 |
|---|---|---|
| committer | John Renner <john@jrenner.net> | 2018-09-04 22:33:00 -0700 |
| commit | 9b27de41d4e00cb6c23df270572472fd4c6f47f8 (patch) | |
| tree | 9f51c7baf3dd1fe2b1d021007b36aa70fb36689d /src/libsyntax | |
| parent | 0be2c303692cab31390e52701007cfa87867bf74 (diff) | |
| download | rust-9b27de41d4e00cb6c23df270572472fd4c6f47f8.tar.gz rust-9b27de41d4e00cb6c23df270572472fd4c6f47f8.zip | |
Introduce Custom Test Frameworks
Diffstat (limited to 'src/libsyntax')
| -rw-r--r-- | src/libsyntax/ast.rs | 2 | ||||
| -rw-r--r-- | src/libsyntax/config.rs | 8 | ||||
| -rw-r--r-- | src/libsyntax/ext/expand.rs | 48 | ||||
| -rw-r--r-- | src/libsyntax/feature_gate.rs | 12 | ||||
| -rw-r--r-- | src/libsyntax/test.rs | 580 |
5 files changed, 131 insertions, 519 deletions
diff --git a/src/libsyntax/ast.rs b/src/libsyntax/ast.rs index 72f1791ef7c..9851749be37 100644 --- a/src/libsyntax/ast.rs +++ b/src/libsyntax/ast.rs @@ -1587,7 +1587,7 @@ impl TyKind { if let TyKind::ImplicitSelf = *self { true } else { false } } - crate fn is_unit(&self) -> bool { + pub fn is_unit(&self) -> bool { if let TyKind::Tup(ref tys) = *self { tys.is_empty() } else { false } } } diff --git a/src/libsyntax/config.rs b/src/libsyntax/config.rs index 0e52434ec01..900d830b4c0 100644 --- a/src/libsyntax/config.rs +++ b/src/libsyntax/config.rs @@ -119,7 +119,7 @@ impl<'a> StripUnconfigured<'a> { pub fn in_cfg(&mut self, attrs: &[ast::Attribute]) -> bool { attrs.iter().all(|attr| { // When not compiling with --test we should not compile the #[test] functions - if !self.should_test && is_test_or_bench(attr) { + if !self.should_test && is_test(attr) { return false; } @@ -249,7 +249,7 @@ impl<'a> StripUnconfigured<'a> { // // NB: This is intentionally not part of the fold_expr() function // in order for fold_opt_expr() to be able to avoid this check - if let Some(attr) = expr.attrs().iter().find(|a| is_cfg(a) || is_test_or_bench(a)) { + if let Some(attr) = expr.attrs().iter().find(|a| is_cfg(a) || is_test(a)) { let msg = "removing an expression is not supported in this position"; self.sess.span_diagnostic.span_err(attr.span, msg); } @@ -353,6 +353,6 @@ fn is_cfg(attr: &ast::Attribute) -> bool { attr.check_name("cfg") } -pub fn is_test_or_bench(attr: &ast::Attribute) -> bool { - attr.check_name("test") || attr.check_name("bench") +pub fn is_test(att: &ast::Attribute) -> bool { + att.check_name("test_case") } diff --git a/src/libsyntax/ext/expand.rs b/src/libsyntax/ext/expand.rs index 6e38f820586..956e086b01b 100644 --- a/src/libsyntax/ext/expand.rs +++ b/src/libsyntax/ext/expand.rs @@ -12,10 +12,9 @@ use ast::{self, Block, Ident, NodeId, PatKind, Path}; use ast::{MacStmtStyle, StmtKind, ItemKind}; use attr::{self, HasAttrs}; use source_map::{ExpnInfo, MacroBang, MacroAttribute, dummy_spanned, respan}; -use config::{is_test_or_bench, StripUnconfigured}; +use config::StripUnconfigured; use errors::{Applicability, FatalError}; use ext::base::*; -use ext::build::AstBuilder; use ext::derive::{add_derived_markers, collect_derives}; use ext::hygiene::{self, Mark, SyntaxContext}; use ext::placeholders::{placeholder, PlaceholderExpander}; @@ -37,7 +36,6 @@ use visit::{self, Visitor}; use rustc_data_structures::fx::FxHashMap; use std::fs::File; use std::io::Read; -use std::iter::FromIterator; use std::{iter, mem}; use std::rc::Rc; use std::path::PathBuf; @@ -1366,51 +1364,25 @@ impl<'a, 'b> Folder for InvocationCollector<'a, 'b> { self.cx.current_expansion.directory_ownership = orig_directory_ownership; result } - // Ensure that test functions are accessible from the test harness. + + // Ensure that test items can be exported by the harness generator. // #[test] fn foo() {} // becomes: // #[test] pub fn foo_gensym(){} - // #[allow(unused)] - // use foo_gensym as foo; - ast::ItemKind::Fn(..) if self.cx.ecfg.should_test => { - if self.tests_nameable && item.attrs.iter().any(|attr| is_test_or_bench(attr)) { - let orig_ident = item.ident; - let orig_vis = item.vis.clone(); - + ast::ItemKind::Const(..) + | ast::ItemKind::Static(..) + | ast::ItemKind::Fn(..) if self.cx.ecfg.should_test => { + if self.tests_nameable && attr::contains_name(&item.attrs, "test_case") { // Publicize the item under gensymed name to avoid pollution + // This means #[test_case] items can't be referenced by user code item = item.map(|mut item| { item.vis = respan(item.vis.span, ast::VisibilityKind::Public); item.ident = item.ident.gensym(); item }); - - // Use the gensymed name under the item's original visibility - let mut use_item = self.cx.item_use_simple_( - item.ident.span, - orig_vis, - Some(orig_ident), - self.cx.path(item.ident.span, - vec![keywords::SelfValue.ident(), item.ident])); - - // #[allow(unused)] because the test function probably isn't being referenced - use_item = use_item.map(|mut ui| { - ui.attrs.push( - self.cx.attribute(DUMMY_SP, attr::mk_list_item(DUMMY_SP, - Ident::from_str("allow"), vec![ - attr::mk_nested_word_item(Ident::from_str("unused")) - ] - )) - ); - - ui - }); - - OneVector::from_iter( - self.fold_unnameable(item).into_iter() - .chain(self.fold_unnameable(use_item))) - } else { - self.fold_unnameable(item) } + + self.fold_unnameable(item) } _ => self.fold_unnameable(item), } diff --git a/src/libsyntax/feature_gate.rs b/src/libsyntax/feature_gate.rs index 14781dd8e24..35cb5aa5475 100644 --- a/src/libsyntax/feature_gate.rs +++ b/src/libsyntax/feature_gate.rs @@ -515,6 +515,10 @@ declare_features! ( // unsized rvalues at arguments and parameters (active, unsized_locals, "1.30.0", Some(48055), None), + + // #![test_runner] + // #[test_case] + (active, custom_test_frameworks, "1.30.0", Some(50297), None), ); declare_features! ( @@ -775,6 +779,10 @@ pub const BUILTIN_ATTRIBUTES: &'static [(&'static str, AttributeType, AttributeG ("no_link", Normal, Ungated), ("derive", Normal, Ungated), ("should_panic", Normal, Ungated), + ("test_case", Normal, Gated(Stability::Unstable, + "custom_test_frameworks", + "Custom test frameworks are experimental", + cfg_fn!(custom_test_frameworks))), ("ignore", Normal, Ungated), ("no_implicit_prelude", Normal, Ungated), ("reexport_test_harness_main", Normal, Ungated), @@ -1156,6 +1164,10 @@ pub const BUILTIN_ATTRIBUTES: &'static [(&'static str, AttributeType, AttributeG ("no_builtins", CrateLevel, Ungated), ("recursion_limit", CrateLevel, Ungated), ("type_length_limit", CrateLevel, Ungated), + ("test_runner", CrateLevel, Gated(Stability::Unstable, + "custom_test_frameworks", + "Custom Test Frameworks is an unstable feature", + cfg_fn!(custom_test_frameworks))), ]; // cfg(...)'s that are feature gated diff --git a/src/libsyntax/test.rs b/src/libsyntax/test.rs index 49ab0c2256e..91c63227d30 100644 --- a/src/libsyntax/test.rs +++ b/src/libsyntax/test.rs @@ -22,7 +22,7 @@ use std::vec; use attr::{self, HasAttrs}; use syntax_pos::{self, DUMMY_SP, NO_EXPANSION, Span, SourceFile, BytePos}; -use source_map::{self, SourceMap, ExpnInfo, MacroAttribute, dummy_spanned}; +use source_map::{self, SourceMap, ExpnInfo, MacroAttribute, dummy_spanned, respan}; use errors; use config; use entry::{self, EntryPointType}; @@ -43,29 +43,21 @@ use symbol::{self, Symbol, keywords}; use ThinVec; use rustc_data_structures::small_vec::ExpectOne; -enum ShouldPanic { - No, - Yes(Option<Symbol>), -} - struct Test { span: Span, - path: Vec<Ident> , - bench: bool, - ignore: bool, - should_panic: ShouldPanic, - allow_fail: bool, + path: Vec<Ident>, } struct TestCtxt<'a> { span_diagnostic: &'a errors::Handler, path: Vec<Ident>, ext_cx: ExtCtxt<'a>, - testfns: Vec<Test>, + test_cases: Vec<Test>, reexport_test_harness_main: Option<Symbol>, is_libtest: bool, ctxt: SyntaxContext, features: &'a Features, + test_runner: Option<ast::Path>, // top-level re-export submodule, filled out after folding is finished toplevel_reexport: Option<Ident>, @@ -87,9 +79,13 @@ pub fn modify_for_testing(sess: &ParseSess, attr::first_attr_value_str_by_name(&krate.attrs, "reexport_test_harness_main"); + // Do this here so that the test_runner crate attribute gets marked as used + // even in non-test builds + let test_runner = get_test_runner(span_diagnostic, &krate); + if should_test { generate_test_harness(sess, resolver, reexport_test_harness_main, - krate, span_diagnostic, features) + krate, span_diagnostic, features, test_runner) } else { krate } @@ -107,13 +103,13 @@ impl<'a> fold::Folder for TestHarnessGenerator<'a> { fn fold_crate(&mut self, c: ast::Crate) -> ast::Crate { let mut folded = fold::noop_fold_crate(c, self); - // Add a special __test module to the crate that will contain code - // generated for the test harness - let (mod_, reexport) = mk_test_module(&mut self.cx); - if let Some(re) = reexport { - folded.module.items.push(re) - } - folded.module.items.push(mod_); + // Create a main function to run our tests + let test_main = { + let unresolved = mk_main(&mut self.cx); + self.cx.ext_cx.monotonic_expander().fold_item(unresolved).pop().unwrap() + }; + + folded.module.items.push(test_main); folded } @@ -124,41 +120,18 @@ impl<'a> fold::Folder for TestHarnessGenerator<'a> { } debug!("current path: {}", path_name_i(&self.cx.path)); - if is_test_fn(&self.cx, &i) || is_bench_fn(&self.cx, &i) { - match i.node { - ast::ItemKind::Fn(_, header, _, _) => { - if header.unsafety == ast::Unsafety::Unsafe { - let diag = self.cx.span_diagnostic; - diag.span_fatal( - i.span, - "unsafe functions cannot be used for tests" - ).raise(); - } - if header.asyncness.is_async() { - let diag = self.cx.span_diagnostic; - diag.span_fatal( - i.span, - "async functions cannot be used for tests" - ).raise(); - } - } - _ => {}, - } + let mut item = i.into_inner(); + if is_test_case(&item) { + debug!("this is a test item"); - debug!("this is a test function"); let test = Test { - span: i.span, + span: item.span, path: self.cx.path.clone(), - bench: is_bench_fn(&self.cx, &i), - ignore: is_ignored(&i), - should_panic: should_panic(&i, &self.cx), - allow_fail: is_allowed_fail(&i), }; - self.cx.testfns.push(test); - self.tests.push(i.ident); + self.cx.test_cases.push(test); + self.tests.push(item.ident); } - let mut item = i.into_inner(); // We don't want to recurse into anything other than mods, since // mods or tests inside of functions will break things if let ast::ItemKind::Mod(module) = item.node { @@ -190,6 +163,8 @@ impl<'a> fold::Folder for TestHarnessGenerator<'a> { fn fold_mac(&mut self, mac: ast::Mac) -> ast::Mac { mac } } +/// A folder used to remove any entry points (like fn main) because the harness +/// generator will provide its own struct EntryPointCleaner { // Current depth in the ast depth: usize, @@ -242,6 +217,10 @@ impl fold::Folder for EntryPointCleaner { fn fold_mac(&mut self, mac: ast::Mac) -> ast::Mac { mac } } +/// Creates an item (specifically a module) that "pub use"s the tests passed in. +/// Each tested submodule will contain a similar reexport module that we will export +/// under the name of the original module. That is, `submod::__test_reexports` is +/// reexported like so `pub use submod::__test_reexports as submod`. fn mk_reexport_mod(cx: &mut TestCtxt, parent: ast::NodeId, tests: Vec<Ident>, @@ -279,12 +258,14 @@ fn mk_reexport_mod(cx: &mut TestCtxt, (it, sym) } +/// Crawl over the crate, inserting test reexports and the test main function fn generate_test_harness(sess: &ParseSess, resolver: &mut dyn Resolver, reexport_test_harness_main: Option<Symbol>, krate: ast::Crate, sd: &errors::Handler, - features: &Features) -> ast::Crate { + features: &Features, + test_runner: Option<ast::Path>) -> ast::Crate { // Remove the entry points let mut cleaner = EntryPointCleaner { depth: 0 }; let krate = cleaner.fold_crate(krate); @@ -298,19 +279,20 @@ fn generate_test_harness(sess: &ParseSess, span_diagnostic: sd, ext_cx: ExtCtxt::new(sess, econfig, resolver), path: Vec::new(), - testfns: Vec::new(), + test_cases: Vec::new(), reexport_test_harness_main, // NB: doesn't consider the value of `--crate-name` passed on the command line. is_libtest: attr::find_crate_name(&krate.attrs).map(|s| s == "test").unwrap_or(false), toplevel_reexport: None, ctxt: SyntaxContext::empty().apply_mark(mark), features, + test_runner }; mark.set_expn_info(ExpnInfo { call_site: DUMMY_SP, def_site: None, - format: MacroAttribute(Symbol::intern("test")), + format: MacroAttribute(Symbol::intern("test_case")), allow_internal_unstable: true, allow_internal_unsafe: false, local_inner_macros: false, @@ -344,216 +326,64 @@ enum BadTestSignature { ShouldPanicOnlyWithNoArgs, } -fn is_test_fn(cx: &TestCtxt, i: &ast::Item) -> bool { - let has_test_attr = attr::contains_name(&i.attrs, "test"); - - fn has_test_signature(_cx: &TestCtxt, i: &ast::Item) -> HasTestSignature { - let has_should_panic_attr = attr::contains_name(&i.attrs, "should_panic"); - match i.node { - ast::ItemKind::Fn(ref decl, _, ref generics, _) => { - // If the termination trait is active, the compiler will check that the output - // type implements the `Termination` trait as `libtest` enforces that. - let has_output = match decl.output { - ast::FunctionRetTy::Default(..) => false, - ast::FunctionRetTy::Ty(ref t) if t.node.is_unit() => false, - _ => true - }; - - if !decl.inputs.is_empty() { - return No(BadTestSignature::NoArgumentsAllowed); - } - - match (has_output, has_should_panic_attr) { - (true, true) => No(BadTestSignature::ShouldPanicOnlyWithNoArgs), - (true, false) => if !generics.params.is_empty() { - No(BadTestSignature::WrongTypeSignature) - } else { - Yes - }, - (false, _) => Yes - } - } - _ => No(BadTestSignature::NotEvenAFunction), - } - } - - let has_test_signature = if has_test_attr { - let diag = cx.span_diagnostic; - match has_test_signature(cx, i) { - Yes => true, - No(cause) => { - match cause { - BadTestSignature::NotEvenAFunction => - diag.span_err(i.span, "only functions may be used as tests"), - BadTestSignature::WrongTypeSignature => - diag.span_err(i.span, - "functions used as tests must have signature fn() -> ()"), - BadTestSignature::NoArgumentsAllowed => - diag.span_err(i.span, "functions used as tests can not have any arguments"), - BadTestSignature::ShouldPanicOnlyWithNoArgs => - diag.span_err(i.span, "functions using `#[should_panic]` must return `()`"), - } - false - } - } - } else { - false - }; - - has_test_attr && has_test_signature -} - -fn is_bench_fn(cx: &TestCtxt, i: &ast::Item) -> bool { - let has_bench_attr = attr::contains_name(&i.attrs, "bench"); - - fn has_bench_signature(_cx: &TestCtxt, i: &ast::Item) -> bool { - match i.node { - ast::ItemKind::Fn(ref decl, _, _, _) => { - // NB: inadequate check, but we're running - // well before resolve, can't get too deep. - decl.inputs.len() == 1 - } - _ => false - } - } - - let has_bench_signature = has_bench_signature(cx, i); - - if has_bench_attr && !has_bench_signature { - let diag = cx.span_diagnostic; - - diag.span_err(i.span, "functions used as benches must have signature \ - `fn(&mut Bencher) -> impl Termination`"); - } - - has_bench_attr && has_bench_signature -} - -fn is_ignored(i: &ast::Item) -> bool { - attr::contains_name(&i.attrs, "ignore") -} - -fn is_allowed_fail(i: &ast::Item) -> bool { - attr::contains_name(&i.attrs, "allow_fail") -} - -fn should_panic(i: &ast::Item, cx: &TestCtxt) -> ShouldPanic { - match attr::find_by_name(&i.attrs, "should_panic") { - Some(attr) => { - let sd = cx.span_diagnostic; - if attr.is_value_str() { - sd.struct_span_warn( - attr.span(), - "attribute must be of the form: \ - `#[should_panic]` or \ - `#[should_panic(expected = \"error message\")]`" - ).note("Errors in this attribute were erroneously allowed \ - and will become a hard error in a future release.") - .emit(); - return ShouldPanic::Yes(None); - } - match attr.meta_item_list() { - // Handle #[should_panic] - None => ShouldPanic::Yes(None), - // Handle #[should_panic(expected = "foo")] - Some(list) => { - let msg = list.iter() - .find(|mi| mi.check_name("expected")) - .and_then(|mi| mi.meta_item()) - .and_then(|mi| mi.value_str()); - if list.len() != 1 || msg.is_none() { - sd.struct_span_warn( - attr.span(), - "argument must be of the form: \ - `expected = \"error message\"`" - ).note("Errors in this attribute were erroneously \ - allowed and will become a hard error in a \ - future release.").emit(); - ShouldPanic::Yes(None) - } else { - ShouldPanic::Yes(msg) - } - }, - } - } - None => ShouldPanic::No, - } -} - -/* - -We're going to be building a module that looks more or less like: - -mod __test { - extern crate test (name = "test", vers = "..."); - fn main() { - test::test_main_static(&::os::args()[], tests, test::Options::new()) - } - - static tests : &'static [test::TestDescAndFn] = &[ - ... the list of tests in the crate ... - ]; -} - -*/ - -fn mk_std(cx: &TestCtxt) -> P<ast::Item> { - let id_test = Ident::from_str("test"); - let sp = ignored_span(cx, DUMMY_SP); - let (vi, vis, ident) = if cx.is_libtest { - (ast::ItemKind::Use(P(ast::UseTree { - span: DUMMY_SP, - prefix: path_node(vec![id_test]), - kind: ast::UseTreeKind::Simple(None, ast::DUMMY_NODE_ID, ast::DUMMY_NODE_ID), - })), - ast::VisibilityKind::Public, keywords::Invalid.ident()) - } else { - (ast::ItemKind::ExternCrate(None), ast::VisibilityKind::Inherited, id_test) - }; - P(ast::Item { - id: ast::DUMMY_NODE_ID, - ident, - node: vi, - attrs: vec![], - vis: dummy_spanned(vis), - span: sp, - tokens: None, - }) -} - +/// Creates a function item for use as the main function of a test build. +/// This function will call the `test_runner` as specified by the crate attribute fn mk_main(cx: &mut TestCtxt) -> P<ast::Item> { // Writing this out by hand with 'ignored_span': // pub fn main() { // #![main] - // use std::slice::AsSlice; - // test::test_main_static(::std::os::args().as_slice(), TESTS, test::Options::new()); + // test::test_main_static(::std::os::args().as_slice(), &[..tests]); // } - let sp = ignored_span(cx, DUMMY_SP); let ecx = &cx.ext_cx; - - // test::test_main_static - let test_main_path = - ecx.path(sp, vec![Ident::from_str("test"), Ident::from_str("test_main_static")]); + let test_id = ecx.ident_of("test").gensym(); // test::test_main_static(...) - let test_main_path_expr = ecx.expr_path(test_main_path); - let tests_ident_expr = ecx.expr_ident(sp, Ident::from_str("TESTS")); + let mut test_runner = cx.test_runner.clone().unwrap_or( + ecx.path(sp, vec![ + test_id, ecx.ident_of("test_main_static") + ])); + + test_runner.span = sp; + + let test_main_path_expr = ecx.expr_path(test_runner.clone()); let call_test_main = ecx.expr_call(sp, test_main_path_expr, - vec![tests_ident_expr]); + vec![mk_tests_slice(cx)]); let call_test_main = ecx.stmt_expr(call_test_main); + // #![main] let main_meta = ecx.meta_word(sp, Symbol::intern("main")); let main_attr = ecx.attribute(sp, main_meta); + + // extern crate test as test_gensym + let test_extern_stmt = ecx.stmt_item(sp, ecx.item(sp, + test_id, + vec![], + ast::ItemKind::ExternCrate(Some(Symbol::intern("test"))) + )); + // pub fn main() { ... } let main_ret_ty = ecx.ty(sp, ast::TyKind::Tup(vec![])); - let main_body = ecx.block(sp, vec![call_test_main]); + + // If no test runner is provided we need to import the test crate + let main_body = if cx.test_runner.is_none() { + ecx.block(sp, vec![test_extern_stmt, call_test_main]) + } else { + ecx.block(sp, vec![call_test_main]) + }; + let main = ast::ItemKind::Fn(ecx.fn_decl(vec![], ast::FunctionRetTy::Ty(main_ret_ty)), ast::FnHeader::default(), ast::Generics::default(), main_body); + + // Honor the reexport_test_harness_main attribute + let main_id = Ident::new( + cx.reexport_test_harness_main.unwrap_or(Symbol::gensym("main")), + sp); + P(ast::Item { - ident: Ident::from_str("main"), + ident: main_id, attrs: vec![main_attr], id: ast::DUMMY_NODE_ID, node: main, @@ -561,71 +391,7 @@ fn mk_main(cx: &mut TestCtxt) -> P<ast::Item> { span: sp, tokens: None, }) -} - -fn mk_test_module(cx: &mut TestCtxt) -> (P<ast::Item>, Option<P<ast::Item>>) { - // Link to test crate - let import = mk_std(cx); - - // A constant vector of test descriptors. - let tests = mk_tests(cx); - - // The synthesized main function which will call the console test runner - // with our list of tests - let mainfn = mk_main(cx); - let testmod = ast::Mod { - inner: DUMMY_SP, - items: vec![import, mainfn, tests], - }; - let item_ = ast::ItemKind::Mod(testmod); - let mod_ident = Ident::with_empty_ctxt(Symbol::gensym("__test")); - - let mut expander = cx.ext_cx.monotonic_expander(); - let item = expander.fold_item(P(ast::Item { - id: ast::DUMMY_NODE_ID, - ident: mod_ident, - attrs: vec![], - node: item_, - vis: dummy_spanned(ast::VisibilityKind::Public), - span: DUMMY_SP, - tokens: None, - })).pop().unwrap(); - let reexport = cx.reexport_test_harness_main.map(|s| { - // building `use __test::main as <ident>;` - let rename = Ident::with_empty_ctxt(s); - - let use_path = ast::UseTree { - span: DUMMY_SP, - prefix: path_node(vec![mod_ident, Ident::from_str("main")]), - kind: ast::UseTreeKind::Simple(Some(rename), ast::DUMMY_NODE_ID, ast::DUMMY_NODE_ID), - }; - - expander.fold_item(P(ast::Item { - id: ast::DUMMY_NODE_ID, - ident: keywords::Invalid.ident(), - attrs: vec![], - node: ast::ItemKind::Use(P(use_path)), - vis: dummy_spanned(ast::VisibilityKind::Inherited), - span: DUMMY_SP, - tokens: None, - })).pop().unwrap() - }); - - debug!("Synthetic test module:\n{}\n", pprust::item_to_string(&item)); - - (item, reexport) -} - -fn nospan<T>(t: T) -> source_map::Spanned<T> { - source_map::Spanned { node: t, span: DUMMY_SP } -} - -fn path_node(ids: Vec<Ident>) -> ast::Path { - ast::Path { - span: DUMMY_SP, - segments: ids.into_iter().map(|id| ast::PathSegment::from_ident(id)).collect(), - } } fn path_name_i(idents: &[Ident]) -> String { @@ -640,184 +406,46 @@ fn path_name_i(idents: &[Ident]) -> String { path_name } -fn mk_tests(cx: &TestCtxt) -> P<ast::Item> { - // The vector of test_descs for this crate - let test_descs = mk_test_descs(cx); - - // FIXME #15962: should be using quote_item, but that stringifies - // __test_reexports, causing it to be reinterned, losing the - // gensym information. - let sp = ignored_span(cx, DUMMY_SP); - let ecx = &cx.ext_cx; - let struct_type = ecx.ty_path(ecx.path(sp, vec![ecx.ident_of("self"), - ecx.ident_of("test"), - ecx.ident_of("TestDescAndFn")])); - let static_lt = ecx.lifetime(sp, keywords::StaticLifetime.ident()); - // &'static [self::test::TestDescAndFn] - let static_type = ecx.ty_rptr(sp, - ecx.ty(sp, ast::TyKind::Slice(struct_type)), - Some(static_lt), - ast::Mutability::Immutable); - // static TESTS: $static_type = &[...]; - ecx.item_const(sp, - ecx.ident_of("TESTS"), - static_type, - test_descs) +/// Creates a slice containing every test like so: +/// &[path::to::test1, path::to::test2] +fn mk_tests_slice(cx: &TestCtxt) -> P<ast::Expr> { + debug!("building test vector from {} tests", cx.test_cases.len()); + let ref ecx = cx.ext_cx; + + ecx.expr_vec_slice(DUMMY_SP, + cx.test_cases.iter().map(|test| { + ecx.expr_addr_of(test.span, + ecx.expr_path(ecx.path(test.span, visible_path(cx, &test.path)))) + }).collect()) } -fn mk_test_descs(cx: &TestCtxt) -> P<ast::Expr> { - debug!("building test vector from {} tests", cx.testfns.len()); - - P(ast::Expr { - id: ast::DUMMY_NODE_ID, - node: ast::ExprKind::AddrOf(ast::Mutability::Immutable, - P(ast::Expr { - id: ast::DUMMY_NODE_ID, - node: ast::ExprKind::Array(cx.testfns.iter().map(|test| { - mk_test_desc_and_fn_rec(cx, test) - }).collect()), - span: DUMMY_SP, - attrs: ThinVec::new(), - })), - span: DUMMY_SP, - attrs: ThinVec::new(), - }) -} - -fn mk_test_desc_and_fn_rec(cx: &TestCtxt, test: &Test) -> P<ast::Expr> { - // FIXME #15962: should be using quote_expr, but that stringifies - // __test_reexports, causing it to be reinterned, losing the - // gensym information. - - let span = ignored_span(cx, test.span); - let ecx = &cx.ext_cx; - let self_id = ecx.ident_of("self"); - let test_id = ecx.ident_of("test"); - - // creates self::test::$name - let test_path = |name| { - ecx.path(span, vec![self_id, test_id, ecx.ident_of(name)]) - }; - // creates $name: $expr - let field = |name, expr| ecx.field_imm(span, ecx.ident_of(name), expr); - - // path to the #[test] function: "foo::bar::baz" - let path_string = path_name_i(&test.path[..]); - - debug!("encoding {}", path_string); - - let name_expr = ecx.expr_str(span, Symbol::intern(&path_string)); - - // self::test::StaticTestName($name_expr) - let name_expr = ecx.expr_call(span, - ecx.expr_path(test_path("StaticTestName")), - vec![name_expr]); - - let ignore_expr = ecx.expr_bool(span, test.ignore); - let should_panic_path = |name| { - ecx.path(span, vec![self_id, test_id, ecx.ident_of("ShouldPanic"), ecx.ident_of(name)]) - }; - let fail_expr = match test.should_panic { - ShouldPanic::No => ecx.expr_path(should_panic_path("No")), - ShouldPanic::Yes(msg) => { - match msg { - Some(msg) => { - let msg = ecx.expr_str(span, msg); - let path = should_panic_path("YesWithMessage"); - ecx.expr_call(span, ecx.expr_path(path), vec![msg]) - } - None => ecx.expr_path(should_panic_path("Yes")), - } - } - }; - let allow_fail_expr = ecx.expr_bool(span, test.allow_fail); - - // self::test::TestDesc { ... } - let desc_expr = ecx.expr_struct( - span, - test_path("TestDesc"), - vec![field("name", name_expr), - field("ignore", ignore_expr), - field("should_panic", fail_expr), - field("allow_fail", allow_fail_expr)]); - +/// Creates a path from the top-level __test module to the test via __test_reexports +fn visible_path(cx: &TestCtxt, path: &[Ident]) -> Vec<Ident>{ let mut visible_path = vec![]; - if cx.features.extern_absolute_paths { - visible_path.push(keywords::Crate.ident()); - } match cx.toplevel_reexport { Some(id) => visible_path.push(id), None => { - let diag = cx.span_diagnostic; - diag.bug("expected to find top-level re-export name, but found None"); - } - }; - visible_path.extend_from_slice(&test.path[..]); - - // Rather than directly give the test function to the test - // harness, we create a wrapper like one of the following: - // - // || test::assert_test_result(real_function()) // for test - // |b| test::assert_test_result(real_function(b)) // for bench - // - // this will coerce into a fn pointer that is specialized to the - // actual return type of `real_function` (Typically `()`, but not always). - let fn_expr = { - // construct `real_function()` (this will be inserted into the overall expr) - let real_function_expr = ecx.expr_path(ecx.path_global(span, visible_path)); - // construct path `test::assert_test_result` - let assert_test_result = test_path("assert_test_result"); - if test.bench { - // construct `|b| {..}` - let b_ident = Ident::with_empty_ctxt(Symbol::gensym("b")); - let b_expr = ecx.expr_ident(span, b_ident); - ecx.lambda( - span, - vec![b_ident], - // construct `assert_test_result(..)` - ecx.expr_call( - span, - ecx.expr_path(assert_test_result), - vec![ - // construct `real_function(b)` - ecx.expr_call( - span, - real_function_expr, - vec![b_expr], - ) - ], - ), - ) - } else { - // construct `|| {..}` - ecx.lambda( - span, - vec![], - // construct `assert_test_result(..)` - ecx.expr_call( - span, - ecx.expr_path(assert_test_result), - vec![ - // construct `real_function()` - ecx.expr_call( - span, - real_function_expr, - vec![], - ) - ], - ), - ) + cx.span_diagnostic.bug("expected to find top-level re-export name, but found None"); } - }; - - let variant_name = if test.bench { "StaticBenchFn" } else { "StaticTestFn" }; + } + visible_path.extend_from_slice(path); + visible_path +} - // self::test::$variant_name($fn_expr) - let testfn_expr = ecx.expr_call(span, ecx.expr_path(test_path(variant_name)), vec![fn_expr]); +fn is_test_case(i: &ast::Item) -> bool { + attr::contains_name(&i.attrs, "test_case") +} - // self::test::TestDescAndFn { ... } - ecx.expr_struct(span, - test_path("TestDescAndFn"), - vec![field("desc", desc_expr), - field("testfn", testfn_expr)]) +fn get_test_runner(sd: &errors::Handler, krate: &ast::Crate) -> Option<ast::Path> { + let test_attr = attr::find_by_name(&krate.attrs, "test_runner")?; + if let Some(meta_list) = test_attr.meta_item_list() { + if meta_list.len() != 1 { + sd.span_fatal(test_attr.span(), + "#![test_runner(..)] accepts exactly 1 argument").raise() + } + Some(meta_list[0].word().as_ref().unwrap().ident.clone()) + } else { + sd.span_fatal(test_attr.span(), + "test_runner must be of the form #[test_runner(..)]").raise() + } } |
