1 // Code that generates a test runner to run all the tests in a crate
4 use smallvec
::{smallvec, SmallVec}
;
5 use rustc_feature
::Features
;
6 use rustc_target
::spec
::PanicStrategy
;
7 use syntax
::ast
::{self, Ident}
;
9 use syntax
::entry
::{self, EntryPointType}
;
10 use syntax_expand
::base
::{ExtCtxt, Resolver}
;
11 use syntax_expand
::expand
::{AstFragment, ExpansionConfig}
;
12 use syntax
::mut_visit
::{*, ExpectOne}
;
14 use syntax
::sess
::ParseSess
;
15 use syntax
::source_map
::respan
;
16 use syntax
::symbol
::{sym, Symbol}
;
17 use syntax_pos
::{Span, DUMMY_SP}
;
18 use syntax_pos
::hygiene
::{AstPass, SyntaxContext, Transparency}
;
29 panic_strategy
: PanicStrategy
,
31 test_cases
: Vec
<Test
>,
32 reexport_test_harness_main
: Option
<Symbol
>,
33 test_runner
: Option
<ast
::Path
>,
36 // Traverse the crate, collecting all the test functions, eliding any
37 // existing main functions, and synthesizing a main test harness
40 resolver
: &mut dyn Resolver
,
42 krate
: &mut ast
::Crate
,
43 span_diagnostic
: &errors
::Handler
,
45 panic_strategy
: PanicStrategy
,
46 platform_panic_strategy
: PanicStrategy
,
47 enable_panic_abort_tests
: bool
,
49 // Check for #![reexport_test_harness_main = "some_name"] which gives the
50 // main test function the name `some_name` without hygiene. This needs to be
51 // unconditional, so that the attribute is still marked as used in
53 let reexport_test_harness_main
=
54 attr
::first_attr_value_str_by_name(&krate
.attrs
, sym
::reexport_test_harness_main
);
56 // Do this here so that the test_runner crate attribute gets marked as used
57 // even in non-test builds
58 let test_runner
= get_test_runner(span_diagnostic
, &krate
);
61 let panic_strategy
= match (panic_strategy
, enable_panic_abort_tests
) {
62 (PanicStrategy
::Abort
, true) =>
64 (PanicStrategy
::Abort
, false) if panic_strategy
== platform_panic_strategy
=> {
65 // Silently allow compiling with panic=abort on these platforms,
66 // but with old behavior (abort if a test fails).
69 (PanicStrategy
::Abort
, false) => {
70 span_diagnostic
.err("building tests with panic=abort is not supported \
71 without `-Zpanic_abort_tests`");
74 (PanicStrategy
::Unwind
, _
) => PanicStrategy
::Unwind
,
76 generate_test_harness(sess
, resolver
, reexport_test_harness_main
,
77 krate
, features
, panic_strategy
, test_runner
)
81 struct TestHarnessGenerator
<'a
> {
86 impl<'a
> MutVisitor
for TestHarnessGenerator
<'a
> {
87 fn visit_crate(&mut self, c
: &mut ast
::Crate
) {
88 noop_visit_crate(c
, self);
90 // Create a main function to run our tests
91 c
.module
.items
.push(mk_main(&mut self.cx
));
94 fn flat_map_item(&mut self, i
: P
<ast
::Item
>) -> SmallVec
<[P
<ast
::Item
>; 1]> {
95 let mut item
= i
.into_inner();
96 if is_test_case(&item
) {
97 debug
!("this is a test item");
103 self.tests
.push(test
);
106 // We don't want to recurse into anything other than mods, since
107 // mods or tests inside of functions will break things
108 if let ast
::ItemKind
::Mod(mut module
) = item
.kind
{
109 let tests
= mem
::take(&mut self.tests
);
110 noop_visit_mod(&mut module
, self);
111 let mut tests
= mem
::replace(&mut self.tests
, tests
);
113 if !tests
.is_empty() {
114 let parent
= if item
.id
== ast
::DUMMY_NODE_ID
{
119 // Create an identifier that will hygienically resolve the test
120 // case name, even in another module.
121 let expn_id
= self.cx
.ext_cx
.resolver
.expansion_for_ast_pass(
123 AstPass
::TestHarness
,
127 for test
in &mut tests
{
128 // See the comment on `mk_main` for why we're using
129 // `apply_mark` directly.
130 test
.ident
.span
= test
.ident
.span
.apply_mark(expn_id
, Transparency
::Opaque
);
132 self.cx
.test_cases
.extend(tests
);
134 item
.kind
= ast
::ItemKind
::Mod(module
);
139 fn visit_mac(&mut self, _mac
: &mut ast
::Mac
) {
144 /// A folder used to remove any entry points (like fn main) because the harness
145 /// generator will provide its own
146 struct EntryPointCleaner
{
147 // Current depth in the ast
152 impl MutVisitor
for EntryPointCleaner
{
153 fn flat_map_item(&mut self, i
: P
<ast
::Item
>) -> SmallVec
<[P
<ast
::Item
>; 1]> {
155 let item
= noop_flat_map_item(i
, self).expect_one("noop did something");
158 // Remove any #[main] or #[start] from the AST so it doesn't
159 // clash with the one we're going to add, but mark it as
160 // #[allow(dead_code)] to avoid printing warnings.
161 let item
= match entry
::entry_point_type(&item
, self.depth
) {
162 EntryPointType
::MainNamed
|
163 EntryPointType
::MainAttr
|
164 EntryPointType
::Start
=>
165 item
.map(|ast
::Item {id, ident, attrs, kind, vis, span, tokens}
| {
166 let allow_ident
= Ident
::new(sym
::allow
, self.def_site
);
167 let dc_nested
= attr
::mk_nested_word_item(
168 Ident
::from_str_and_span("dead_code", self.def_site
),
170 let allow_dead_code_item
= attr
::mk_list_item(allow_ident
, vec
![dc_nested
]);
171 let allow_dead_code
= attr
::mk_attr_outer(allow_dead_code_item
);
176 attrs
: attrs
.into_iter()
178 !attr
.check_name(sym
::main
) && !attr
.check_name(sym
::start
)
180 .chain(iter
::once(allow_dead_code
))
188 EntryPointType
::None
|
189 EntryPointType
::OtherMain
=> item
,
195 fn visit_mac(&mut self, _mac
: &mut ast
::Mac
) {
200 /// Crawl over the crate, inserting test reexports and the test main function
201 fn generate_test_harness(sess
: &ParseSess
,
202 resolver
: &mut dyn Resolver
,
203 reexport_test_harness_main
: Option
<Symbol
>,
204 krate
: &mut ast
::Crate
,
206 panic_strategy
: PanicStrategy
,
207 test_runner
: Option
<ast
::Path
>) {
208 let mut econfig
= ExpansionConfig
::default("test".to_string());
209 econfig
.features
= Some(features
);
211 let ext_cx
= ExtCtxt
::new(sess
, econfig
, resolver
);
213 let expn_id
= ext_cx
.resolver
.expansion_for_ast_pass(
215 AstPass
::TestHarness
,
216 &[sym
::main
, sym
::test
, sym
::rustc_attrs
],
219 let def_site
= DUMMY_SP
.with_def_site_ctxt(expn_id
);
221 // Remove the entry points
222 let mut cleaner
= EntryPointCleaner { depth: 0, def_site }
;
223 cleaner
.visit_crate(krate
);
229 test_cases
: Vec
::new(),
230 reexport_test_harness_main
,
234 TestHarnessGenerator
{
237 }.visit_crate(krate
);
240 /// Creates a function item for use as the main function of a test build.
241 /// This function will call the `test_runner` as specified by the crate attribute
243 /// By default this expands to
247 /// extern crate test;
248 /// test::test_main_static(&[
255 /// Most of the Ident have the usual def-site hygiene for the AST pass. The
256 /// exception is the `test_const`s. These have a syntax context that has two
257 /// opaque marks: one from the expansion of `test` or `test_case`, and one
258 /// generated in `TestHarnessGenerator::flat_map_item`. When resolving this
259 /// identifier after failing to find a matching identifier in the root module
260 /// we remove the outer mark, and try resolving at its def-site, which will
261 /// then resolve to `test_const`.
263 /// The expansion here can be controlled by two attributes:
265 /// `reexport_test_harness_main` provides a different name for the `main`
266 /// function and `test_runner` provides a path that replaces
267 /// `test::test_main_static`.
268 fn mk_main(cx
: &mut TestCtxt
<'_
>) -> P
<ast
::Item
> {
269 let sp
= cx
.def_site
;
270 let ecx
= &cx
.ext_cx
;
271 let test_id
= Ident
::new(sym
::test
, sp
);
273 let runner_name
= match cx
.panic_strategy
{
274 PanicStrategy
::Unwind
=> "test_main_static",
275 PanicStrategy
::Abort
=> "test_main_static_abort",
278 // test::test_main_static(...)
279 let mut test_runner
= cx
.test_runner
.clone().unwrap_or(
280 ecx
.path(sp
, vec
![test_id
, ecx
.ident_of(runner_name
, sp
)]));
282 test_runner
.span
= sp
;
284 let test_main_path_expr
= ecx
.expr_path(test_runner
);
285 let call_test_main
= ecx
.expr_call(sp
, test_main_path_expr
,
286 vec
![mk_tests_slice(cx
, sp
)]);
287 let call_test_main
= ecx
.stmt_expr(call_test_main
);
290 let test_extern_stmt
= ecx
.stmt_item(sp
, ecx
.item(sp
,
293 ast
::ItemKind
::ExternCrate(None
)
297 let main_meta
= ecx
.meta_word(sp
, sym
::main
);
298 let main_attr
= ecx
.attribute(main_meta
);
300 // pub fn main() { ... }
301 let main_ret_ty
= ecx
.ty(sp
, ast
::TyKind
::Tup(vec
![]));
303 // If no test runner is provided we need to import the test crate
304 let main_body
= if cx
.test_runner
.is_none() {
305 ecx
.block(sp
, vec
![test_extern_stmt
, call_test_main
])
307 ecx
.block(sp
, vec
![call_test_main
])
310 let decl
= ecx
.fn_decl(vec
![], ast
::FunctionRetTy
::Ty(main_ret_ty
));
311 let sig
= ast
::FnSig { decl, header: ast::FnHeader::default() }
;
312 let main
= ast
::ItemKind
::Fn(sig
, ast
::Generics
::default(), main_body
);
314 // Honor the reexport_test_harness_main attribute
315 let main_id
= match cx
.reexport_test_harness_main
{
316 Some(sym
) => Ident
::new(sym
, sp
.with_ctxt(SyntaxContext
::root())),
317 None
=> Ident
::new(sym
::main
, sp
),
320 let main
= P(ast
::Item
{
322 attrs
: vec
![main_attr
],
323 id
: ast
::DUMMY_NODE_ID
,
325 vis
: respan(sp
, ast
::VisibilityKind
::Public
),
330 // Integrate the new item into existing module structures.
331 let main
= AstFragment
::Items(smallvec
![main
]);
332 cx
.ext_cx
.monotonic_expander().fully_expand_fragment(main
).make_items().pop().unwrap()
335 /// Creates a slice containing every test like so:
336 /// &[&test1, &test2]
337 fn mk_tests_slice(cx
: &TestCtxt
<'_
>, sp
: Span
) -> P
<ast
::Expr
> {
338 debug
!("building test vector from {} tests", cx
.test_cases
.len());
339 let ref ecx
= cx
.ext_cx
;
342 ecx
.expr_vec_slice(sp
,
343 cx
.test_cases
.iter().map(|test
| {
344 ecx
.expr_addr_of(test
.span
,
345 ecx
.expr_path(ecx
.path(test
.span
, vec
![test
.ident
])))
349 fn is_test_case(i
: &ast
::Item
) -> bool
{
350 attr
::contains_name(&i
.attrs
, sym
::rustc_test_marker
)
353 fn get_test_runner(sd
: &errors
::Handler
, krate
: &ast
::Crate
) -> Option
<ast
::Path
> {
354 let test_attr
= attr
::find_by_name(&krate
.attrs
, sym
::test_runner
)?
;
355 test_attr
.meta_item_list().map(|meta_list
| {
356 if meta_list
.len() != 1 {
357 sd
.span_fatal(test_attr
.span
,
358 "`#![test_runner(..)]` accepts exactly 1 argument").raise()
360 match meta_list
[0].meta_item() {
361 Some(meta_item
) if meta_item
.is_word() => meta_item
.path
.clone(),
362 _
=> sd
.span_fatal(test_attr
.span
, "`test_runner` argument must be a path").raise()