1 // Code that generates a test runner to run all the tests in a crate
4 use rustc_ast
::entry
::EntryPointType
;
5 use rustc_ast
::mut_visit
::{ExpectOne, *}
;
7 use rustc_ast
::{attr, ModKind}
;
8 use rustc_expand
::base
::{ExtCtxt, ResolverExpand}
;
9 use rustc_expand
::expand
::{AstFragment, ExpansionConfig}
;
10 use rustc_feature
::Features
;
11 use rustc_session
::Session
;
12 use rustc_span
::hygiene
::{AstPass, SyntaxContext, Transparency}
;
13 use rustc_span
::symbol
::{sym, Ident, Symbol}
;
14 use rustc_span
::{Span, DUMMY_SP}
;
15 use rustc_target
::spec
::PanicStrategy
;
16 use smallvec
::{smallvec, SmallVec}
;
28 panic_strategy
: PanicStrategy
,
30 test_cases
: Vec
<Test
>,
31 reexport_test_harness_main
: Option
<Symbol
>,
32 test_runner
: Option
<ast
::Path
>,
35 // Traverse the crate, collecting all the test functions, eliding any
36 // existing main functions, and synthesizing a main test harness
37 pub fn inject(sess
: &Session
, resolver
: &mut dyn ResolverExpand
, krate
: &mut ast
::Crate
) {
38 let span_diagnostic
= sess
.diagnostic();
39 let panic_strategy
= sess
.panic_strategy();
40 let platform_panic_strategy
= sess
.target
.panic_strategy
;
42 // Check for #![reexport_test_harness_main = "some_name"] which gives the
43 // main test function the name `some_name` without hygiene. This needs to be
44 // unconditional, so that the attribute is still marked as used in
46 let reexport_test_harness_main
=
47 sess
.first_attr_value_str_by_name(&krate
.attrs
, sym
::reexport_test_harness_main
);
49 // Do this here so that the test_runner crate attribute gets marked as used
50 // even in non-test builds
51 let test_runner
= get_test_runner(sess
, span_diagnostic
, &krate
);
54 let panic_strategy
= match (panic_strategy
, sess
.opts
.debugging_opts
.panic_abort_tests
) {
55 (PanicStrategy
::Abort
, true) => PanicStrategy
::Abort
,
56 (PanicStrategy
::Abort
, false) => {
57 if panic_strategy
== platform_panic_strategy
{
58 // Silently allow compiling with panic=abort on these platforms,
59 // but with old behavior (abort if a test fails).
62 "building tests with panic=abort is not supported \
63 without `-Zpanic_abort_tests`",
68 (PanicStrategy
::Unwind
, _
) => PanicStrategy
::Unwind
,
70 generate_test_harness(
73 reexport_test_harness_main
,
75 &sess
.features_untracked(),
82 struct TestHarnessGenerator
<'a
> {
87 impl<'a
> MutVisitor
for TestHarnessGenerator
<'a
> {
88 fn visit_crate(&mut self, c
: &mut ast
::Crate
) {
89 noop_visit_crate(c
, self);
91 // Create a main function to run our tests
92 c
.items
.push(mk_main(&mut self.cx
));
95 fn flat_map_item(&mut self, i
: P
<ast
::Item
>) -> SmallVec
<[P
<ast
::Item
>; 1]> {
96 let mut item
= i
.into_inner();
97 if is_test_case(&self.cx
.ext_cx
.sess
, &item
) {
98 debug
!("this is a test item");
100 let test
= Test { span: item.span, ident: item.ident }
;
101 self.tests
.push(test
);
104 // We don't want to recurse into anything other than mods, since
105 // mods or tests inside of functions will break things
106 if let ast
::ItemKind
::Mod(..) = item
.kind
{
107 let tests
= mem
::take(&mut self.tests
);
108 noop_visit_item_kind(&mut item
.kind
, self);
109 let mut tests
= mem
::replace(&mut self.tests
, tests
);
111 if !tests
.is_empty() {
113 if item
.id
== ast
::DUMMY_NODE_ID { ast::CRATE_NODE_ID }
else { item.id }
;
114 // Create an identifier that will hygienically resolve the test
115 // case name, even in another module.
116 let inner_span
= match item
.kind
{
117 ast
::ItemKind
::Mod(_
, ModKind
::Loaded(.., span
)) => span
,
120 let expn_id
= self.cx
.ext_cx
.resolver
.expansion_for_ast_pass(
122 AstPass
::TestHarness
,
126 for test
in &mut tests
{
127 // See the comment on `mk_main` for why we're using
128 // `apply_mark` directly.
129 test
.ident
.span
= test
.ident
.span
.apply_mark(expn_id
, Transparency
::Opaque
);
131 self.cx
.test_cases
.extend(tests
);
138 // Beware, this is duplicated in librustc_passes/entry.rs (with
139 // `rustc_hir::Item`), so make sure to keep them in sync.
140 fn entry_point_type(sess
: &Session
, item
: &ast
::Item
, depth
: usize) -> EntryPointType
{
142 ast
::ItemKind
::Fn(..) => {
143 if sess
.contains_name(&item
.attrs
, sym
::start
) {
144 EntryPointType
::Start
145 } else if sess
.contains_name(&item
.attrs
, sym
::main
) {
146 EntryPointType
::MainAttr
147 } else if item
.ident
.name
== sym
::main
{
149 // This is a top-level function so can be 'main'
150 EntryPointType
::MainNamed
152 EntryPointType
::OtherMain
158 _
=> EntryPointType
::None
,
161 /// A folder used to remove any entry points (like fn main) because the harness
162 /// generator will provide its own
163 struct EntryPointCleaner
<'a
> {
164 // Current depth in the ast
170 impl<'a
> MutVisitor
for EntryPointCleaner
<'a
> {
171 fn flat_map_item(&mut self, i
: P
<ast
::Item
>) -> SmallVec
<[P
<ast
::Item
>; 1]> {
173 let item
= noop_flat_map_item(i
, self).expect_one("noop did something");
176 // Remove any #[main] or #[start] from the AST so it doesn't
177 // clash with the one we're going to add, but mark it as
178 // #[allow(dead_code)] to avoid printing warnings.
179 let item
= match entry_point_type(self.sess
, &item
, self.depth
) {
180 EntryPointType
::MainNamed
| EntryPointType
::MainAttr
| EntryPointType
::Start
=> item
181 .map(|ast
::Item { id, ident, attrs, kind, vis, span, tokens }
| {
182 let allow_ident
= Ident
::new(sym
::allow
, self.def_site
);
184 attr
::mk_nested_word_item(Ident
::new(sym
::dead_code
, self.def_site
));
185 let allow_dead_code_item
= attr
::mk_list_item(allow_ident
, vec
![dc_nested
]);
186 let allow_dead_code
= attr
::mk_attr_outer(allow_dead_code_item
);
190 !self.sess
.check_name(attr
, sym
::main
)
191 && !self.sess
.check_name(attr
, sym
::start
)
193 .chain(iter
::once(allow_dead_code
))
196 ast
::Item { id, ident, attrs, kind, vis, span, tokens }
198 EntryPointType
::None
| EntryPointType
::OtherMain
=> item
,
205 /// Crawl over the crate, inserting test reexports and the test main function
206 fn generate_test_harness(
208 resolver
: &mut dyn ResolverExpand
,
209 reexport_test_harness_main
: Option
<Symbol
>,
210 krate
: &mut ast
::Crate
,
212 panic_strategy
: PanicStrategy
,
213 test_runner
: Option
<ast
::Path
>,
215 let mut econfig
= ExpansionConfig
::default("test".to_string());
216 econfig
.features
= Some(features
);
218 let ext_cx
= ExtCtxt
::new(sess
, econfig
, resolver
, None
);
220 let expn_id
= ext_cx
.resolver
.expansion_for_ast_pass(
222 AstPass
::TestHarness
,
223 &[sym
::main
, sym
::test
, sym
::rustc_attrs
],
226 let def_site
= DUMMY_SP
.with_def_site_ctxt(expn_id
);
228 // Remove the entry points
229 let mut cleaner
= EntryPointCleaner { sess, depth: 0, def_site }
;
230 cleaner
.visit_crate(krate
);
236 test_cases
: Vec
::new(),
237 reexport_test_harness_main
,
241 TestHarnessGenerator { cx, tests: Vec::new() }
.visit_crate(krate
);
244 /// Creates a function item for use as the main function of a test build.
245 /// This function will call the `test_runner` as specified by the crate attribute
247 /// By default this expands to
252 /// extern crate test;
253 /// test::test_main_static(&[
261 /// Most of the Ident have the usual def-site hygiene for the AST pass. The
262 /// exception is the `test_const`s. These have a syntax context that has two
263 /// opaque marks: one from the expansion of `test` or `test_case`, and one
264 /// generated in `TestHarnessGenerator::flat_map_item`. When resolving this
265 /// identifier after failing to find a matching identifier in the root module
266 /// we remove the outer mark, and try resolving at its def-site, which will
267 /// then resolve to `test_const`.
269 /// The expansion here can be controlled by two attributes:
271 /// [`TestCtxt::reexport_test_harness_main`] provides a different name for the `main`
272 /// function and [`TestCtxt::test_runner`] provides a path that replaces
273 /// `test::test_main_static`.
274 fn mk_main(cx
: &mut TestCtxt
<'_
>) -> P
<ast
::Item
> {
275 let sp
= cx
.def_site
;
276 let ecx
= &cx
.ext_cx
;
277 let test_id
= Ident
::new(sym
::test
, sp
);
279 let runner_name
= match cx
.panic_strategy
{
280 PanicStrategy
::Unwind
=> "test_main_static",
281 PanicStrategy
::Abort
=> "test_main_static_abort",
284 // test::test_main_static(...)
285 let mut test_runner
= cx
288 .unwrap_or_else(|| ecx
.path(sp
, vec
![test_id
, Ident
::from_str_and_span(runner_name
, sp
)]));
290 test_runner
.span
= sp
;
292 let test_main_path_expr
= ecx
.expr_path(test_runner
);
293 let call_test_main
= ecx
.expr_call(sp
, test_main_path_expr
, vec
![mk_tests_slice(cx
, sp
)]);
294 let call_test_main
= ecx
.stmt_expr(call_test_main
);
297 let test_extern_stmt
=
298 ecx
.stmt_item(sp
, ecx
.item(sp
, test_id
, vec
![], ast
::ItemKind
::ExternCrate(None
)));
301 let main_meta
= ecx
.meta_word(sp
, sym
::main
);
302 let main_attr
= ecx
.attribute(main_meta
);
304 // pub fn main() { ... }
305 let main_ret_ty
= ecx
.ty(sp
, ast
::TyKind
::Tup(vec
![]));
307 // If no test runner is provided we need to import the test crate
308 let main_body
= if cx
.test_runner
.is_none() {
309 ecx
.block(sp
, vec
![test_extern_stmt
, call_test_main
])
311 ecx
.block(sp
, vec
![call_test_main
])
314 let decl
= ecx
.fn_decl(vec
![], ast
::FnRetTy
::Ty(main_ret_ty
));
315 let sig
= ast
::FnSig { decl, header: ast::FnHeader::default(), span: sp }
;
316 let def
= ast
::Defaultness
::Final
;
318 ast
::ItemKind
::Fn(box ast
::FnKind(def
, sig
, ast
::Generics
::default(), Some(main_body
)));
320 // Honor the reexport_test_harness_main attribute
321 let main_id
= match cx
.reexport_test_harness_main
{
322 Some(sym
) => Ident
::new(sym
, sp
.with_ctxt(SyntaxContext
::root())),
323 None
=> Ident
::new(sym
::main
, sp
),
326 let main
= P(ast
::Item
{
328 attrs
: vec
![main_attr
],
329 id
: ast
::DUMMY_NODE_ID
,
331 vis
: ast
::Visibility { span: sp, kind: ast::VisibilityKind::Public, tokens: None }
,
336 // Integrate the new item into existing module structures.
337 let main
= AstFragment
::Items(smallvec
![main
]);
338 cx
.ext_cx
.monotonic_expander().fully_expand_fragment(main
).make_items().pop().unwrap()
341 /// Creates a slice containing every test like so:
342 /// &[&test1, &test2]
343 fn mk_tests_slice(cx
: &TestCtxt
<'_
>, sp
: Span
) -> P
<ast
::Expr
> {
344 debug
!("building test vector from {} tests", cx
.test_cases
.len());
345 let ecx
= &cx
.ext_cx
;
352 ecx
.expr_addr_of(test
.span
, ecx
.expr_path(ecx
.path(test
.span
, vec
![test
.ident
])))
358 fn is_test_case(sess
: &Session
, i
: &ast
::Item
) -> bool
{
359 sess
.contains_name(&i
.attrs
, sym
::rustc_test_marker
)
364 sd
: &rustc_errors
::Handler
,
366 ) -> Option
<ast
::Path
> {
367 let test_attr
= sess
.find_by_name(&krate
.attrs
, sym
::test_runner
)?
;
368 let meta_list
= test_attr
.meta_item_list()?
;
369 let span
= test_attr
.span
;
371 [single
] => match single
.meta_item() {
372 Some(meta_item
) if meta_item
.is_word() => return Some(meta_item
.path
.clone()),
373 _
=> sd
.struct_span_err(span
, "`test_runner` argument must be a path").emit(),
375 _
=> sd
.struct_span_err(span
, "`#![test_runner(..)]` accepts exactly 1 argument").emit(),