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