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