]>
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}; |
f2b60f7d | 17 | use thin_vec::thin_vec; |
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 | 53 | if sess.opts.test { |
064997fb | 54 | let panic_strategy = match (panic_strategy, sess.opts.unstable_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 | ||
a2a8927a XL |
87 | impl TestHarnessGenerator<'_> { |
88 | fn add_test_cases(&mut self, node_id: ast::NodeId, span: Span, prev_tests: Vec<Test>) { | |
89 | let mut tests = mem::replace(&mut self.tests, prev_tests); | |
90 | ||
91 | if !tests.is_empty() { | |
92 | // Create an identifier that will hygienically resolve the test | |
93 | // case name, even in another module. | |
94 | let expn_id = self.cx.ext_cx.resolver.expansion_for_ast_pass( | |
95 | span, | |
96 | AstPass::TestHarness, | |
97 | &[], | |
98 | Some(node_id), | |
99 | ); | |
100 | for test in &mut tests { | |
101 | // See the comment on `mk_main` for why we're using | |
102 | // `apply_mark` directly. | |
103 | test.ident.span = | |
104 | test.ident.span.apply_mark(expn_id.to_expn_id(), Transparency::Opaque); | |
105 | } | |
106 | self.cx.test_cases.extend(tests); | |
107 | } | |
108 | } | |
109 | } | |
110 | ||
9fa01778 XL |
111 | impl<'a> MutVisitor for TestHarnessGenerator<'a> { |
112 | fn visit_crate(&mut self, c: &mut ast::Crate) { | |
a2a8927a | 113 | let prev_tests = mem::take(&mut self.tests); |
9fa01778 | 114 | noop_visit_crate(c, self); |
5e7ed085 | 115 | self.add_test_cases(ast::CRATE_NODE_ID, c.spans.inner_span, prev_tests); |
1a4d82fc | 116 | |
b7449926 | 117 | // Create a main function to run our tests |
6a06907d | 118 | c.items.push(mk_main(&mut self.cx)); |
1a4d82fc JJ |
119 | } |
120 | ||
9fa01778 | 121 | fn flat_map_item(&mut self, i: P<ast::Item>) -> SmallVec<[P<ast::Item>; 1]> { |
b7449926 | 122 | let mut item = i.into_inner(); |
3dfed10e | 123 | if is_test_case(&self.cx.ext_cx.sess, &item) { |
b7449926 | 124 | debug!("this is a test item"); |
8faf50e0 | 125 | |
dfeec247 | 126 | let test = Test { span: item.span, ident: item.ident }; |
e1599b0c | 127 | self.tests.push(test); |
c30ab7b3 | 128 | } |
1a4d82fc JJ |
129 | |
130 | // We don't want to recurse into anything other than mods, since | |
131 | // mods or tests inside of functions will break things | |
5e7ed085 FG |
132 | if let ast::ItemKind::Mod(_, ModKind::Loaded(.., ref spans)) = item.kind { |
133 | let ast::ModSpans { inner_span: span, inject_use_span: _ } = *spans; | |
a2a8927a | 134 | let prev_tests = mem::take(&mut self.tests); |
6a06907d | 135 | noop_visit_item_kind(&mut item.kind, self); |
a2a8927a | 136 | self.add_test_cases(item.id, span, prev_tests); |
c30ab7b3 | 137 | } |
b7449926 | 138 | smallvec![P(item)] |
1a4d82fc JJ |
139 | } |
140 | } | |
141 | ||
3dfed10e XL |
142 | // Beware, this is duplicated in librustc_passes/entry.rs (with |
143 | // `rustc_hir::Item`), so make sure to keep them in sync. | |
144 | fn entry_point_type(sess: &Session, item: &ast::Item, depth: usize) -> EntryPointType { | |
145 | match item.kind { | |
146 | ast::ItemKind::Fn(..) => { | |
147 | if sess.contains_name(&item.attrs, sym::start) { | |
148 | EntryPointType::Start | |
cdc7bbd5 | 149 | } else if sess.contains_name(&item.attrs, sym::rustc_main) { |
923072b8 | 150 | EntryPointType::RustcMainAttr |
3dfed10e | 151 | } else if item.ident.name == sym::main { |
a2a8927a | 152 | if depth == 0 { |
3dfed10e XL |
153 | // This is a top-level function so can be 'main' |
154 | EntryPointType::MainNamed | |
155 | } else { | |
156 | EntryPointType::OtherMain | |
157 | } | |
158 | } else { | |
159 | EntryPointType::None | |
160 | } | |
161 | } | |
162 | _ => EntryPointType::None, | |
163 | } | |
164 | } | |
b7449926 XL |
165 | /// A folder used to remove any entry points (like fn main) because the harness |
166 | /// generator will provide its own | |
3dfed10e | 167 | struct EntryPointCleaner<'a> { |
e9174d1e | 168 | // Current depth in the ast |
3dfed10e | 169 | sess: &'a Session, |
e9174d1e | 170 | depth: usize, |
e1599b0c | 171 | def_site: Span, |
e9174d1e SL |
172 | } |
173 | ||
3dfed10e | 174 | impl<'a> MutVisitor for EntryPointCleaner<'a> { |
9fa01778 | 175 | fn flat_map_item(&mut self, i: P<ast::Item>) -> SmallVec<[P<ast::Item>; 1]> { |
e9174d1e | 176 | self.depth += 1; |
9fa01778 | 177 | let item = noop_flat_map_item(i, self).expect_one("noop did something"); |
e9174d1e SL |
178 | self.depth -= 1; |
179 | ||
923072b8 | 180 | // Remove any #[rustc_main] or #[start] from the AST so it doesn't |
e9174d1e SL |
181 | // clash with the one we're going to add, but mark it as |
182 | // #[allow(dead_code)] to avoid printing warnings. | |
3dfed10e | 183 | let item = match entry_point_type(self.sess, &item, self.depth) { |
923072b8 FG |
184 | EntryPointType::MainNamed | EntryPointType::RustcMainAttr | EntryPointType::Start => { |
185 | item.map(|ast::Item { id, ident, attrs, kind, vis, span, tokens }| { | |
e1599b0c | 186 | let allow_ident = Ident::new(sym::allow, self.def_site); |
3dfed10e XL |
187 | let dc_nested = |
188 | attr::mk_nested_word_item(Ident::new(sym::dead_code, self.def_site)); | |
416331ca | 189 | let allow_dead_code_item = attr::mk_list_item(allow_ident, vec![dc_nested]); |
f2b60f7d FG |
190 | let allow_dead_code = attr::mk_attr_outer( |
191 | &self.sess.parse_sess.attr_id_generator, | |
192 | allow_dead_code_item, | |
193 | ); | |
74b04a01 XL |
194 | let attrs = attrs |
195 | .into_iter() | |
3dfed10e | 196 | .filter(|attr| { |
94222f64 | 197 | !attr.has_name(sym::rustc_main) && !attr.has_name(sym::start) |
3dfed10e | 198 | }) |
74b04a01 XL |
199 | .chain(iter::once(allow_dead_code)) |
200 | .collect(); | |
e9174d1e | 201 | |
74b04a01 | 202 | ast::Item { id, ident, attrs, kind, vis, span, tokens } |
923072b8 FG |
203 | }) |
204 | } | |
dfeec247 | 205 | EntryPointType::None | EntryPointType::OtherMain => item, |
e9174d1e SL |
206 | }; |
207 | ||
9fa01778 | 208 | smallvec![item] |
e9174d1e SL |
209 | } |
210 | } | |
211 | ||
b7449926 | 212 | /// Crawl over the crate, inserting test reexports and the test main function |
dfeec247 | 213 | fn generate_test_harness( |
3dfed10e | 214 | sess: &Session, |
f035d41b | 215 | resolver: &mut dyn ResolverExpand, |
dfeec247 XL |
216 | reexport_test_harness_main: Option<Symbol>, |
217 | krate: &mut ast::Crate, | |
218 | features: &Features, | |
219 | panic_strategy: PanicStrategy, | |
220 | test_runner: Option<ast::Path>, | |
221 | ) { | |
0531ce1d XL |
222 | let mut econfig = ExpansionConfig::default("test".to_string()); |
223 | econfig.features = Some(features); | |
224 | ||
ba9703b0 | 225 | let ext_cx = ExtCtxt::new(sess, econfig, resolver, None); |
e1599b0c XL |
226 | |
227 | let expn_id = ext_cx.resolver.expansion_for_ast_pass( | |
228 | DUMMY_SP, | |
229 | AstPass::TestHarness, | |
cdc7bbd5 | 230 | &[sym::test, sym::rustc_attrs], |
e1599b0c XL |
231 | None, |
232 | ); | |
136023e0 | 233 | let def_site = DUMMY_SP.with_def_site_ctxt(expn_id.to_expn_id()); |
e1599b0c XL |
234 | |
235 | // Remove the entry points | |
3dfed10e | 236 | let mut cleaner = EntryPointCleaner { sess, depth: 0, def_site }; |
e1599b0c XL |
237 | cleaner.visit_crate(krate); |
238 | ||
ff7c6d11 | 239 | let cx = TestCtxt { |
e1599b0c | 240 | ext_cx, |
e74abb32 | 241 | panic_strategy, |
e1599b0c | 242 | def_site, |
b7449926 | 243 | test_cases: Vec::new(), |
3b2f2976 | 244 | reexport_test_harness_main, |
dfeec247 | 245 | test_runner, |
1a4d82fc JJ |
246 | }; |
247 | ||
dfeec247 | 248 | TestHarnessGenerator { cx, tests: Vec::new() }.visit_crate(krate); |
1a4d82fc JJ |
249 | } |
250 | ||
b7449926 XL |
251 | /// Creates a function item for use as the main function of a test build. |
252 | /// This function will call the `test_runner` as specified by the crate attribute | |
e1599b0c XL |
253 | /// |
254 | /// By default this expands to | |
255 | /// | |
04454e1e | 256 | /// ```ignore UNSOLVED (I think I still need guidance for this one. Is it correct? Do we try to make it run? How do we nicely fill it out?) |
cdc7bbd5 | 257 | /// #[rustc_main] |
e1599b0c XL |
258 | /// pub fn main() { |
259 | /// extern crate test; | |
260 | /// test::test_main_static(&[ | |
261 | /// &test_const1, | |
262 | /// &test_const2, | |
263 | /// &test_const3, | |
264 | /// ]); | |
265 | /// } | |
f9f354fc | 266 | /// ``` |
e1599b0c XL |
267 | /// |
268 | /// Most of the Ident have the usual def-site hygiene for the AST pass. The | |
269 | /// exception is the `test_const`s. These have a syntax context that has two | |
270 | /// opaque marks: one from the expansion of `test` or `test_case`, and one | |
271 | /// generated in `TestHarnessGenerator::flat_map_item`. When resolving this | |
272 | /// identifier after failing to find a matching identifier in the root module | |
273 | /// we remove the outer mark, and try resolving at its def-site, which will | |
274 | /// then resolve to `test_const`. | |
275 | /// | |
276 | /// The expansion here can be controlled by two attributes: | |
277 | /// | |
f9f354fc XL |
278 | /// [`TestCtxt::reexport_test_harness_main`] provides a different name for the `main` |
279 | /// function and [`TestCtxt::test_runner`] provides a path that replaces | |
e1599b0c | 280 | /// `test::test_main_static`. |
9fa01778 | 281 | fn mk_main(cx: &mut TestCtxt<'_>) -> P<ast::Item> { |
e1599b0c | 282 | let sp = cx.def_site; |
85aaf69f | 283 | let ecx = &cx.ext_cx; |
e1599b0c | 284 | let test_id = Ident::new(sym::test, sp); |
476ff2be | 285 | |
e74abb32 XL |
286 | let runner_name = match cx.panic_strategy { |
287 | PanicStrategy::Unwind => "test_main_static", | |
288 | PanicStrategy::Abort => "test_main_static_abort", | |
289 | }; | |
290 | ||
85aaf69f | 291 | // test::test_main_static(...) |
dfeec247 XL |
292 | let mut test_runner = cx |
293 | .test_runner | |
294 | .clone() | |
29967ef6 | 295 | .unwrap_or_else(|| ecx.path(sp, vec![test_id, Ident::from_str_and_span(runner_name, sp)])); |
b7449926 XL |
296 | |
297 | test_runner.span = sp; | |
298 | ||
0bf4aa26 | 299 | let test_main_path_expr = ecx.expr_path(test_runner); |
dfeec247 | 300 | let call_test_main = ecx.expr_call(sp, test_main_path_expr, vec![mk_tests_slice(cx, sp)]); |
85aaf69f | 301 | let call_test_main = ecx.stmt_expr(call_test_main); |
b7449926 | 302 | |
e1599b0c | 303 | // extern crate test |
f2b60f7d FG |
304 | let test_extern_stmt = ecx.stmt_item( |
305 | sp, | |
306 | ecx.item(sp, test_id, ast::AttrVec::new(), ast::ItemKind::ExternCrate(None)), | |
307 | ); | |
b7449926 | 308 | |
cdc7bbd5 XL |
309 | // #[rustc_main] |
310 | let main_meta = ecx.meta_word(sp, sym::rustc_main); | |
e1599b0c XL |
311 | let main_attr = ecx.attribute(main_meta); |
312 | ||
85aaf69f | 313 | // pub fn main() { ... } |
7453a54e | 314 | let main_ret_ty = ecx.ty(sp, ast::TyKind::Tup(vec![])); |
b7449926 XL |
315 | |
316 | // If no test runner is provided we need to import the test crate | |
317 | let main_body = if cx.test_runner.is_none() { | |
318 | ecx.block(sp, vec![test_extern_stmt, call_test_main]) | |
319 | } else { | |
320 | ecx.block(sp, vec![call_test_main]) | |
321 | }; | |
322 | ||
74b04a01 | 323 | let decl = ecx.fn_decl(vec![], ast::FnRetTy::Ty(main_ret_ty)); |
3dfed10e | 324 | let sig = ast::FnSig { decl, header: ast::FnHeader::default(), span: sp }; |
3c0e092e XL |
325 | let defaultness = ast::Defaultness::Final; |
326 | let main = ast::ItemKind::Fn(Box::new(ast::Fn { | |
327 | defaultness, | |
94222f64 | 328 | sig, |
3c0e092e XL |
329 | generics: ast::Generics::default(), |
330 | body: Some(main_body), | |
331 | })); | |
b7449926 XL |
332 | |
333 | // Honor the reexport_test_harness_main attribute | |
dc9dc135 | 334 | let main_id = match cx.reexport_test_harness_main { |
e1599b0c XL |
335 | Some(sym) => Ident::new(sym, sp.with_ctxt(SyntaxContext::root())), |
336 | None => Ident::new(sym::main, sp), | |
dc9dc135 | 337 | }; |
b7449926 | 338 | |
e1599b0c | 339 | let main = P(ast::Item { |
b7449926 | 340 | ident: main_id, |
f2b60f7d | 341 | attrs: thin_vec![main_attr], |
85aaf69f | 342 | id: ast::DUMMY_NODE_ID, |
e74abb32 | 343 | kind: main, |
1b1a35ee | 344 | vis: ast::Visibility { span: sp, kind: ast::VisibilityKind::Public, tokens: None }, |
3b2f2976 XL |
345 | span: sp, |
346 | tokens: None, | |
e1599b0c | 347 | }); |
1a4d82fc | 348 | |
e1599b0c XL |
349 | // Integrate the new item into existing module structures. |
350 | let main = AstFragment::Items(smallvec![main]); | |
351 | cx.ext_cx.monotonic_expander().fully_expand_fragment(main).make_items().pop().unwrap() | |
54a0048b SL |
352 | } |
353 | ||
b7449926 | 354 | /// Creates a slice containing every test like so: |
e1599b0c XL |
355 | /// &[&test1, &test2] |
356 | fn mk_tests_slice(cx: &TestCtxt<'_>, sp: Span) -> P<ast::Expr> { | |
b7449926 | 357 | debug!("building test vector from {} tests", cx.test_cases.len()); |
74b04a01 | 358 | let ecx = &cx.ext_cx; |
b7449926 | 359 | |
064997fb | 360 | ecx.expr_array_ref( |
dfeec247 XL |
361 | sp, |
362 | cx.test_cases | |
363 | .iter() | |
364 | .map(|test| { | |
365 | ecx.expr_addr_of(test.span, ecx.expr_path(ecx.path(test.span, vec![test.ident]))) | |
366 | }) | |
367 | .collect(), | |
368 | ) | |
1a4d82fc JJ |
369 | } |
370 | ||
3dfed10e XL |
371 | fn is_test_case(sess: &Session, i: &ast::Item) -> bool { |
372 | sess.contains_name(&i.attrs, sym::rustc_test_marker) | |
b7449926 | 373 | } |
1a4d82fc | 374 | |
3dfed10e XL |
375 | fn get_test_runner( |
376 | sess: &Session, | |
377 | sd: &rustc_errors::Handler, | |
378 | krate: &ast::Crate, | |
379 | ) -> Option<ast::Path> { | |
380 | let test_attr = sess.find_by_name(&krate.attrs, sym::test_runner)?; | |
ba9703b0 XL |
381 | let meta_list = test_attr.meta_item_list()?; |
382 | let span = test_attr.span; | |
383 | match &*meta_list { | |
384 | [single] => match single.meta_item() { | |
385 | Some(meta_item) if meta_item.is_word() => return Some(meta_item.path.clone()), | |
5e7ed085 FG |
386 | _ => { |
387 | sd.struct_span_err(span, "`test_runner` argument must be a path").emit(); | |
388 | } | |
ba9703b0 | 389 | }, |
5e7ed085 FG |
390 | _ => { |
391 | sd.struct_span_err(span, "`#![test_runner(..)]` accepts exactly 1 argument").emit(); | |
392 | } | |
ba9703b0 XL |
393 | } |
394 | None | |
1a4d82fc | 395 | } |