]>
Commit | Line | Data |
---|---|---|
1a4d82fc JJ |
1 | // Code that generates a test runner to run all the tests in a crate |
2 | ||
3dfed10e | 3 | use rustc_ast as ast; |
3dfed10e | 4 | use rustc_ast::entry::EntryPointType; |
74b04a01 XL |
5 | use rustc_ast::mut_visit::{ExpectOne, *}; |
6 | use rustc_ast::ptr::P; | |
6a06907d | 7 | use rustc_ast::{attr, ModKind}; |
f035d41b | 8 | use rustc_expand::base::{ExtCtxt, ResolverExpand}; |
dfeec247 | 9 | use rustc_expand::expand::{AstFragment, ExpansionConfig}; |
60c5eb7d | 10 | use rustc_feature::Features; |
3dfed10e | 11 | use rustc_session::Session; |
dfeec247 | 12 | use rustc_span::hygiene::{AstPass, SyntaxContext, Transparency}; |
f9f354fc | 13 | use rustc_span::symbol::{sym, Ident, Symbol}; |
dfeec247 | 14 | use rustc_span::{Span, DUMMY_SP}; |
e74abb32 | 15 | use rustc_target::spec::PanicStrategy; |
dfeec247 | 16 | use smallvec::{smallvec, SmallVec}; |
3dfed10e | 17 | use tracing::debug; |
416331ca XL |
18 | |
19 | use std::{iter, mem}; | |
1a4d82fc JJ |
20 | |
21 | struct Test { | |
22 | span: Span, | |
e1599b0c | 23 | ident: Ident, |
1a4d82fc JJ |
24 | } |
25 | ||
26 | struct TestCtxt<'a> { | |
1a4d82fc | 27 | ext_cx: ExtCtxt<'a>, |
e74abb32 | 28 | panic_strategy: PanicStrategy, |
e1599b0c | 29 | def_site: Span, |
b7449926 | 30 | test_cases: Vec<Test>, |
476ff2be | 31 | reexport_test_harness_main: Option<Symbol>, |
b7449926 | 32 | test_runner: Option<ast::Path>, |
1a4d82fc JJ |
33 | } |
34 | ||
35 | // Traverse the crate, collecting all the test functions, eliding any | |
36 | // existing main functions, and synthesizing a main test harness | |
3dfed10e XL |
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(); | |
29967ef6 | 40 | let platform_panic_strategy = sess.target.panic_strategy; |
3dfed10e | 41 | |
e1599b0c XL |
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 | |
1a4d82fc JJ |
44 | // unconditional, so that the attribute is still marked as used in |
45 | // non-test builds. | |
46 | let reexport_test_harness_main = | |
3dfed10e | 47 | sess.first_attr_value_str_by_name(&krate.attrs, sym::reexport_test_harness_main); |
1a4d82fc | 48 | |
b7449926 XL |
49 | // Do this here so that the test_runner crate attribute gets marked as used |
50 | // even in non-test builds | |
3dfed10e | 51 | let test_runner = get_test_runner(sess, span_diagnostic, &krate); |
b7449926 | 52 | |
3dfed10e XL |
53 | if sess.opts.test { |
54 | let panic_strategy = match (panic_strategy, sess.opts.debugging_opts.panic_abort_tests) { | |
dfeec247 | 55 | (PanicStrategy::Abort, true) => PanicStrategy::Abort, |
e74abb32 | 56 | (PanicStrategy::Abort, false) => { |
3dfed10e XL |
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). | |
60 | } else { | |
61 | span_diagnostic.err( | |
62 | "building tests with panic=abort is not supported \ | |
63 | without `-Zpanic_abort_tests`", | |
64 | ); | |
65 | } | |
e74abb32 XL |
66 | PanicStrategy::Unwind |
67 | } | |
68 | (PanicStrategy::Unwind, _) => PanicStrategy::Unwind, | |
69 | }; | |
dfeec247 XL |
70 | generate_test_harness( |
71 | sess, | |
72 | resolver, | |
73 | reexport_test_harness_main, | |
74 | krate, | |
3dfed10e | 75 | &sess.features_untracked(), |
dfeec247 XL |
76 | panic_strategy, |
77 | test_runner, | |
78 | ) | |
1a4d82fc JJ |
79 | } |
80 | } | |
81 | ||
82 | struct TestHarnessGenerator<'a> { | |
83 | cx: TestCtxt<'a>, | |
e1599b0c | 84 | tests: Vec<Test>, |
1a4d82fc JJ |
85 | } |
86 | ||
9fa01778 XL |
87 | impl<'a> MutVisitor for TestHarnessGenerator<'a> { |
88 | fn visit_crate(&mut self, c: &mut ast::Crate) { | |
89 | noop_visit_crate(c, self); | |
1a4d82fc | 90 | |
b7449926 | 91 | // Create a main function to run our tests |
6a06907d | 92 | c.items.push(mk_main(&mut self.cx)); |
1a4d82fc JJ |
93 | } |
94 | ||
9fa01778 | 95 | fn flat_map_item(&mut self, i: P<ast::Item>) -> SmallVec<[P<ast::Item>; 1]> { |
b7449926 | 96 | let mut item = i.into_inner(); |
3dfed10e | 97 | if is_test_case(&self.cx.ext_cx.sess, &item) { |
b7449926 | 98 | debug!("this is a test item"); |
8faf50e0 | 99 | |
dfeec247 | 100 | let test = Test { span: item.span, ident: item.ident }; |
e1599b0c | 101 | self.tests.push(test); |
c30ab7b3 | 102 | } |
1a4d82fc JJ |
103 | |
104 | // We don't want to recurse into anything other than mods, since | |
105 | // mods or tests inside of functions will break things | |
6a06907d | 106 | if let ast::ItemKind::Mod(..) = item.kind { |
416331ca | 107 | let tests = mem::take(&mut self.tests); |
6a06907d | 108 | noop_visit_item_kind(&mut item.kind, self); |
e1599b0c | 109 | let mut tests = mem::replace(&mut self.tests, tests); |
c30ab7b3 | 110 | |
e1599b0c | 111 | if !tests.is_empty() { |
dfeec247 XL |
112 | let parent = |
113 | if item.id == ast::DUMMY_NODE_ID { ast::CRATE_NODE_ID } else { item.id }; | |
e1599b0c XL |
114 | // Create an identifier that will hygienically resolve the test |
115 | // case name, even in another module. | |
6a06907d XL |
116 | let inner_span = match item.kind { |
117 | ast::ItemKind::Mod(_, ModKind::Loaded(.., span)) => span, | |
118 | _ => unreachable!(), | |
119 | }; | |
e1599b0c | 120 | let expn_id = self.cx.ext_cx.resolver.expansion_for_ast_pass( |
6a06907d | 121 | inner_span, |
e1599b0c XL |
122 | AstPass::TestHarness, |
123 | &[], | |
124 | Some(parent), | |
125 | ); | |
126 | for test in &mut tests { | |
127 | // See the comment on `mk_main` for why we're using | |
128 | // `apply_mark` directly. | |
136023e0 XL |
129 | test.ident.span = |
130 | test.ident.span.apply_mark(expn_id.to_expn_id(), Transparency::Opaque); | |
c30ab7b3 | 131 | } |
e1599b0c | 132 | self.cx.test_cases.extend(tests); |
c30ab7b3 | 133 | } |
c30ab7b3 | 134 | } |
b7449926 | 135 | smallvec![P(item)] |
1a4d82fc JJ |
136 | } |
137 | } | |
138 | ||
3dfed10e XL |
139 | // Beware, this is duplicated in librustc_passes/entry.rs (with |
140 | // `rustc_hir::Item`), so make sure to keep them in sync. | |
141 | fn entry_point_type(sess: &Session, item: &ast::Item, depth: usize) -> EntryPointType { | |
142 | match item.kind { | |
143 | ast::ItemKind::Fn(..) => { | |
144 | if sess.contains_name(&item.attrs, sym::start) { | |
145 | EntryPointType::Start | |
cdc7bbd5 | 146 | } else if sess.contains_name(&item.attrs, sym::rustc_main) { |
3dfed10e XL |
147 | EntryPointType::MainAttr |
148 | } else if item.ident.name == sym::main { | |
149 | if depth == 1 { | |
150 | // This is a top-level function so can be 'main' | |
151 | EntryPointType::MainNamed | |
152 | } else { | |
153 | EntryPointType::OtherMain | |
154 | } | |
155 | } else { | |
156 | EntryPointType::None | |
157 | } | |
158 | } | |
159 | _ => EntryPointType::None, | |
160 | } | |
161 | } | |
b7449926 XL |
162 | /// A folder used to remove any entry points (like fn main) because the harness |
163 | /// generator will provide its own | |
3dfed10e | 164 | struct EntryPointCleaner<'a> { |
e9174d1e | 165 | // Current depth in the ast |
3dfed10e | 166 | sess: &'a Session, |
e9174d1e | 167 | depth: usize, |
e1599b0c | 168 | def_site: Span, |
e9174d1e SL |
169 | } |
170 | ||
3dfed10e | 171 | impl<'a> MutVisitor for EntryPointCleaner<'a> { |
9fa01778 | 172 | fn flat_map_item(&mut self, i: P<ast::Item>) -> SmallVec<[P<ast::Item>; 1]> { |
e9174d1e | 173 | self.depth += 1; |
9fa01778 | 174 | let item = noop_flat_map_item(i, self).expect_one("noop did something"); |
e9174d1e SL |
175 | self.depth -= 1; |
176 | ||
177 | // Remove any #[main] or #[start] from the AST so it doesn't | |
178 | // clash with the one we're going to add, but mark it as | |
179 | // #[allow(dead_code)] to avoid printing warnings. | |
3dfed10e | 180 | let item = match entry_point_type(self.sess, &item, self.depth) { |
dfeec247 XL |
181 | EntryPointType::MainNamed | EntryPointType::MainAttr | EntryPointType::Start => item |
182 | .map(|ast::Item { id, ident, attrs, kind, vis, span, tokens }| { | |
e1599b0c | 183 | let allow_ident = Ident::new(sym::allow, self.def_site); |
3dfed10e XL |
184 | let dc_nested = |
185 | attr::mk_nested_word_item(Ident::new(sym::dead_code, self.def_site)); | |
416331ca XL |
186 | let allow_dead_code_item = attr::mk_list_item(allow_ident, vec![dc_nested]); |
187 | let allow_dead_code = attr::mk_attr_outer(allow_dead_code_item); | |
74b04a01 XL |
188 | let attrs = attrs |
189 | .into_iter() | |
3dfed10e | 190 | .filter(|attr| { |
94222f64 | 191 | !attr.has_name(sym::rustc_main) && !attr.has_name(sym::start) |
3dfed10e | 192 | }) |
74b04a01 XL |
193 | .chain(iter::once(allow_dead_code)) |
194 | .collect(); | |
e9174d1e | 195 | |
74b04a01 | 196 | ast::Item { id, ident, attrs, kind, vis, span, tokens } |
e9174d1e | 197 | }), |
dfeec247 | 198 | EntryPointType::None | EntryPointType::OtherMain => item, |
e9174d1e SL |
199 | }; |
200 | ||
9fa01778 | 201 | smallvec![item] |
e9174d1e SL |
202 | } |
203 | } | |
204 | ||
b7449926 | 205 | /// Crawl over the crate, inserting test reexports and the test main function |
dfeec247 | 206 | fn generate_test_harness( |
3dfed10e | 207 | sess: &Session, |
f035d41b | 208 | resolver: &mut dyn ResolverExpand, |
dfeec247 XL |
209 | reexport_test_harness_main: Option<Symbol>, |
210 | krate: &mut ast::Crate, | |
211 | features: &Features, | |
212 | panic_strategy: PanicStrategy, | |
213 | test_runner: Option<ast::Path>, | |
214 | ) { | |
0531ce1d XL |
215 | let mut econfig = ExpansionConfig::default("test".to_string()); |
216 | econfig.features = Some(features); | |
217 | ||
ba9703b0 | 218 | let ext_cx = ExtCtxt::new(sess, econfig, resolver, None); |
e1599b0c XL |
219 | |
220 | let expn_id = ext_cx.resolver.expansion_for_ast_pass( | |
221 | DUMMY_SP, | |
222 | AstPass::TestHarness, | |
cdc7bbd5 | 223 | &[sym::test, sym::rustc_attrs], |
e1599b0c XL |
224 | None, |
225 | ); | |
136023e0 | 226 | let def_site = DUMMY_SP.with_def_site_ctxt(expn_id.to_expn_id()); |
e1599b0c XL |
227 | |
228 | // Remove the entry points | |
3dfed10e | 229 | let mut cleaner = EntryPointCleaner { sess, depth: 0, def_site }; |
e1599b0c XL |
230 | cleaner.visit_crate(krate); |
231 | ||
ff7c6d11 | 232 | let cx = TestCtxt { |
e1599b0c | 233 | ext_cx, |
e74abb32 | 234 | panic_strategy, |
e1599b0c | 235 | def_site, |
b7449926 | 236 | test_cases: Vec::new(), |
3b2f2976 | 237 | reexport_test_harness_main, |
dfeec247 | 238 | test_runner, |
1a4d82fc JJ |
239 | }; |
240 | ||
dfeec247 | 241 | TestHarnessGenerator { cx, tests: Vec::new() }.visit_crate(krate); |
1a4d82fc JJ |
242 | } |
243 | ||
b7449926 XL |
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 | |
e1599b0c XL |
246 | /// |
247 | /// By default this expands to | |
248 | /// | |
f9f354fc | 249 | /// ``` |
cdc7bbd5 | 250 | /// #[rustc_main] |
e1599b0c XL |
251 | /// pub fn main() { |
252 | /// extern crate test; | |
253 | /// test::test_main_static(&[ | |
254 | /// &test_const1, | |
255 | /// &test_const2, | |
256 | /// &test_const3, | |
257 | /// ]); | |
258 | /// } | |
f9f354fc | 259 | /// ``` |
e1599b0c XL |
260 | /// |
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`. | |
268 | /// | |
269 | /// The expansion here can be controlled by two attributes: | |
270 | /// | |
f9f354fc XL |
271 | /// [`TestCtxt::reexport_test_harness_main`] provides a different name for the `main` |
272 | /// function and [`TestCtxt::test_runner`] provides a path that replaces | |
e1599b0c | 273 | /// `test::test_main_static`. |
9fa01778 | 274 | fn mk_main(cx: &mut TestCtxt<'_>) -> P<ast::Item> { |
e1599b0c | 275 | let sp = cx.def_site; |
85aaf69f | 276 | let ecx = &cx.ext_cx; |
e1599b0c | 277 | let test_id = Ident::new(sym::test, sp); |
476ff2be | 278 | |
e74abb32 XL |
279 | let runner_name = match cx.panic_strategy { |
280 | PanicStrategy::Unwind => "test_main_static", | |
281 | PanicStrategy::Abort => "test_main_static_abort", | |
282 | }; | |
283 | ||
85aaf69f | 284 | // test::test_main_static(...) |
dfeec247 XL |
285 | let mut test_runner = cx |
286 | .test_runner | |
287 | .clone() | |
29967ef6 | 288 | .unwrap_or_else(|| ecx.path(sp, vec![test_id, Ident::from_str_and_span(runner_name, sp)])); |
b7449926 XL |
289 | |
290 | test_runner.span = sp; | |
291 | ||
0bf4aa26 | 292 | let test_main_path_expr = ecx.expr_path(test_runner); |
dfeec247 | 293 | let call_test_main = ecx.expr_call(sp, test_main_path_expr, vec![mk_tests_slice(cx, sp)]); |
85aaf69f | 294 | let call_test_main = ecx.stmt_expr(call_test_main); |
b7449926 | 295 | |
e1599b0c | 296 | // extern crate test |
dfeec247 XL |
297 | let test_extern_stmt = |
298 | ecx.stmt_item(sp, ecx.item(sp, test_id, vec![], ast::ItemKind::ExternCrate(None))); | |
b7449926 | 299 | |
cdc7bbd5 XL |
300 | // #[rustc_main] |
301 | let main_meta = ecx.meta_word(sp, sym::rustc_main); | |
e1599b0c XL |
302 | let main_attr = ecx.attribute(main_meta); |
303 | ||
85aaf69f | 304 | // pub fn main() { ... } |
7453a54e | 305 | let main_ret_ty = ecx.ty(sp, ast::TyKind::Tup(vec![])); |
b7449926 XL |
306 | |
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]) | |
310 | } else { | |
311 | ecx.block(sp, vec![call_test_main]) | |
312 | }; | |
313 | ||
74b04a01 | 314 | let decl = ecx.fn_decl(vec![], ast::FnRetTy::Ty(main_ret_ty)); |
3dfed10e | 315 | let sig = ast::FnSig { decl, header: ast::FnHeader::default(), span: sp }; |
3c0e092e XL |
316 | let defaultness = ast::Defaultness::Final; |
317 | let main = ast::ItemKind::Fn(Box::new(ast::Fn { | |
318 | defaultness, | |
94222f64 | 319 | sig, |
3c0e092e XL |
320 | generics: ast::Generics::default(), |
321 | body: Some(main_body), | |
322 | })); | |
b7449926 XL |
323 | |
324 | // Honor the reexport_test_harness_main attribute | |
dc9dc135 | 325 | let main_id = match cx.reexport_test_harness_main { |
e1599b0c XL |
326 | Some(sym) => Ident::new(sym, sp.with_ctxt(SyntaxContext::root())), |
327 | None => Ident::new(sym::main, sp), | |
dc9dc135 | 328 | }; |
b7449926 | 329 | |
e1599b0c | 330 | let main = P(ast::Item { |
b7449926 | 331 | ident: main_id, |
85aaf69f SL |
332 | attrs: vec![main_attr], |
333 | id: ast::DUMMY_NODE_ID, | |
e74abb32 | 334 | kind: main, |
1b1a35ee | 335 | vis: ast::Visibility { span: sp, kind: ast::VisibilityKind::Public, tokens: None }, |
3b2f2976 XL |
336 | span: sp, |
337 | tokens: None, | |
e1599b0c | 338 | }); |
1a4d82fc | 339 | |
e1599b0c XL |
340 | // Integrate the new item into existing module structures. |
341 | let main = AstFragment::Items(smallvec![main]); | |
342 | cx.ext_cx.monotonic_expander().fully_expand_fragment(main).make_items().pop().unwrap() | |
54a0048b SL |
343 | } |
344 | ||
b7449926 | 345 | /// Creates a slice containing every test like so: |
e1599b0c XL |
346 | /// &[&test1, &test2] |
347 | fn mk_tests_slice(cx: &TestCtxt<'_>, sp: Span) -> P<ast::Expr> { | |
b7449926 | 348 | debug!("building test vector from {} tests", cx.test_cases.len()); |
74b04a01 | 349 | let ecx = &cx.ext_cx; |
b7449926 | 350 | |
dfeec247 XL |
351 | ecx.expr_vec_slice( |
352 | sp, | |
353 | cx.test_cases | |
354 | .iter() | |
355 | .map(|test| { | |
356 | ecx.expr_addr_of(test.span, ecx.expr_path(ecx.path(test.span, vec![test.ident]))) | |
357 | }) | |
358 | .collect(), | |
359 | ) | |
1a4d82fc JJ |
360 | } |
361 | ||
3dfed10e XL |
362 | fn is_test_case(sess: &Session, i: &ast::Item) -> bool { |
363 | sess.contains_name(&i.attrs, sym::rustc_test_marker) | |
b7449926 | 364 | } |
1a4d82fc | 365 | |
3dfed10e XL |
366 | fn get_test_runner( |
367 | sess: &Session, | |
368 | sd: &rustc_errors::Handler, | |
369 | krate: &ast::Crate, | |
370 | ) -> Option<ast::Path> { | |
371 | let test_attr = sess.find_by_name(&krate.attrs, sym::test_runner)?; | |
ba9703b0 XL |
372 | let meta_list = test_attr.meta_item_list()?; |
373 | let span = test_attr.span; | |
374 | match &*meta_list { | |
375 | [single] => match single.meta_item() { | |
376 | Some(meta_item) if meta_item.is_word() => return Some(meta_item.path.clone()), | |
377 | _ => sd.struct_span_err(span, "`test_runner` argument must be a path").emit(), | |
378 | }, | |
379 | _ => sd.struct_span_err(span, "`#![test_runner(..)]` accepts exactly 1 argument").emit(), | |
380 | } | |
381 | None | |
1a4d82fc | 382 | } |