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