]>
Commit | Line | Data |
---|---|---|
3dfed10e | 1 | use rustc_ast as ast; |
cdc7bbd5 | 2 | use rustc_data_structures::fx::{FxHashMap, FxHashSet}; |
0531ce1d | 3 | use rustc_data_structures::sync::Lrc; |
5e7ed085 | 4 | use rustc_errors::{ColorConfig, ErrorGuaranteed, FatalError}; |
dfeec247 | 5 | use rustc_hir as hir; |
136023e0 | 6 | use rustc_hir::def_id::LOCAL_CRATE; |
dfeec247 | 7 | use rustc_hir::intravisit; |
f9f354fc | 8 | use rustc_hir::{HirId, CRATE_HIR_ID}; |
532ac7d7 | 9 | use rustc_interface::interface; |
ba9703b0 | 10 | use rustc_middle::hir::map::Map; |
5099ac24 | 11 | use rustc_middle::hir::nested_filter; |
f9f354fc | 12 | use rustc_middle::ty::TyCtxt; |
5e7ed085 FG |
13 | use rustc_parse::maybe_new_parser_from_source_str; |
14 | use rustc_parse::parser::attr::InnerAttrPolicy; | |
fc512014 | 15 | use rustc_session::config::{self, CrateType, ErrorOutputType}; |
5e7ed085 | 16 | use rustc_session::parse::ParseSess; |
f9f354fc | 17 | use rustc_session::{lint, DiagnosticOutput, Session}; |
dfeec247 XL |
18 | use rustc_span::edition::Edition; |
19 | use rustc_span::source_map::SourceMap; | |
20 | use rustc_span::symbol::sym; | |
136023e0 | 21 | use rustc_span::Symbol; |
dfeec247 | 22 | use rustc_span::{BytePos, FileName, Pos, Span, DUMMY_SP}; |
e1599b0c | 23 | use rustc_target::spec::TargetTriple; |
f9f354fc XL |
24 | use tempfile::Builder as TempFileBuilder; |
25 | ||
0731742a | 26 | use std::env; |
e1599b0c XL |
27 | use std::io::{self, Write}; |
28 | use std::panic; | |
532ac7d7 | 29 | use std::path::PathBuf; |
e1599b0c | 30 | use std::process::{self, Command, Stdio}; |
0731742a | 31 | use std::str; |
cdc7bbd5 XL |
32 | use std::sync::atomic::{AtomicUsize, Ordering}; |
33 | use std::sync::{Arc, Mutex}; | |
1a4d82fc | 34 | |
cdc7bbd5 | 35 | use crate::clean::{types::AttributesExt, Attributes}; |
a2a8927a | 36 | use crate::config::Options as RustdocOptions; |
dfeec247 | 37 | use crate::html::markdown::{self, ErrorCodes, Ignore, LangString}; |
6a06907d | 38 | use crate::lint::init_lints; |
f9f354fc | 39 | use crate::passes::span_of_attrs; |
1a4d82fc | 40 | |
a2a8927a | 41 | /// Options that apply to all doctests in a crate or Markdown file (for `rustdoc foo.md`). |
9346a6ac | 42 | #[derive(Clone, Default)] |
923072b8 | 43 | pub(crate) struct GlobalTestOptions { |
83c7162d | 44 | /// Whether to disable the default `extern crate my_crate;` when creating doctests. |
923072b8 | 45 | pub(crate) no_crate_inject: bool, |
83c7162d | 46 | /// Additional crate-level attributes to add to doctests. |
923072b8 | 47 | pub(crate) attrs: Vec<String>, |
9346a6ac AL |
48 | } |
49 | ||
923072b8 | 50 | pub(crate) fn run(options: RustdocOptions) -> Result<(), ErrorGuaranteed> { |
a1dfa0c6 | 51 | let input = config::Input::File(options.input.clone()); |
1a4d82fc | 52 | |
6a06907d | 53 | let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name; |
f9f354fc | 54 | |
6a06907d XL |
55 | // See core::create_config for what's going on here. |
56 | let allowed_lints = vec![ | |
57 | invalid_codeblock_attributes_name.to_owned(), | |
58 | lint::builtin::UNKNOWN_LINTS.name.to_owned(), | |
59 | lint::builtin::RENAMED_AND_REMOVED_LINTS.name.to_owned(), | |
60 | ]; | |
f9f354fc | 61 | |
f035d41b | 62 | let (lint_opts, lint_caps) = init_lints(allowed_lints, options.lint_opts.clone(), |lint| { |
3dfed10e | 63 | if lint.name == invalid_codeblock_attributes_name { |
f9f354fc XL |
64 | None |
65 | } else { | |
66 | Some((lint.name_lower(), lint::Allow)) | |
67 | } | |
68 | }); | |
69 | ||
3c0e092e XL |
70 | debug!(?lint_opts); |
71 | ||
f9f354fc XL |
72 | let crate_types = |
73 | if options.proc_macro_crate { vec![CrateType::ProcMacro] } else { vec![CrateType::Rlib] }; | |
e1599b0c | 74 | |
1a4d82fc | 75 | let sessopts = config::Options { |
dc9dc135 | 76 | maybe_sysroot: options.maybe_sysroot.clone(), |
a1dfa0c6 | 77 | search_paths: options.libs.clone(), |
e1599b0c | 78 | crate_types, |
3c0e092e XL |
79 | lint_opts, |
80 | lint_cap: Some(options.lint_cap.unwrap_or(lint::Forbid)), | |
a1dfa0c6 XL |
81 | cg: options.codegen_options.clone(), |
82 | externs: options.externs.clone(), | |
fc512014 | 83 | unstable_features: options.render_options.unstable_features, |
32a655c1 | 84 | actually_rustdoc: true, |
a1dfa0c6 | 85 | edition: options.edition, |
e1599b0c | 86 | target_triple: options.target.clone(), |
fc512014 | 87 | crate_name: options.crate_name.clone(), |
b7449926 | 88 | ..config::Options::default() |
1a4d82fc | 89 | }; |
94b46f34 | 90 | |
e74abb32 | 91 | let mut cfgs = options.cfgs.clone(); |
60c5eb7d | 92 | cfgs.push("doc".to_owned()); |
e74abb32 | 93 | cfgs.push("doctest".to_owned()); |
532ac7d7 XL |
94 | let config = interface::Config { |
95 | opts: sessopts, | |
e74abb32 | 96 | crate_cfg: interface::parse_cfgspecs(cfgs), |
5e7ed085 | 97 | crate_check_cfg: interface::parse_check_cfg(options.check_cfgs.clone()), |
532ac7d7 XL |
98 | input, |
99 | input_path: None, | |
100 | output_file: None, | |
101 | output_dir: None, | |
102 | file_loader: None, | |
103 | diagnostic_output: DiagnosticOutput::Default, | |
f9f354fc | 104 | lint_caps, |
6a06907d | 105 | parse_sess_created: None, |
c295e0f8 | 106 | register_lints: Some(box crate::lint::register_lints), |
60c5eb7d | 107 | override_queries: None, |
1b1a35ee | 108 | make_codegen_backend: None, |
60c5eb7d | 109 | registry: rustc_driver::diagnostics_registry(), |
532ac7d7 XL |
110 | }; |
111 | ||
136023e0 | 112 | let test_args = options.test_args.clone(); |
136023e0 | 113 | let nocapture = options.nocapture; |
cdc7bbd5 XL |
114 | let externs = options.externs.clone(); |
115 | let json_unused_externs = options.json_unused_externs; | |
532ac7d7 | 116 | |
5e7ed085 FG |
117 | let (tests, unused_extern_reports, compiling_test_count) = |
118 | interface::run_compiler(config, |compiler| { | |
119 | compiler.enter(|queries| { | |
120 | let mut global_ctxt = queries.global_ctxt()?.take(); | |
121 | ||
122 | let collector = global_ctxt.enter(|tcx| { | |
123 | let crate_attrs = tcx.hir().attrs(CRATE_HIR_ID); | |
124 | ||
125 | let opts = scrape_test_config(crate_attrs); | |
126 | let enable_per_target_ignores = options.enable_per_target_ignores; | |
127 | let mut collector = Collector::new( | |
128 | tcx.crate_name(LOCAL_CRATE), | |
129 | options, | |
130 | false, | |
131 | opts, | |
132 | Some(compiler.session().parse_sess.clone_source_map()), | |
133 | None, | |
134 | enable_per_target_ignores, | |
135 | ); | |
136 | ||
137 | let mut hir_collector = HirCollector { | |
138 | sess: compiler.session(), | |
139 | collector: &mut collector, | |
140 | map: tcx.hir(), | |
141 | codes: ErrorCodes::from( | |
142 | compiler.session().opts.unstable_features.is_nightly_build(), | |
143 | ), | |
144 | tcx, | |
145 | }; | |
146 | hir_collector.visit_testable( | |
147 | "".to_string(), | |
148 | CRATE_HIR_ID, | |
149 | tcx.hir().span(CRATE_HIR_ID), | |
150 | |this| tcx.hir().walk_toplevel_module(this), | |
151 | ); | |
152 | ||
153 | collector | |
154 | }); | |
155 | if compiler.session().diagnostic().has_errors_or_lint_errors().is_some() { | |
156 | FatalError.raise(); | |
157 | } | |
1a4d82fc | 158 | |
5e7ed085 FG |
159 | let unused_extern_reports = collector.unused_extern_reports.clone(); |
160 | let compiling_test_count = collector.compiling_test_count.load(Ordering::SeqCst); | |
161 | let ret: Result<_, ErrorGuaranteed> = | |
162 | Ok((collector.tests, unused_extern_reports, compiling_test_count)); | |
163 | ret | |
164 | }) | |
165 | })?; | |
1a4d82fc | 166 | |
3c0e092e | 167 | run_tests(test_args, nocapture, tests); |
532ac7d7 | 168 | |
cdc7bbd5 XL |
169 | // Collect and warn about unused externs, but only if we've gotten |
170 | // reports for each doctest | |
04454e1e | 171 | if json_unused_externs.is_enabled() { |
cdc7bbd5 XL |
172 | let unused_extern_reports: Vec<_> = |
173 | std::mem::take(&mut unused_extern_reports.lock().unwrap()); | |
174 | if unused_extern_reports.len() == compiling_test_count { | |
175 | let extern_names = externs.iter().map(|(name, _)| name).collect::<FxHashSet<&String>>(); | |
176 | let mut unused_extern_names = unused_extern_reports | |
177 | .iter() | |
178 | .map(|uexts| uexts.unused_extern_names.iter().collect::<FxHashSet<&String>>()) | |
179 | .fold(extern_names, |uextsa, uextsb| { | |
3c0e092e | 180 | uextsa.intersection(&uextsb).copied().collect::<FxHashSet<&String>>() |
cdc7bbd5 XL |
181 | }) |
182 | .iter() | |
183 | .map(|v| (*v).clone()) | |
184 | .collect::<Vec<String>>(); | |
185 | unused_extern_names.sort(); | |
186 | // Take the most severe lint level | |
187 | let lint_level = unused_extern_reports | |
188 | .iter() | |
189 | .map(|uexts| uexts.lint_level.as_str()) | |
190 | .max_by_key(|v| match *v { | |
191 | "warn" => 1, | |
192 | "deny" => 2, | |
193 | "forbid" => 3, | |
194 | // The allow lint level is not expected, | |
195 | // as if allow is specified, no message | |
196 | // is to be emitted. | |
197 | v => unreachable!("Invalid lint level '{}'", v), | |
198 | }) | |
199 | .unwrap_or("warn") | |
200 | .to_string(); | |
201 | let uext = UnusedExterns { lint_level, unused_extern_names }; | |
202 | let unused_extern_json = serde_json::to_string(&uext).unwrap(); | |
5e7ed085 | 203 | eprintln!("{unused_extern_json}"); |
cdc7bbd5 XL |
204 | } |
205 | } | |
206 | ||
f9f354fc | 207 | Ok(()) |
1a4d82fc JJ |
208 | } |
209 | ||
923072b8 FG |
210 | pub(crate) fn run_tests( |
211 | mut test_args: Vec<String>, | |
212 | nocapture: bool, | |
213 | tests: Vec<test::TestDescAndFn>, | |
214 | ) { | |
136023e0 XL |
215 | test_args.insert(0, "rustdoctest".to_string()); |
216 | if nocapture { | |
217 | test_args.push("--nocapture".to_string()); | |
218 | } | |
3c0e092e | 219 | test::test_main(&test_args, tests, None); |
136023e0 XL |
220 | } |
221 | ||
0731742a | 222 | // Look for `#![doc(test(no_crate_inject))]`, used by crates in the std facade. |
a2a8927a | 223 | fn scrape_test_config(attrs: &[ast::Attribute]) -> GlobalTestOptions { |
74b04a01 | 224 | use rustc_ast_pretty::pprust; |
c34b1796 | 225 | |
a2a8927a | 226 | let mut opts = GlobalTestOptions { no_crate_inject: false, attrs: Vec::new() }; |
9346a6ac | 227 | |
6a06907d | 228 | let test_attrs: Vec<_> = attrs |
dfeec247 | 229 | .iter() |
3dfed10e | 230 | .filter(|a| a.has_name(sym::doc)) |
923072b8 | 231 | .flat_map(|a| a.meta_item_list().unwrap_or_default()) |
3dfed10e | 232 | .filter(|a| a.has_name(sym::test)) |
cc61c64b XL |
233 | .collect(); |
234 | let attrs = test_attrs.iter().flat_map(|a| a.meta_item_list().unwrap_or(&[])); | |
235 | ||
9346a6ac | 236 | for attr in attrs { |
3dfed10e | 237 | if attr.has_name(sym::no_crate_inject) { |
9346a6ac AL |
238 | opts.no_crate_inject = true; |
239 | } | |
3dfed10e | 240 | if attr.has_name(sym::attr) { |
9346a6ac AL |
241 | if let Some(l) = attr.meta_item_list() { |
242 | for item in l { | |
9e0c209e | 243 | opts.attrs.push(pprust::meta_list_item_to_string(item)); |
c34b1796 AL |
244 | } |
245 | } | |
246 | } | |
247 | } | |
248 | ||
c30ab7b3 | 249 | opts |
c34b1796 AL |
250 | } |
251 | ||
dc9dc135 XL |
252 | /// Documentation test failure modes. |
253 | enum TestFailure { | |
254 | /// The test failed to compile. | |
255 | CompileError, | |
256 | /// The test is marked `compile_fail` but compiled successfully. | |
257 | UnexpectedCompilePass, | |
258 | /// The test failed to compile (as expected) but the compiler output did not contain all | |
259 | /// expected error codes. | |
260 | MissingErrorCodes(Vec<String>), | |
261 | /// The test binary was unable to be executed. | |
262 | ExecutionError(io::Error), | |
263 | /// The test binary exited with a non-zero exit code. | |
264 | /// | |
265 | /// This typically means an assertion in the test failed or another form of panic occurred. | |
266 | ExecutionFailure(process::Output), | |
267 | /// The test is marked `should_panic` but the test binary executed successfully. | |
268 | UnexpectedRunPass, | |
269 | } | |
270 | ||
ba9703b0 XL |
271 | enum DirState { |
272 | Temp(tempfile::TempDir), | |
273 | Perm(PathBuf), | |
274 | } | |
275 | ||
276 | impl DirState { | |
277 | fn path(&self) -> &std::path::Path { | |
278 | match self { | |
279 | DirState::Temp(t) => t.path(), | |
280 | DirState::Perm(p) => p.as_path(), | |
281 | } | |
282 | } | |
283 | } | |
284 | ||
cdc7bbd5 XL |
285 | // NOTE: Keep this in sync with the equivalent structs in rustc |
286 | // and cargo. | |
287 | // We could unify this struct the one in rustc but they have different | |
288 | // ownership semantics, so doing so would create wasteful allocations. | |
289 | #[derive(serde::Serialize, serde::Deserialize)] | |
290 | struct UnusedExterns { | |
291 | /// Lint level of the unused_crate_dependencies lint | |
292 | lint_level: String, | |
293 | /// List of unused externs by their names. | |
294 | unused_extern_names: Vec<String>, | |
295 | } | |
296 | ||
dc9dc135 XL |
297 | fn run_test( |
298 | test: &str, | |
136023e0 | 299 | crate_name: &str, |
dc9dc135 | 300 | line: usize, |
a2a8927a XL |
301 | rustdoc_options: RustdocOptions, |
302 | mut lang_string: LangString, | |
dc9dc135 | 303 | no_run: bool, |
e1599b0c XL |
304 | runtool: Option<String>, |
305 | runtool_args: Vec<String>, | |
306 | target: TargetTriple, | |
a2a8927a | 307 | opts: &GlobalTestOptions, |
dc9dc135 | 308 | edition: Edition, |
ba9703b0 XL |
309 | outdir: DirState, |
310 | path: PathBuf, | |
fc512014 | 311 | test_id: &str, |
cdc7bbd5 | 312 | report_unused_externs: impl Fn(UnusedExterns), |
dc9dc135 | 313 | ) -> Result<(), TestFailure> { |
fc512014 | 314 | let (test, line_offset, supports_color) = |
a2a8927a | 315 | make_test(test, Some(crate_name), lang_string.test_harness, opts, edition, Some(test_id)); |
48663c56 | 316 | |
532ac7d7 XL |
317 | let output_file = outdir.path().join("rust_out"); |
318 | ||
a2a8927a | 319 | let rustc_binary = rustdoc_options |
dfeec247 | 320 | .test_builder |
f9f354fc | 321 | .as_deref() |
dfeec247 | 322 | .unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc")); |
e1599b0c XL |
323 | let mut compiler = Command::new(&rustc_binary); |
324 | compiler.arg("--crate-type").arg("bin"); | |
a2a8927a | 325 | for cfg in &rustdoc_options.cfgs { |
e1599b0c XL |
326 | compiler.arg("--cfg").arg(&cfg); |
327 | } | |
5e7ed085 FG |
328 | if !rustdoc_options.check_cfgs.is_empty() { |
329 | compiler.arg("-Z").arg("unstable-options"); | |
330 | for check_cfg in &rustdoc_options.check_cfgs { | |
331 | compiler.arg("--check-cfg").arg(&check_cfg); | |
332 | } | |
333 | } | |
a2a8927a | 334 | if let Some(sysroot) = rustdoc_options.maybe_sysroot { |
e1599b0c XL |
335 | compiler.arg("--sysroot").arg(sysroot); |
336 | } | |
337 | compiler.arg("--edition").arg(&edition.to_string()); | |
338 | compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", path); | |
dfeec247 | 339 | compiler.env("UNSTABLE_RUSTDOC_TEST_LINE", format!("{}", line as isize - line_offset as isize)); |
e1599b0c | 340 | compiler.arg("-o").arg(&output_file); |
a2a8927a | 341 | if lang_string.test_harness { |
e1599b0c XL |
342 | compiler.arg("--test"); |
343 | } | |
04454e1e | 344 | if rustdoc_options.json_unused_externs.is_enabled() && !lang_string.compile_fail { |
cdc7bbd5 XL |
345 | compiler.arg("--error-format=json"); |
346 | compiler.arg("--json").arg("unused-externs"); | |
347 | compiler.arg("-Z").arg("unstable-options"); | |
348 | compiler.arg("-W").arg("unused_crate_dependencies"); | |
349 | } | |
a2a8927a | 350 | for lib_str in &rustdoc_options.lib_strs { |
e1599b0c XL |
351 | compiler.arg("-L").arg(&lib_str); |
352 | } | |
a2a8927a | 353 | for extern_str in &rustdoc_options.extern_strs { |
e1599b0c XL |
354 | compiler.arg("--extern").arg(&extern_str); |
355 | } | |
356 | compiler.arg("-Ccodegen-units=1"); | |
a2a8927a | 357 | for codegen_options_str in &rustdoc_options.codegen_options_strs { |
e1599b0c XL |
358 | compiler.arg("-C").arg(&codegen_options_str); |
359 | } | |
a2a8927a | 360 | for debugging_option_str in &rustdoc_options.debugging_opts_strs { |
e74abb32 XL |
361 | compiler.arg("-Z").arg(&debugging_option_str); |
362 | } | |
a2a8927a | 363 | if no_run && !lang_string.compile_fail && rustdoc_options.persist_doctests.is_none() { |
e1599b0c XL |
364 | compiler.arg("--emit=metadata"); |
365 | } | |
ba9703b0 XL |
366 | compiler.arg("--target").arg(match target { |
367 | TargetTriple::TargetTriple(s) => s, | |
923072b8 FG |
368 | TargetTriple::TargetJson { path_for_rustdoc, .. } => { |
369 | path_for_rustdoc.to_str().expect("target path must be valid unicode").to_string() | |
ba9703b0 XL |
370 | } |
371 | }); | |
a2a8927a | 372 | if let ErrorOutputType::HumanReadable(kind) = rustdoc_options.error_format { |
5869c6ff XL |
373 | let (short, color_config) = kind.unzip(); |
374 | ||
375 | if short { | |
376 | compiler.arg("--error-format").arg("short"); | |
377 | } | |
378 | ||
fc512014 XL |
379 | match color_config { |
380 | ColorConfig::Never => { | |
381 | compiler.arg("--color").arg("never"); | |
382 | } | |
383 | ColorConfig::Always => { | |
384 | compiler.arg("--color").arg("always"); | |
385 | } | |
386 | ColorConfig::Auto => { | |
387 | compiler.arg("--color").arg(if supports_color { "always" } else { "never" }); | |
388 | } | |
389 | } | |
390 | } | |
041b39d2 | 391 | |
e1599b0c XL |
392 | compiler.arg("-"); |
393 | compiler.stdin(Stdio::piped()); | |
394 | compiler.stderr(Stdio::piped()); | |
74d20737 | 395 | |
e1599b0c XL |
396 | let mut child = compiler.spawn().expect("Failed to spawn rustc process"); |
397 | { | |
398 | let stdin = child.stdin.as_mut().expect("Failed to open stdin"); | |
399 | stdin.write_all(test.as_bytes()).expect("could write out test sources"); | |
400 | } | |
401 | let output = child.wait_with_output().expect("Failed to read stdout"); | |
402 | ||
403 | struct Bomb<'a>(&'a str); | |
404 | impl Drop for Bomb<'_> { | |
405 | fn drop(&mut self) { | |
dfeec247 | 406 | eprint!("{}", self.0); |
e1599b0c XL |
407 | } |
408 | } | |
5e7ed085 | 409 | let mut out = str::from_utf8(&output.stderr) |
cdc7bbd5 XL |
410 | .unwrap() |
411 | .lines() | |
412 | .filter(|l| { | |
413 | if let Ok(uext) = serde_json::from_str::<UnusedExterns>(l) { | |
414 | report_unused_externs(uext); | |
415 | false | |
416 | } else { | |
417 | true | |
418 | } | |
419 | }) | |
5e7ed085 FG |
420 | .intersperse_with(|| "\n") |
421 | .collect::<String>(); | |
cdc7bbd5 XL |
422 | |
423 | // Add a \n to the end to properly terminate the last line, | |
424 | // but only if there was output to be printed | |
5e7ed085 FG |
425 | if !out.is_empty() { |
426 | out.push('\n'); | |
cdc7bbd5 XL |
427 | } |
428 | ||
e1599b0c | 429 | let _bomb = Bomb(&out); |
a2a8927a | 430 | match (output.status.success(), lang_string.compile_fail) { |
e1599b0c | 431 | (true, true) => { |
dc9dc135 | 432 | return Err(TestFailure::UnexpectedCompilePass); |
74d20737 | 433 | } |
e1599b0c XL |
434 | (true, false) => {} |
435 | (false, true) => { | |
a2a8927a | 436 | if !lang_string.error_codes.is_empty() { |
fc512014 XL |
437 | // We used to check if the output contained "error[{}]: " but since we added the |
438 | // colored output, we can't anymore because of the color escape characters before | |
439 | // the ":". | |
5e7ed085 | 440 | lang_string.error_codes.retain(|err| !out.contains(&format!("error[{err}]"))); |
dc9dc135 | 441 | |
a2a8927a XL |
442 | if !lang_string.error_codes.is_empty() { |
443 | return Err(TestFailure::MissingErrorCodes(lang_string.error_codes)); | |
dc9dc135 | 444 | } |
3157f602 XL |
445 | } |
446 | } | |
e1599b0c | 447 | (false, false) => { |
dc9dc135 | 448 | return Err(TestFailure::CompileError); |
041b39d2 | 449 | } |
74d20737 | 450 | } |
3157f602 | 451 | |
dc9dc135 XL |
452 | if no_run { |
453 | return Ok(()); | |
74d20737 | 454 | } |
1a4d82fc | 455 | |
1a4d82fc | 456 | // Run the code! |
e1599b0c XL |
457 | let mut cmd; |
458 | ||
459 | if let Some(tool) = runtool { | |
460 | cmd = Command::new(tool); | |
e1599b0c | 461 | cmd.args(runtool_args); |
ba9703b0 | 462 | cmd.arg(output_file); |
e1599b0c XL |
463 | } else { |
464 | cmd = Command::new(output_file); | |
465 | } | |
a2a8927a | 466 | if let Some(run_directory) = rustdoc_options.test_run_directory { |
5869c6ff XL |
467 | cmd.current_dir(run_directory); |
468 | } | |
532ac7d7 | 469 | |
a2a8927a | 470 | let result = if rustdoc_options.nocapture { |
136023e0 XL |
471 | cmd.status().map(|status| process::Output { |
472 | status, | |
473 | stdout: Vec::new(), | |
474 | stderr: Vec::new(), | |
475 | }) | |
476 | } else { | |
477 | cmd.output() | |
478 | }; | |
479 | match result { | |
dc9dc135 | 480 | Err(e) => return Err(TestFailure::ExecutionError(e)), |
1a4d82fc | 481 | Ok(out) => { |
a2a8927a | 482 | if lang_string.should_panic && out.status.success() { |
dc9dc135 | 483 | return Err(TestFailure::UnexpectedRunPass); |
a2a8927a | 484 | } else if !lang_string.should_panic && !out.status.success() { |
dc9dc135 | 485 | return Err(TestFailure::ExecutionFailure(out)); |
1a4d82fc JJ |
486 | } |
487 | } | |
488 | } | |
dc9dc135 XL |
489 | |
490 | Ok(()) | |
1a4d82fc JJ |
491 | } |
492 | ||
48663c56 | 493 | /// Transforms a test into code that can be compiled into a Rust binary, and returns the number of |
fc512014 | 494 | /// lines before the test code begins as well as if the output stream supports colors or not. |
923072b8 | 495 | pub(crate) fn make_test( |
dfeec247 | 496 | s: &str, |
136023e0 | 497 | crate_name: Option<&str>, |
dfeec247 | 498 | dont_insert_main: bool, |
a2a8927a | 499 | opts: &GlobalTestOptions, |
dfeec247 | 500 | edition: Edition, |
fc512014 XL |
501 | test_id: Option<&str>, |
502 | ) -> (String, usize, bool) { | |
5e7ed085 | 503 | let (crate_attrs, everything_else, crates) = partition_source(s, edition); |
0531ce1d | 504 | let everything_else = everything_else.trim(); |
2c00a5a8 | 505 | let mut line_offset = 0; |
1a4d82fc | 506 | let mut prog = String::new(); |
fc512014 | 507 | let mut supports_color = false; |
c34b1796 | 508 | |
3c0e092e | 509 | if opts.attrs.is_empty() { |
abe05a73 XL |
510 | // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some |
511 | // lints that are commonly triggered in doctests. The crate-level test attributes are | |
512 | // commonly used to make tests fail in case they trigger warnings, so having this there in | |
513 | // that case may cause some tests to pass when they shouldn't have. | |
514 | prog.push_str("#![allow(unused)]\n"); | |
2c00a5a8 | 515 | line_offset += 1; |
abe05a73 | 516 | } |
c34b1796 | 517 | |
abe05a73 | 518 | // Next, any attributes that came from the crate root via #![doc(test(attr(...)))]. |
9346a6ac | 519 | for attr in &opts.attrs { |
5e7ed085 | 520 | prog.push_str(&format!("#![{attr}]\n")); |
2c00a5a8 | 521 | line_offset += 1; |
1a4d82fc JJ |
522 | } |
523 | ||
abe05a73 XL |
524 | // Now push any outer attributes from the example, assuming they |
525 | // are intended to be crate attributes. | |
526 | prog.push_str(&crate_attrs); | |
0731742a | 527 | prog.push_str(&crates); |
abe05a73 | 528 | |
74b04a01 | 529 | // Uses librustc_ast to parse the doctest and find if there's a main fn and the extern |
a1dfa0c6 | 530 | // crate already is included. |
dfeec247 | 531 | let result = rustc_driver::catch_fatal_errors(|| { |
136023e0 | 532 | rustc_span::create_session_if_not_set_then(edition, |_| { |
fc512014 | 533 | use rustc_errors::emitter::{Emitter, EmitterWriter}; |
dfeec247 | 534 | use rustc_errors::Handler; |
5869c6ff | 535 | use rustc_parse::parser::ForceCollect; |
dfeec247 | 536 | use rustc_span::source_map::FilePathMapping; |
dfeec247 XL |
537 | |
538 | let filename = FileName::anon_source_code(s); | |
74b04a01 | 539 | let source = crates + everything_else; |
dfeec247 XL |
540 | |
541 | // Any errors in parsing should also appear when the doctest is compiled for real, so just | |
74b04a01 XL |
542 | // send all the errors that librustc_ast emits directly into a `Sink` instead of stderr. |
543 | let sm = Lrc::new(SourceMap::new(FilePathMapping::empty())); | |
04454e1e FG |
544 | let fallback_bundle = |
545 | rustc_errors::fallback_fluent_bundle(rustc_errors::DEFAULT_LOCALE_RESOURCES, false); | |
546 | supports_color = EmitterWriter::stderr( | |
547 | ColorConfig::Auto, | |
548 | None, | |
549 | None, | |
550 | fallback_bundle.clone(), | |
551 | false, | |
552 | false, | |
553 | Some(80), | |
554 | false, | |
555 | ) | |
556 | .supports_color(); | |
557 | ||
558 | let emitter = EmitterWriter::new( | |
559 | box io::sink(), | |
560 | None, | |
561 | None, | |
562 | fallback_bundle, | |
563 | false, | |
564 | false, | |
565 | false, | |
566 | None, | |
567 | false, | |
568 | ); | |
fc512014 | 569 | |
dfeec247 | 570 | // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser |
c295e0f8 | 571 | let handler = Handler::with_emitter(false, None, box emitter); |
74b04a01 | 572 | let sess = ParseSess::with_span_handler(handler, sm); |
dfeec247 XL |
573 | |
574 | let mut found_main = false; | |
136023e0 | 575 | let mut found_extern_crate = crate_name.is_none(); |
dfeec247 XL |
576 | let mut found_macro = false; |
577 | ||
578 | let mut parser = match maybe_new_parser_from_source_str(&sess, filename, source) { | |
579 | Ok(p) => p, | |
580 | Err(errs) => { | |
5e7ed085 | 581 | drop(errs); |
dfeec247 XL |
582 | return (found_main, found_extern_crate, found_macro); |
583 | } | |
584 | }; | |
a1dfa0c6 | 585 | |
dfeec247 | 586 | loop { |
5869c6ff | 587 | match parser.parse_item(ForceCollect::No) { |
dfeec247 XL |
588 | Ok(Some(item)) => { |
589 | if !found_main { | |
590 | if let ast::ItemKind::Fn(..) = item.kind { | |
591 | if item.ident.name == sym::main { | |
592 | found_main = true; | |
593 | } | |
a1dfa0c6 XL |
594 | } |
595 | } | |
a1dfa0c6 | 596 | |
dfeec247 XL |
597 | if !found_extern_crate { |
598 | if let ast::ItemKind::ExternCrate(original) = item.kind { | |
136023e0 | 599 | // This code will never be reached if `crate_name` is none because |
dfeec247 | 600 | // `found_extern_crate` is initialized to `true` if it is none. |
136023e0 | 601 | let crate_name = crate_name.unwrap(); |
a1dfa0c6 | 602 | |
dfeec247 | 603 | match original { |
136023e0 XL |
604 | Some(name) => found_extern_crate = name.as_str() == crate_name, |
605 | None => found_extern_crate = item.ident.as_str() == crate_name, | |
dfeec247 | 606 | } |
a1dfa0c6 XL |
607 | } |
608 | } | |
a1dfa0c6 | 609 | |
dfeec247 | 610 | if !found_macro { |
ba9703b0 | 611 | if let ast::ItemKind::MacCall(..) = item.kind { |
dfeec247 XL |
612 | found_macro = true; |
613 | } | |
69743fb6 | 614 | } |
69743fb6 | 615 | |
dfeec247 XL |
616 | if found_main && found_extern_crate { |
617 | break; | |
618 | } | |
619 | } | |
620 | Ok(None) => break, | |
5e7ed085 | 621 | Err(e) => { |
dfeec247 | 622 | e.cancel(); |
a1dfa0c6 XL |
623 | break; |
624 | } | |
625 | } | |
3c0e092e XL |
626 | |
627 | // The supplied slice is only used for diagnostics, | |
628 | // which are swallowed here anyway. | |
629 | parser.maybe_consume_incorrect_semicolon(&[]); | |
a1dfa0c6 | 630 | } |
a1dfa0c6 | 631 | |
5869c6ff XL |
632 | // Reset errors so that they won't be reported as compiler bugs when dropping the |
633 | // handler. Any errors in the tests will be reported when the test file is compiled, | |
634 | // Note that we still need to cancel the errors above otherwise `DiagnosticBuilder` | |
635 | // will panic on drop. | |
636 | sess.span_diagnostic.reset_err_count(); | |
637 | ||
dfeec247 XL |
638 | (found_main, found_extern_crate, found_macro) |
639 | }) | |
a1dfa0c6 | 640 | }); |
5e7ed085 FG |
641 | let Ok((already_has_main, already_has_extern_crate, found_macro)) = result |
642 | else { | |
643 | // If the parser panicked due to a fatal error, pass the test code through unchanged. | |
644 | // The error will be reported during compilation. | |
645 | return (s.to_owned(), 0, false); | |
dfeec247 | 646 | }; |
a1dfa0c6 | 647 | |
69743fb6 XL |
648 | // If a doctest's `fn main` is being masked by a wrapper macro, the parsing loop above won't |
649 | // see it. In that case, run the old text-based scan to see if they at least have a main | |
650 | // function written inside a macro invocation. See | |
651 | // https://github.com/rust-lang/rust/issues/56898 | |
652 | let already_has_main = if found_macro && !already_has_main { | |
653 | s.lines() | |
654 | .map(|line| { | |
655 | let comment = line.find("//"); | |
dfeec247 | 656 | if let Some(comment_begins) = comment { &line[0..comment_begins] } else { line } |
69743fb6 XL |
657 | }) |
658 | .any(|code| code.contains("fn main")) | |
659 | } else { | |
660 | already_has_main | |
661 | }; | |
662 | ||
1a4d82fc JJ |
663 | // Don't inject `extern crate std` because it's already injected by the |
664 | // compiler. | |
136023e0 XL |
665 | if !already_has_extern_crate && !opts.no_crate_inject && crate_name != Some("std") { |
666 | if let Some(crate_name) = crate_name { | |
6a06907d XL |
667 | // Don't inject `extern crate` if the crate is never used. |
668 | // NOTE: this is terribly inaccurate because it doesn't actually | |
669 | // parse the source, but only has false positives, not false | |
670 | // negatives. | |
136023e0 | 671 | if s.contains(crate_name) { |
5e7ed085 | 672 | prog.push_str(&format!("extern crate r#{crate_name};\n")); |
2c00a5a8 | 673 | line_offset += 1; |
1a4d82fc | 674 | } |
1a4d82fc JJ |
675 | } |
676 | } | |
ea8adc8c | 677 | |
9fa01778 XL |
678 | // FIXME: This code cannot yet handle no_std test cases yet |
679 | if dont_insert_main || already_has_main || prog.contains("![no_std]") { | |
0531ce1d | 680 | prog.push_str(everything_else); |
1a4d82fc | 681 | } else { |
9fa01778 | 682 | let returns_result = everything_else.trim_end().ends_with("(())"); |
fc512014 | 683 | // Give each doctest main function a unique name. |
5099ac24 | 684 | // This is for example needed for the tooling around `-C instrument-coverage`. |
fc512014 | 685 | let inner_fn_name = if let Some(test_id) = test_id { |
5e7ed085 | 686 | format!("_doctest_main_{test_id}") |
fc512014 XL |
687 | } else { |
688 | "_inner".into() | |
689 | }; | |
690 | let inner_attr = if test_id.is_some() { "#[allow(non_snake_case)] " } else { "" }; | |
9fa01778 | 691 | let (main_pre, main_post) = if returns_result { |
dfeec247 | 692 | ( |
fc512014 | 693 | format!( |
5e7ed085 | 694 | "fn main() {{ {inner_attr}fn {inner_fn_name}() -> Result<(), impl core::fmt::Debug> {{\n", |
fc512014 | 695 | ), |
5e7ed085 | 696 | format!("\n}} {inner_fn_name}().unwrap() }}"), |
fc512014 XL |
697 | ) |
698 | } else if test_id.is_some() { | |
699 | ( | |
5e7ed085 FG |
700 | format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",), |
701 | format!("\n}} {inner_fn_name}() }}"), | |
dfeec247 | 702 | ) |
9fa01778 | 703 | } else { |
fc512014 | 704 | ("fn main() {\n".into(), "\n}".into()) |
9fa01778 | 705 | }; |
fc512014 XL |
706 | // Note on newlines: We insert a line/newline *before*, and *after* |
707 | // the doctest and adjust the `line_offset` accordingly. | |
5099ac24 | 708 | // In the case of `-C instrument-coverage`, this means that the generated |
fc512014 XL |
709 | // inner `main` function spans from the doctest opening codeblock to the |
710 | // closing one. For example | |
711 | // /// ``` <- start of the inner main | |
712 | // /// <- code under doctest | |
713 | // /// ``` <- end of the inner main | |
2c00a5a8 | 714 | line_offset += 1; |
fc512014 XL |
715 | |
716 | prog.extend([&main_pre, everything_else, &main_post].iter().cloned()); | |
1a4d82fc JJ |
717 | } |
718 | ||
5e7ed085 | 719 | debug!("final doctest:\n{prog}"); |
c34b1796 | 720 | |
fc512014 | 721 | (prog, line_offset, supports_color) |
1a4d82fc JJ |
722 | } |
723 | ||
5e7ed085 FG |
724 | fn check_if_attr_is_complete(source: &str, edition: Edition) -> bool { |
725 | if source.is_empty() { | |
726 | // Empty content so nothing to check in here... | |
727 | return true; | |
728 | } | |
729 | rustc_span::create_session_if_not_set_then(edition, |_| { | |
730 | let filename = FileName::anon_source_code(source); | |
731 | let sess = ParseSess::with_silent_emitter(None); | |
732 | let mut parser = match maybe_new_parser_from_source_str(&sess, filename, source.to_owned()) | |
733 | { | |
734 | Ok(p) => p, | |
735 | Err(_) => { | |
736 | debug!("Cannot build a parser to check mod attr so skipping..."); | |
737 | return true; | |
738 | } | |
739 | }; | |
740 | // If a parsing error happened, it's very likely that the attribute is incomplete. | |
923072b8 | 741 | if parser.parse_attribute(InnerAttrPolicy::Permitted).is_err() { |
5e7ed085 FG |
742 | return false; |
743 | } | |
744 | // We now check if there is an unclosed delimiter for the attribute. To do so, we look at | |
745 | // the `unclosed_delims` and see if the opening square bracket was closed. | |
746 | parser | |
747 | .unclosed_delims() | |
748 | .get(0) | |
749 | .map(|unclosed| { | |
750 | unclosed.unclosed_span.map(|s| s.lo()).unwrap_or(BytePos(0)) != BytePos(2) | |
751 | }) | |
752 | .unwrap_or(true) | |
753 | }) | |
754 | } | |
755 | ||
756 | fn partition_source(s: &str, edition: Edition) -> (String, String, String) { | |
0731742a XL |
757 | #[derive(Copy, Clone, PartialEq)] |
758 | enum PartitionState { | |
759 | Attrs, | |
760 | Crates, | |
761 | Other, | |
762 | } | |
763 | let mut state = PartitionState::Attrs; | |
c34b1796 | 764 | let mut before = String::new(); |
a1dfa0c6 | 765 | let mut crates = String::new(); |
c34b1796 AL |
766 | let mut after = String::new(); |
767 | ||
5e7ed085 FG |
768 | let mut mod_attr_pending = String::new(); |
769 | ||
c34b1796 AL |
770 | for line in s.lines() { |
771 | let trimline = line.trim(); | |
0731742a XL |
772 | |
773 | // FIXME(misdreavus): if a doc comment is placed on an extern crate statement, it will be | |
774 | // shunted into "everything else" | |
775 | match state { | |
776 | PartitionState::Attrs => { | |
5e7ed085 FG |
777 | state = if trimline.starts_with("#![") { |
778 | if !check_if_attr_is_complete(line, edition) { | |
779 | mod_attr_pending = line.to_owned(); | |
780 | } else { | |
781 | mod_attr_pending.clear(); | |
782 | } | |
783 | PartitionState::Attrs | |
784 | } else if trimline.chars().all(|c| c.is_whitespace()) | |
dfeec247 | 785 | || (trimline.starts_with("//") && !trimline.starts_with("///")) |
0731742a XL |
786 | { |
787 | PartitionState::Attrs | |
dfeec247 XL |
788 | } else if trimline.starts_with("extern crate") |
789 | || trimline.starts_with("#[macro_use] extern crate") | |
0731742a XL |
790 | { |
791 | PartitionState::Crates | |
792 | } else { | |
5e7ed085 FG |
793 | // First we check if the previous attribute was "complete"... |
794 | if !mod_attr_pending.is_empty() { | |
795 | // If not, then we append the new line into the pending attribute to check | |
796 | // if this time it's complete... | |
797 | mod_attr_pending.push_str(line); | |
923072b8 FG |
798 | if !trimline.is_empty() |
799 | && check_if_attr_is_complete(&mod_attr_pending, edition) | |
800 | { | |
5e7ed085 FG |
801 | // If it's complete, then we can clear the pending content. |
802 | mod_attr_pending.clear(); | |
803 | } | |
804 | // In any case, this is considered as `PartitionState::Attrs` so it's | |
805 | // prepended before rustdoc's inserts. | |
806 | PartitionState::Attrs | |
807 | } else { | |
808 | PartitionState::Other | |
809 | } | |
0731742a XL |
810 | }; |
811 | } | |
812 | PartitionState::Crates => { | |
dfeec247 XL |
813 | state = if trimline.starts_with("extern crate") |
814 | || trimline.starts_with("#[macro_use] extern crate") | |
815 | || trimline.chars().all(|c| c.is_whitespace()) | |
816 | || (trimline.starts_with("//") && !trimline.starts_with("///")) | |
0731742a XL |
817 | { |
818 | PartitionState::Crates | |
819 | } else { | |
820 | PartitionState::Other | |
821 | }; | |
822 | } | |
823 | PartitionState::Other => {} | |
824 | } | |
825 | ||
826 | match state { | |
827 | PartitionState::Attrs => { | |
828 | before.push_str(line); | |
5869c6ff | 829 | before.push('\n'); |
0731742a XL |
830 | } |
831 | PartitionState::Crates => { | |
a1dfa0c6 | 832 | crates.push_str(line); |
5869c6ff | 833 | crates.push('\n'); |
a1dfa0c6 | 834 | } |
0731742a XL |
835 | PartitionState::Other => { |
836 | after.push_str(line); | |
5869c6ff | 837 | after.push('\n'); |
0731742a | 838 | } |
c34b1796 AL |
839 | } |
840 | } | |
841 | ||
5e7ed085 FG |
842 | debug!("before:\n{before}"); |
843 | debug!("crates:\n{crates}"); | |
844 | debug!("after:\n{after}"); | |
0731742a | 845 | |
a1dfa0c6 | 846 | (before, after, crates) |
c34b1796 AL |
847 | } |
848 | ||
923072b8 | 849 | pub(crate) trait Tester { |
0bf4aa26 XL |
850 | fn add_test(&mut self, test: String, config: LangString, line: usize); |
851 | fn get_line(&self) -> usize { | |
852 | 0 | |
853 | } | |
854 | fn register_header(&mut self, _name: &str, _level: u32) {} | |
855 | } | |
856 | ||
923072b8 FG |
857 | pub(crate) struct Collector { |
858 | pub(crate) tests: Vec<test::TestDescAndFn>, | |
abe05a73 XL |
859 | |
860 | // The name of the test displayed to the user, separated by `::`. | |
861 | // | |
862 | // In tests from Rust source, this is the path to the item | |
0731742a | 863 | // e.g., `["std", "vec", "Vec", "push"]`. |
abe05a73 XL |
864 | // |
865 | // In tests from a markdown file, this is the titles of all headers (h1~h6) | |
0731742a | 866 | // of the sections that contain the code block, e.g., if the markdown file is |
abe05a73 XL |
867 | // written as: |
868 | // | |
869 | // ``````markdown | |
870 | // # Title | |
871 | // | |
872 | // ## Subtitle | |
873 | // | |
874 | // ```rust | |
875 | // assert!(true); | |
876 | // ``` | |
877 | // `````` | |
878 | // | |
879 | // the `names` vector of that test will be `["Title", "Subtitle"]`. | |
1a4d82fc | 880 | names: Vec<String>, |
abe05a73 | 881 | |
a2a8927a | 882 | rustdoc_options: RustdocOptions, |
1a4d82fc | 883 | use_headers: bool, |
e1599b0c | 884 | enable_per_target_ignores: bool, |
136023e0 | 885 | crate_name: Symbol, |
a2a8927a | 886 | opts: GlobalTestOptions, |
8bb4bdeb | 887 | position: Span, |
b7449926 | 888 | source_map: Option<Lrc<SourceMap>>, |
ff7c6d11 | 889 | filename: Option<PathBuf>, |
5869c6ff | 890 | visited_tests: FxHashMap<(String, usize), usize>, |
cdc7bbd5 XL |
891 | unused_extern_reports: Arc<Mutex<Vec<UnusedExterns>>>, |
892 | compiling_test_count: AtomicUsize, | |
1a4d82fc JJ |
893 | } |
894 | ||
895 | impl Collector { | |
923072b8 | 896 | pub(crate) fn new( |
136023e0 | 897 | crate_name: Symbol, |
a2a8927a | 898 | rustdoc_options: RustdocOptions, |
dfeec247 | 899 | use_headers: bool, |
a2a8927a | 900 | opts: GlobalTestOptions, |
dfeec247 XL |
901 | source_map: Option<Lrc<SourceMap>>, |
902 | filename: Option<PathBuf>, | |
903 | enable_per_target_ignores: bool, | |
904 | ) -> Collector { | |
1a4d82fc JJ |
905 | Collector { |
906 | tests: Vec::new(), | |
907 | names: Vec::new(), | |
a2a8927a | 908 | rustdoc_options, |
3b2f2976 | 909 | use_headers, |
e1599b0c | 910 | enable_per_target_ignores, |
136023e0 | 911 | crate_name, |
3b2f2976 | 912 | opts, |
8bb4bdeb | 913 | position: DUMMY_SP, |
b7449926 | 914 | source_map, |
3b2f2976 | 915 | filename, |
5869c6ff | 916 | visited_tests: FxHashMap::default(), |
cdc7bbd5 XL |
917 | unused_extern_reports: Default::default(), |
918 | compiling_test_count: AtomicUsize::new(0), | |
1a4d82fc JJ |
919 | } |
920 | } | |
921 | ||
ff7c6d11 | 922 | fn generate_name(&self, line: usize, filename: &FileName) -> String { |
f035d41b | 923 | let mut item_path = self.names.join("::"); |
c295e0f8 | 924 | item_path.retain(|c| c != ' '); |
f035d41b XL |
925 | if !item_path.is_empty() { |
926 | item_path.push(' '); | |
927 | } | |
17df50a5 | 928 | format!("{} - {}(line {})", filename.prefer_local(), item_path, line) |
cc61c64b XL |
929 | } |
930 | ||
923072b8 | 931 | pub(crate) fn set_position(&mut self, position: Span) { |
0bf4aa26 XL |
932 | self.position = position; |
933 | } | |
934 | ||
935 | fn get_filename(&self) -> FileName { | |
936 | if let Some(ref source_map) = self.source_map { | |
937 | let filename = source_map.span_to_filename(self.position); | |
938 | if let FileName::Real(ref filename) = filename { | |
939 | if let Ok(cur_dir) = env::current_dir() { | |
17df50a5 XL |
940 | if let Some(local_path) = filename.local_path() { |
941 | if let Ok(path) = local_path.strip_prefix(&cur_dir) { | |
942 | return path.to_owned().into(); | |
943 | } | |
0bf4aa26 XL |
944 | } |
945 | } | |
946 | } | |
947 | filename | |
948 | } else if let Some(ref filename) = self.filename { | |
949 | filename.clone().into() | |
950 | } else { | |
951 | FileName::Custom("input".to_owned()) | |
952 | } | |
953 | } | |
954 | } | |
955 | ||
956 | impl Tester for Collector { | |
957 | fn add_test(&mut self, test: String, config: LangString, line: usize) { | |
b7449926 | 958 | let filename = self.get_filename(); |
cc61c64b | 959 | let name = self.generate_name(line, &filename); |
136023e0 | 960 | let crate_name = self.crate_name.to_string(); |
9346a6ac | 961 | let opts = self.opts.clone(); |
a2a8927a XL |
962 | let edition = config.edition.unwrap_or(self.rustdoc_options.edition); |
963 | let rustdoc_options = self.rustdoc_options.clone(); | |
964 | let runtool = self.rustdoc_options.runtool.clone(); | |
965 | let runtool_args = self.rustdoc_options.runtool_args.clone(); | |
966 | let target = self.rustdoc_options.target.clone(); | |
e1599b0c | 967 | let target_str = target.to_string(); |
cdc7bbd5 | 968 | let unused_externs = self.unused_extern_reports.clone(); |
a2a8927a | 969 | let no_run = config.no_run || rustdoc_options.no_run; |
cdc7bbd5 XL |
970 | if !config.compile_fail { |
971 | self.compiling_test_count.fetch_add(1, Ordering::SeqCst); | |
972 | } | |
9fa01778 | 973 | |
ba9703b0 | 974 | let path = match &filename { |
17df50a5 XL |
975 | FileName::Real(path) => { |
976 | if let Some(local_path) = path.local_path() { | |
977 | local_path.to_path_buf() | |
978 | } else { | |
979 | // Somehow we got the filename from the metadata of another crate, should never happen | |
980 | unreachable!("doctest from a different crate"); | |
981 | } | |
982 | } | |
ba9703b0 XL |
983 | _ => PathBuf::from(r"doctest.rs"), |
984 | }; | |
985 | ||
fc512014 XL |
986 | // For example `module/file.rs` would become `module_file_rs` |
987 | let file = filename | |
17df50a5 XL |
988 | .prefer_local() |
989 | .to_string_lossy() | |
fc512014 XL |
990 | .chars() |
991 | .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) | |
992 | .collect::<String>(); | |
993 | let test_id = format!( | |
994 | "{file}_{line}_{number}", | |
995 | file = file, | |
996 | line = line, | |
997 | number = { | |
998 | // Increases the current test number, if this file already | |
999 | // exists or it creates a new entry with a test number of 0. | |
1000 | self.visited_tests.entry((file.clone(), line)).and_modify(|v| *v += 1).or_insert(0) | |
1001 | }, | |
1002 | ); | |
a2a8927a | 1003 | let outdir = if let Some(mut path) = rustdoc_options.persist_doctests.clone() { |
fc512014 | 1004 | path.push(&test_id); |
ba9703b0 XL |
1005 | |
1006 | std::fs::create_dir_all(&path) | |
1007 | .expect("Couldn't create directory for doctest executables"); | |
1008 | ||
1009 | DirState::Perm(path) | |
1010 | } else { | |
1011 | DirState::Temp( | |
1012 | TempFileBuilder::new() | |
1013 | .prefix("rustdoctest") | |
1014 | .tempdir() | |
1015 | .expect("rustdoc needs a tempdir"), | |
1016 | ) | |
1017 | }; | |
1018 | ||
5e7ed085 | 1019 | debug!("creating test {name}: {test}"); |
136023e0 XL |
1020 | self.tests.push(test::TestDescAndFn { |
1021 | desc: test::TestDesc { | |
1022 | name: test::DynTestName(name), | |
e1599b0c XL |
1023 | ignore: match config.ignore { |
1024 | Ignore::All => true, | |
1025 | Ignore::None => false, | |
dfeec247 | 1026 | Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), |
e1599b0c | 1027 | }, |
5e7ed085 | 1028 | ignore_message: None, |
9346a6ac | 1029 | // compiler failures are test failures |
136023e0 | 1030 | should_panic: test::ShouldPanic::No, |
17df50a5 | 1031 | compile_fail: config.compile_fail, |
17df50a5 | 1032 | no_run, |
136023e0 | 1033 | test_type: test::TestType::DocTest, |
1a4d82fc | 1034 | }, |
c295e0f8 | 1035 | testfn: test::DynTestFn(box move || { |
cdc7bbd5 XL |
1036 | let report_unused_externs = |uext| { |
1037 | unused_externs.lock().unwrap().push(uext); | |
1038 | }; | |
dc9dc135 | 1039 | let res = run_test( |
532ac7d7 | 1040 | &test, |
136023e0 | 1041 | &crate_name, |
532ac7d7 | 1042 | line, |
a2a8927a XL |
1043 | rustdoc_options, |
1044 | config, | |
17df50a5 | 1045 | no_run, |
e1599b0c XL |
1046 | runtool, |
1047 | runtool_args, | |
1048 | target, | |
532ac7d7 | 1049 | &opts, |
532ac7d7 | 1050 | edition, |
ba9703b0 XL |
1051 | outdir, |
1052 | path, | |
fc512014 | 1053 | &test_id, |
cdc7bbd5 | 1054 | report_unused_externs, |
dc9dc135 XL |
1055 | ); |
1056 | ||
1057 | if let Err(err) = res { | |
1058 | match err { | |
1059 | TestFailure::CompileError => { | |
1060 | eprint!("Couldn't compile the test."); | |
1061 | } | |
1062 | TestFailure::UnexpectedCompilePass => { | |
1063 | eprint!("Test compiled successfully, but it's marked `compile_fail`."); | |
1064 | } | |
1065 | TestFailure::UnexpectedRunPass => { | |
1066 | eprint!("Test executable succeeded, but it's marked `should_panic`."); | |
1067 | } | |
1068 | TestFailure::MissingErrorCodes(codes) => { | |
1069 | eprint!("Some expected error codes were not found: {:?}", codes); | |
1070 | } | |
1071 | TestFailure::ExecutionError(err) => { | |
5e7ed085 | 1072 | eprint!("Couldn't run the test: {err}"); |
dc9dc135 XL |
1073 | if err.kind() == io::ErrorKind::PermissionDenied { |
1074 | eprint!(" - maybe your tempdir is mounted with noexec?"); | |
1075 | } | |
1076 | } | |
1077 | TestFailure::ExecutionFailure(out) => { | |
04454e1e | 1078 | eprintln!("Test executable failed ({reason}).", reason = out.status); |
dc9dc135 XL |
1079 | |
1080 | // FIXME(#12309): An unfortunate side-effect of capturing the test | |
1081 | // executable's output is that the relative ordering between the test's | |
1082 | // stdout and stderr is lost. However, this is better than the | |
1083 | // alternative: if the test executable inherited the parent's I/O | |
1084 | // handles the output wouldn't be captured at all, even on success. | |
1085 | // | |
1086 | // The ordering could be preserved if the test process' stderr was | |
1087 | // redirected to stdout, but that functionality does not exist in the | |
1088 | // standard library, so it may not be portable enough. | |
1089 | let stdout = str::from_utf8(&out.stdout).unwrap_or_default(); | |
1090 | let stderr = str::from_utf8(&out.stderr).unwrap_or_default(); | |
1091 | ||
1092 | if !stdout.is_empty() || !stderr.is_empty() { | |
1093 | eprintln!(); | |
1094 | ||
1095 | if !stdout.is_empty() { | |
5e7ed085 | 1096 | eprintln!("stdout:\n{stdout}"); |
dc9dc135 XL |
1097 | } |
1098 | ||
1099 | if !stderr.is_empty() { | |
5e7ed085 | 1100 | eprintln!("stderr:\n{stderr}"); |
dc9dc135 XL |
1101 | } |
1102 | } | |
1103 | } | |
1104 | } | |
1105 | ||
c295e0f8 | 1106 | panic::resume_unwind(box ()); |
dc9dc135 | 1107 | } |
c295e0f8 | 1108 | }), |
1a4d82fc JJ |
1109 | }); |
1110 | } | |
1111 | ||
0bf4aa26 | 1112 | fn get_line(&self) -> usize { |
b7449926 | 1113 | if let Some(ref source_map) = self.source_map { |
ea8adc8c | 1114 | let line = self.position.lo().to_usize(); |
b7449926 | 1115 | let line = source_map.lookup_char_pos(BytePos(line as u32)).line; |
8bb4bdeb XL |
1116 | if line > 0 { line - 1 } else { line } |
1117 | } else { | |
1118 | 0 | |
1119 | } | |
1120 | } | |
1121 | ||
0bf4aa26 | 1122 | fn register_header(&mut self, name: &str, level: u32) { |
abe05a73 | 1123 | if self.use_headers { |
0731742a | 1124 | // We use these headings as test names, so it's good if |
1a4d82fc | 1125 | // they're valid identifiers. |
dfeec247 XL |
1126 | let name = name |
1127 | .chars() | |
1128 | .enumerate() | |
1129 | .map(|(i, c)| { | |
1130 | if (i == 0 && rustc_lexer::is_id_start(c)) | |
1131 | || (i != 0 && rustc_lexer::is_id_continue(c)) | |
1132 | { | |
1a4d82fc JJ |
1133 | c |
1134 | } else { | |
1135 | '_' | |
1136 | } | |
dfeec247 XL |
1137 | }) |
1138 | .collect::<String>(); | |
1a4d82fc | 1139 | |
abe05a73 XL |
1140 | // Here we try to efficiently assemble the header titles into the |
1141 | // test name in the form of `h1::h2::h3::h4::h5::h6`. | |
1142 | // | |
0731742a | 1143 | // Suppose that originally `self.names` contains `[h1, h2, h3]`... |
abe05a73 XL |
1144 | let level = level as usize; |
1145 | if level <= self.names.len() { | |
1146 | // ... Consider `level == 2`. All headers in the lower levels | |
1147 | // are irrelevant in this new level. So we should reset | |
1148 | // `self.names` to contain headers until <h2>, and replace that | |
1149 | // slot with the new name: `[h1, name]`. | |
1150 | self.names.truncate(level); | |
1151 | self.names[level - 1] = name; | |
1152 | } else { | |
1153 | // ... On the other hand, consider `level == 5`. This means we | |
1154 | // need to extend `self.names` to contain five headers. We fill | |
1155 | // in the missing level (<h4>) with `_`. Thus `self.names` will | |
1156 | // become `[h1, h2, h3, "_", name]`. | |
1157 | if level - 1 > self.names.len() { | |
1158 | self.names.resize(level - 1, "_".to_owned()); | |
1159 | } | |
1160 | self.names.push(name); | |
1161 | } | |
1a4d82fc JJ |
1162 | } |
1163 | } | |
1164 | } | |
1165 | ||
f9f354fc | 1166 | struct HirCollector<'a, 'hir, 'tcx> { |
ba9703b0 | 1167 | sess: &'a Session, |
476ff2be | 1168 | collector: &'a mut Collector, |
ba9703b0 | 1169 | map: Map<'hir>, |
b7449926 | 1170 | codes: ErrorCodes, |
f9f354fc | 1171 | tcx: TyCtxt<'tcx>, |
476ff2be | 1172 | } |
92a42be0 | 1173 | |
f9f354fc | 1174 | impl<'a, 'hir, 'tcx> HirCollector<'a, 'hir, 'tcx> { |
dfeec247 XL |
1175 | fn visit_testable<F: FnOnce(&mut Self)>( |
1176 | &mut self, | |
1177 | name: String, | |
f9f354fc XL |
1178 | hir_id: HirId, |
1179 | sp: Span, | |
dfeec247 XL |
1180 | nested: F, |
1181 | ) { | |
cdc7bbd5 | 1182 | let ast_attrs = self.tcx.hir().attrs(hir_id); |
c295e0f8 | 1183 | if let Some(ref cfg) = ast_attrs.cfg(self.tcx, &FxHashSet::default()) { |
3c0e092e | 1184 | if !cfg.matches(&self.sess.parse_sess, Some(self.sess.features_untracked())) { |
3b2f2976 XL |
1185 | return; |
1186 | } | |
1187 | } | |
1188 | ||
476ff2be SL |
1189 | let has_name = !name.is_empty(); |
1190 | if has_name { | |
1191 | self.collector.names.push(name); | |
1192 | } | |
92a42be0 | 1193 | |
0731742a XL |
1194 | // The collapse-docs pass won't combine sugared/raw doc attributes, or included files with |
1195 | // anything else, this will combine them for us. | |
04454e1e | 1196 | let attrs = Attributes::from_ast(ast_attrs, None); |
ff7c6d11 | 1197 | if let Some(doc) = attrs.collapsed_doc_value() { |
3dfed10e | 1198 | // Use the outermost invocation, so that doctest names come from where the docs were written. |
cdc7bbd5 XL |
1199 | let span = ast_attrs |
1200 | .span() | |
3dfed10e XL |
1201 | .map(|span| span.ctxt().outer_expn().expansion_cause().unwrap_or(span)) |
1202 | .unwrap_or(DUMMY_SP); | |
1203 | self.collector.set_position(span); | |
dfeec247 XL |
1204 | markdown::find_testable_code( |
1205 | &doc, | |
1206 | self.collector, | |
1207 | self.codes, | |
1208 | self.collector.enable_per_target_ignores, | |
f9f354fc | 1209 | Some(&crate::html::markdown::ExtraInfo::new( |
5869c6ff | 1210 | self.tcx, |
f9f354fc XL |
1211 | hir_id, |
1212 | span_of_attrs(&attrs).unwrap_or(sp), | |
1213 | )), | |
dfeec247 | 1214 | ); |
1a4d82fc | 1215 | } |
92a42be0 | 1216 | |
476ff2be SL |
1217 | nested(self); |
1218 | ||
1219 | if has_name { | |
1220 | self.collector.names.pop(); | |
1a4d82fc | 1221 | } |
476ff2be SL |
1222 | } |
1223 | } | |
92a42be0 | 1224 | |
f9f354fc | 1225 | impl<'a, 'hir, 'tcx> intravisit::Visitor<'hir> for HirCollector<'a, 'hir, 'tcx> { |
5099ac24 | 1226 | type NestedFilter = nested_filter::All; |
dfeec247 | 1227 | |
5099ac24 FG |
1228 | fn nested_visit_map(&mut self) -> Self::Map { |
1229 | self.map | |
476ff2be | 1230 | } |
92a42be0 | 1231 | |
f035d41b | 1232 | fn visit_item(&mut self, item: &'hir hir::Item<'_>) { |
94222f64 | 1233 | let name = match &item.kind { |
94222f64 XL |
1234 | hir::ItemKind::Impl(impl_) => { |
1235 | rustc_hir_pretty::id_to_string(&self.map, impl_.self_ty.hir_id) | |
1236 | } | |
1237 | _ => item.ident.to_string(), | |
476ff2be | 1238 | }; |
92a42be0 | 1239 | |
6a06907d | 1240 | self.visit_testable(name, item.hir_id(), item.span, |this| { |
476ff2be SL |
1241 | intravisit::walk_item(this, item); |
1242 | }); | |
1243 | } | |
92a42be0 | 1244 | |
f035d41b | 1245 | fn visit_trait_item(&mut self, item: &'hir hir::TraitItem<'_>) { |
6a06907d | 1246 | self.visit_testable(item.ident.to_string(), item.hir_id(), item.span, |this| { |
476ff2be SL |
1247 | intravisit::walk_trait_item(this, item); |
1248 | }); | |
1249 | } | |
92a42be0 | 1250 | |
f035d41b | 1251 | fn visit_impl_item(&mut self, item: &'hir hir::ImplItem<'_>) { |
6a06907d | 1252 | self.visit_testable(item.ident.to_string(), item.hir_id(), item.span, |this| { |
476ff2be SL |
1253 | intravisit::walk_impl_item(this, item); |
1254 | }); | |
1255 | } | |
1256 | ||
f035d41b | 1257 | fn visit_foreign_item(&mut self, item: &'hir hir::ForeignItem<'_>) { |
6a06907d | 1258 | self.visit_testable(item.ident.to_string(), item.hir_id(), item.span, |this| { |
476ff2be SL |
1259 | intravisit::walk_foreign_item(this, item); |
1260 | }); | |
1261 | } | |
1262 | ||
dfeec247 XL |
1263 | fn visit_variant( |
1264 | &mut self, | |
f035d41b XL |
1265 | v: &'hir hir::Variant<'_>, |
1266 | g: &'hir hir::Generics<'_>, | |
dfeec247 XL |
1267 | item_id: hir::HirId, |
1268 | ) { | |
6a06907d | 1269 | self.visit_testable(v.ident.to_string(), v.id, v.span, |this| { |
476ff2be SL |
1270 | intravisit::walk_variant(this, v, g, item_id); |
1271 | }); | |
1272 | } | |
1273 | ||
6a06907d XL |
1274 | fn visit_field_def(&mut self, f: &'hir hir::FieldDef<'_>) { |
1275 | self.visit_testable(f.ident.to_string(), f.hir_id, f.span, |this| { | |
1276 | intravisit::walk_field_def(this, f); | |
476ff2be SL |
1277 | }); |
1278 | } | |
1a4d82fc | 1279 | } |
0531ce1d XL |
1280 | |
1281 | #[cfg(test)] | |
dc9dc135 | 1282 | mod tests; |