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_ext | |
| parent | 0be2c303692cab31390e52701007cfa87867bf74 (diff) | |
| download | rust-9b27de41d4e00cb6c23df270572472fd4c6f47f8.tar.gz rust-9b27de41d4e00cb6c23df270572472fd4c6f47f8.zip | |
Introduce Custom Test Frameworks
Diffstat (limited to 'src/libsyntax_ext')
| -rw-r--r-- | src/libsyntax_ext/Cargo.toml | 1 | ||||
| -rw-r--r-- | src/libsyntax_ext/lib.rs | 10 | ||||
| -rw-r--r-- | src/libsyntax_ext/test.rs | 328 |
3 files changed, 337 insertions, 2 deletions
diff --git a/src/libsyntax_ext/Cargo.toml b/src/libsyntax_ext/Cargo.toml index 8dba34583be..5a691bde3ec 100644 --- a/src/libsyntax_ext/Cargo.toml +++ b/src/libsyntax_ext/Cargo.toml @@ -17,3 +17,4 @@ syntax_pos = { path = "../libsyntax_pos" } rustc_data_structures = { path = "../librustc_data_structures" } rustc_target = { path = "../librustc_target" } smallvec = { version = "0.6.5", features = ["union"] } +log = "0.4" diff --git a/src/libsyntax_ext/lib.rs b/src/libsyntax_ext/lib.rs index 84436b4e4ea..bbbf338c4f3 100644 --- a/src/libsyntax_ext/lib.rs +++ b/src/libsyntax_ext/lib.rs @@ -19,7 +19,7 @@ #![cfg_attr(not(stage0), feature(nll))] #![cfg_attr(not(stage0), feature(infer_outlives_requirements))] #![feature(str_escape)] - +#![feature(quote)] #![feature(rustc_diagnostic_macros)] extern crate fmt_macros; @@ -32,6 +32,8 @@ extern crate rustc_errors as errors; extern crate rustc_target; #[macro_use] extern crate smallvec; +#[macro_use] +extern crate log; mod diagnostics; @@ -51,6 +53,7 @@ mod format_foreign; mod global_asm; mod log_syntax; mod trace_macros; +mod test; pub mod proc_macro_registrar; @@ -59,7 +62,7 @@ pub mod proc_macro_impl; use rustc_data_structures::sync::Lrc; use syntax::ast; -use syntax::ext::base::{MacroExpanderFn, NormalTT, NamedSyntaxExtension}; +use syntax::ext::base::{MacroExpanderFn, NormalTT, NamedSyntaxExtension, MultiModifier}; use syntax::ext::hygiene; use syntax::symbol::Symbol; @@ -130,6 +133,9 @@ pub fn register_builtins(resolver: &mut dyn syntax::ext::base::Resolver, assert: assert::expand_assert, } + register(Symbol::intern("test"), MultiModifier(Box::new(test::expand_test))); + register(Symbol::intern("bench"), MultiModifier(Box::new(test::expand_bench))); + // format_args uses `unstable` things internally. register(Symbol::intern("format_args"), NormalTT { diff --git a/src/libsyntax_ext/test.rs b/src/libsyntax_ext/test.rs new file mode 100644 index 00000000000..d9d0f3d0a32 --- /dev/null +++ b/src/libsyntax_ext/test.rs @@ -0,0 +1,328 @@ +// Copyright 2013 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. + +/// The expansion from a test function to the appropriate test struct for libtest +/// Ideally, this code would be in libtest but for efficiency and error messages it lives here. + +use syntax::ext::base::*; +use syntax::ext::build::AstBuilder; +use syntax::ext::hygiene::{self, Mark, SyntaxContext}; +use syntax::attr; +use syntax::ast; +use syntax::print::pprust; +use syntax::symbol::Symbol; +use syntax_pos::{DUMMY_SP, Span}; +use syntax::source_map::{ExpnInfo, MacroAttribute}; +use std::iter; + +pub fn expand_test( + cx: &mut ExtCtxt, + attr_sp: Span, + _meta_item: &ast::MetaItem, + item: Annotatable, +) -> Vec<Annotatable> { + expand_test_or_bench(cx, attr_sp, item, false) +} + +pub fn expand_bench( + cx: &mut ExtCtxt, + attr_sp: Span, + _meta_item: &ast::MetaItem, + item: Annotatable, +) -> Vec<Annotatable> { + expand_test_or_bench(cx, attr_sp, item, true) +} + +pub fn expand_test_or_bench( + cx: &mut ExtCtxt, + attr_sp: Span, + item: Annotatable, + is_bench: bool +) -> Vec<Annotatable> { + // If we're not in test configuration, remove the annotated item + if !cx.ecfg.should_test { return vec![]; } + + let item = + if let Annotatable::Item(i) = item { i } + else { + cx.parse_sess.span_diagnostic.span_fatal(item.span(), + "#[test] attribute is only allowed on fn items").raise(); + }; + + if let ast::ItemKind::Mac(_) = item.node { + cx.parse_sess.span_diagnostic.span_warn(item.span, + "#[test] attribute should not be used on macros. Use #[cfg(test)] instead."); + return vec![Annotatable::Item(item)]; + } + + // has_*_signature will report any errors in the type so compilation + // will fail. We shouldn't try to expand in this case because the errors + // would be spurious. + if (!is_bench && !has_test_signature(cx, &item)) || + (is_bench && !has_bench_signature(cx, &item)) { + return vec![Annotatable::Item(item)]; + } + + let (sp, attr_sp) = { + let mark = Mark::fresh(Mark::root()); + mark.set_expn_info(ExpnInfo { + call_site: DUMMY_SP, + def_site: None, + format: MacroAttribute(Symbol::intern("test")), + allow_internal_unstable: true, + allow_internal_unsafe: false, + local_inner_macros: false, + edition: hygiene::default_edition(), + }); + (item.span.with_ctxt(SyntaxContext::empty().apply_mark(mark)), + attr_sp.with_ctxt(SyntaxContext::empty().apply_mark(mark))) + }; + + // Gensym "test" so we can extern crate without conflicting with any local names + let test_id = cx.ident_of("test").gensym(); + + // creates test::$name + let test_path = |name| { + cx.path(sp, vec![test_id, cx.ident_of(name)]) + }; + + // creates test::$name + let should_panic_path = |name| { + cx.path(sp, vec![test_id, cx.ident_of("ShouldPanic"), cx.ident_of(name)]) + }; + + // creates $name: $expr + let field = |name, expr| cx.field_imm(sp, cx.ident_of(name), expr); + + let test_fn = if is_bench { + // A simple ident for a lambda + let b = cx.ident_of("b"); + + cx.expr_call(sp, cx.expr_path(test_path("StaticBenchFn")), vec![ + // |b| self::test::assert_test_result( + cx.lambda1(sp, + cx.expr_call(sp, cx.expr_path(test_path("assert_test_result")), vec![ + // super::$test_fn(b) + cx.expr_call(sp, + cx.expr_path(cx.path(sp, vec![item.ident])), + vec![cx.expr_ident(sp, b)]) + ]), + b + ) + // ) + ]) + } else { + cx.expr_call(sp, cx.expr_path(test_path("StaticTestFn")), vec![ + // || { + cx.lambda0(sp, + // test::assert_test_result( + cx.expr_call(sp, cx.expr_path(test_path("assert_test_result")), vec![ + // $test_fn() + cx.expr_call(sp, cx.expr_path(cx.path(sp, vec![item.ident])), vec![]) + // ) + ]) + // } + ) + // ) + ]) + }; + + let mut test_const = cx.item(sp, item.ident.gensym(), + // #[test_case] + vec![cx.attribute(attr_sp, cx.meta_word(attr_sp, Symbol::intern("test_case")))], + // const $ident: test::TestDescAndFn = + ast::ItemKind::Const(cx.ty(sp, ast::TyKind::Path(None, test_path("TestDescAndFn"))), + // test::TestDescAndFn { + cx.expr_struct(sp, test_path("TestDescAndFn"), vec![ + // desc: test::TestDesc { + field("desc", cx.expr_struct(sp, test_path("TestDesc"), vec![ + // name: "path::to::test" + field("name", cx.expr_call(sp, cx.expr_path(test_path("StaticTestName")), + vec![ + cx.expr_str(sp, Symbol::intern(&item_path( + // skip the name of the root module + &cx.current_expansion.module.mod_path[1..], + &item.ident + ))) + ])), + // ignore: true | false + field("ignore", cx.expr_bool(sp, should_ignore(&item))), + // allow_fail: true | false + field("allow_fail", cx.expr_bool(sp, should_fail(&item))), + // should_panic: ... + field("should_panic", match should_panic(cx, &item) { + // test::ShouldPanic::No + ShouldPanic::No => cx.expr_path(should_panic_path("No")), + // test::ShouldPanic::Yes + ShouldPanic::Yes(None) => cx.expr_path(should_panic_path("Yes")), + // test::ShouldPanic::YesWithMessage("...") + ShouldPanic::Yes(Some(sym)) => cx.expr_call(sp, + cx.expr_path(should_panic_path("YesWithMessage")), + vec![cx.expr_str(sp, sym)]), + }), + // }, + ])), + // testfn: test::StaticTestFn(...) | test::StaticBenchFn(...) + field("testfn", test_fn) + // } + ]) + // } + )); + test_const = test_const.map(|mut tc| { tc.vis.node = ast::VisibilityKind::Public; tc}); + + // extern crate test as test_gensym + let test_extern = cx.item(sp, + test_id, + vec![], + ast::ItemKind::ExternCrate(Some(Symbol::intern("test"))) + ); + + debug!("Synthetic test item:\n{}\n", pprust::item_to_string(&test_const)); + + vec![ + // Access to libtest under a gensymed name + Annotatable::Item(test_extern), + // The generated test case + Annotatable::Item(test_const), + // The original item + Annotatable::Item(item) + ] +} + +fn item_path(mod_path: &[ast::Ident], item_ident: &ast::Ident) -> String { + mod_path.iter().chain(iter::once(item_ident)) + .map(|x| x.to_string()).collect::<Vec<String>>().join("::") +} + +enum ShouldPanic { + No, + Yes(Option<Symbol>), +} + +fn should_ignore(i: &ast::Item) -> bool { + attr::contains_name(&i.attrs, "ignore") +} + +fn should_fail(i: &ast::Item) -> bool { + attr::contains_name(&i.attrs, "allow_fail") +} + +fn should_panic(cx: &ExtCtxt, i: &ast::Item) -> ShouldPanic { + match attr::find_by_name(&i.attrs, "should_panic") { + Some(attr) => { + let ref sd = cx.parse_sess.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, + } +} + +fn has_test_signature(cx: &ExtCtxt, i: &ast::Item) -> bool { + let has_should_panic_attr = attr::contains_name(&i.attrs, "should_panic"); + let ref sd = cx.parse_sess.span_diagnostic; + if let ast::ItemKind::Fn(ref decl, ref header, ref generics, _) = i.node { + if header.unsafety == ast::Unsafety::Unsafe { + sd.span_err( + i.span, + "unsafe functions cannot be used for tests" + ); + return false + } + if header.asyncness.is_async() { + sd.span_err( + i.span, + "async functions cannot be used for tests" + ); + return false + } + + + // 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() { + sd.span_err(i.span, "functions used as tests can not have any arguments"); + return false; + } + + match (has_output, has_should_panic_attr) { + (true, true) => { + sd.span_err(i.span, "functions using `#[should_panic]` must return `()`"); + false + }, + (true, false) => if !generics.params.is_empty() { + sd.span_err(i.span, + "functions used as tests must have signature fn() -> ()"); + false + } else { + true + }, + (false, _) => true + } + } else { + sd.span_err(i.span, "only functions may be used as tests"); + false + } +} + +fn has_bench_signature(cx: &ExtCtxt, i: &ast::Item) -> bool { + let has_sig = if let ast::ItemKind::Fn(ref decl, _, _, _) = i.node { + // NB: inadequate check, but we're running + // well before resolve, can't get too deep. + decl.inputs.len() == 1 + } else { + false + }; + + if !has_sig { + cx.parse_sess.span_diagnostic.span_err(i.span, "functions used as benches must have \ + signature `fn(&mut Bencher) -> impl Termination`"); + } + + has_sig +} |
