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