diff --git a/crates/cargo-test-support/src/compare.rs b/crates/cargo-test-support/src/compare.rs index a9feb5f6feac..d7a8362104ce 100644 --- a/crates/cargo-test-support/src/compare.rs +++ b/crates/cargo-test-support/src/compare.rs @@ -168,6 +168,7 @@ fn substitute_macros(input: &str) -> String { ("[NOTE]", "note:"), ("[HELP]", "help:"), ("[DOCUMENTING]", " Documenting"), + ("[SCRAPING]", " Scraping"), ("[FRESH]", " Fresh"), ("[UPDATING]", " Updating"), ("[ADDING]", " Adding"), diff --git a/src/cargo/core/compiler/context/compilation_files.rs b/src/cargo/core/compiler/context/compilation_files.rs index f4d53c9240f8..dcfaabf327a1 100644 --- a/src/cargo/core/compiler/context/compilation_files.rs +++ b/src/cargo/core/compiler/context/compilation_files.rs @@ -451,9 +451,11 @@ impl<'a, 'cfg: 'a> CompilationFiles<'a, 'cfg> { vec![] } CompileMode::Docscrape => { - let path = self - .deps_dir(unit) - .join(format!("{}.examples", unit.buildkey())); + // The file name needs to be stable across Cargo sessions. + // This originally used unit.buildkey(), but that isn't stable, + // so we use metadata instead (prefixed with name for debugging). + let file_name = format!("{}-{}.examples", unit.pkg.name(), self.metadata(unit)); + let path = self.deps_dir(unit).join(file_name); vec![OutputFile { path, hardlink: None, diff --git a/src/cargo/core/compiler/context/mod.rs b/src/cargo/core/compiler/context/mod.rs index 75da79024b12..08c86a5b6dd1 100644 --- a/src/cargo/core/compiler/context/mod.rs +++ b/src/cargo/core/compiler/context/mod.rs @@ -77,6 +77,11 @@ pub struct Context<'a, 'cfg> { /// Map of Doc/Docscrape units to metadata for their -Cmetadata flag. /// See Context::find_metadata_units for more details. pub metadata_for_doc_units: HashMap, + + /// Set of metadata of Docscrape units that fail before completion, e.g. + /// because the target has a type error. This is in an Arc> + /// because it is continuously updated as the job progresses. + pub failed_scrape_units: Arc>>, } impl<'a, 'cfg> Context<'a, 'cfg> { @@ -115,6 +120,7 @@ impl<'a, 'cfg> Context<'a, 'cfg> { rustc_clients: HashMap::new(), lto: HashMap::new(), metadata_for_doc_units: HashMap::new(), + failed_scrape_units: Arc::new(Mutex::new(HashSet::new())), }) } diff --git a/src/cargo/core/compiler/fingerprint.rs b/src/cargo/core/compiler/fingerprint.rs index e147ac03d86d..eb78fd8cae7f 100644 --- a/src/cargo/core/compiler/fingerprint.rs +++ b/src/cargo/core/compiler/fingerprint.rs @@ -1275,7 +1275,7 @@ fn calculate_normal(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult, unit: &Unit) -> CargoResult { active: HashMap, compiled: HashSet, documented: HashSet, + scraped: HashSet, counts: HashMap, progress: Progress<'cfg>, next_id: u32, @@ -347,17 +348,29 @@ enum Message { BuildPlanMsg(String, ProcessBuilder, Arc>), Stdout(String), Stderr(String), + + // This is for general stderr output from subprocesses Diagnostic { id: JobId, level: String, diag: String, fixable: bool, }, + // This handles duplicate output that is suppressed, for showing + // only a count of duplicate messages instead WarningCount { id: JobId, emitted: bool, fixable: bool, }, + // This is for warnings generated by Cargo's interpretation of the + // subprocess output, e.g. scrape-examples prints a warning if a + // unit fails to be scraped + Warning { + id: JobId, + warning: String, + }, + FixDiagnostic(diagnostic_server::Message), Token(io::Result), Finish(JobId, Artifact, CargoResult<()>), @@ -405,6 +418,7 @@ impl<'a, 'cfg> JobState<'a, 'cfg> { Ok(()) } + /// See [`Message::Diagnostic`] and [`Message::WarningCount`]. pub fn emit_diag(&self, level: String, diag: String, fixable: bool) -> CargoResult<()> { if let Some(dedupe) = self.output { let emitted = dedupe.emit_diag(&diag)?; @@ -426,6 +440,15 @@ impl<'a, 'cfg> JobState<'a, 'cfg> { Ok(()) } + /// See [`Message::Warning`]. + pub fn warning(&self, warning: String) -> CargoResult<()> { + self.messages.push_bounded(Message::Warning { + id: self.id, + warning, + }); + Ok(()) + } + /// A method used to signal to the coordinator thread that the rmeta file /// for an rlib has been produced. This is only called for some rmeta /// builds when required, and can be called at any time before a job ends. @@ -475,8 +498,10 @@ impl<'cfg> JobQueue<'cfg> { .filter(|dep| { // Binaries aren't actually needed to *compile* tests, just to run // them, so we don't include this dependency edge in the job graph. + // But we shouldn't filter out dependencies being scraped for Rustdoc. (!dep.unit.target.is_test() && !dep.unit.target.is_bin()) || dep.unit.artifact.is_true() + || dep.unit.mode.is_doc_scrape() }) .map(|dep| { // Handle the case here where our `unit -> dep` dependency may @@ -563,6 +588,7 @@ impl<'cfg> JobQueue<'cfg> { active: HashMap::new(), compiled: HashSet::new(), documented: HashSet::new(), + scraped: HashSet::new(), counts: self.counts, progress, next_id: 0, @@ -739,6 +765,10 @@ impl<'cfg> DrainState<'cfg> { cnts.disallow_fixable(); } } + Message::Warning { id, warning } => { + cx.bcx.config.shell().warn(warning)?; + self.bump_warning_count(id, true, false); + } Message::WarningCount { id, emitted, @@ -782,6 +812,16 @@ impl<'cfg> DrainState<'cfg> { debug!("end ({:?}): {:?}", unit, result); match result { Ok(()) => self.finish(id, &unit, artifact, cx)?, + Err(_) + if unit.mode.is_doc_scrape() + && unit.target.doc_scrape_examples().is_unset() => + { + cx.failed_scrape_units + .lock() + .unwrap() + .insert(cx.files().metadata(&unit)); + self.queue.finish(&unit, &artifact); + } Err(error) => { let msg = "The following warnings were emitted during compilation:"; self.emit_warnings(Some(msg), &unit, cx)?; @@ -1303,8 +1343,11 @@ impl<'cfg> DrainState<'cfg> { unit: &Unit, fresh: Freshness, ) -> CargoResult<()> { - if (self.compiled.contains(&unit.pkg.package_id()) && !unit.mode.is_doc()) + if (self.compiled.contains(&unit.pkg.package_id()) + && !unit.mode.is_doc() + && !unit.mode.is_doc_scrape()) || (self.documented.contains(&unit.pkg.package_id()) && unit.mode.is_doc()) + || (self.scraped.contains(&unit.pkg.package_id()) && unit.mode.is_doc_scrape()) { return Ok(()); } @@ -1318,6 +1361,9 @@ impl<'cfg> DrainState<'cfg> { config.shell().status("Documenting", &unit.pkg)?; } else if unit.mode.is_doc_test() { // Skip doc test. + } else if unit.mode.is_doc_scrape() { + self.scraped.insert(unit.pkg.package_id()); + config.shell().status("Scraping", &unit.pkg)?; } else { self.compiled.insert(unit.pkg.package_id()); if unit.mode.is_check() { diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index ac47aafa7062..00c1f3ef292e 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -22,7 +22,7 @@ mod unit; pub mod unit_dependencies; pub mod unit_graph; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::env; use std::ffi::{OsStr, OsString}; use std::fs::{self, File}; @@ -55,7 +55,7 @@ use crate::core::compiler::future_incompat::FutureIncompatReport; pub use crate::core::compiler::unit::{Unit, UnitInterner}; use crate::core::manifest::TargetSourcePath; use crate::core::profiles::{PanicStrategy, Profile, Strip}; -use crate::core::{Feature, PackageId, Target}; +use crate::core::{Feature, PackageId, Target, Verbosity}; use crate::util::errors::{CargoResult, VerboseError}; use crate::util::interning::InternedString; use crate::util::machine_message::{self, Message}; @@ -654,13 +654,16 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult { rustdoc.arg("-C").arg(format!("metadata={}", metadata)); let scrape_output_path = |unit: &Unit| -> CargoResult { - let output_dir = cx.files().deps_dir(unit); - Ok(output_dir.join(format!("{}.examples", unit.buildkey()))) + cx.outputs(unit).map(|outputs| outputs[0].path.clone()) }; if unit.mode.is_doc_scrape() { debug_assert!(cx.bcx.scrape_units.contains(unit)); + if unit.target.is_test() { + rustdoc.arg("--scrape-tests"); + } + rustdoc.arg("-Zunstable-options"); rustdoc @@ -678,18 +681,23 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult { rustdoc.arg("--scrape-examples-target-crate").arg(name); } } - } else if cx.bcx.scrape_units.len() > 0 && cx.bcx.ws.unit_needs_doc_scrape(unit) { - // We only pass scraped examples to packages in the workspace - // since examples are only coming from reverse-dependencies of workspace packages + } + let should_include_scrape_units = unit.mode.is_doc() + && cx.bcx.scrape_units.len() > 0 + && cx.bcx.ws.unit_needs_doc_scrape(unit); + let scrape_outputs = if should_include_scrape_units { rustdoc.arg("-Zunstable-options"); - - for scrape_unit in &cx.bcx.scrape_units { - rustdoc - .arg("--with-examples") - .arg(scrape_output_path(scrape_unit)?); - } - } + Some( + cx.bcx + .scrape_units + .iter() + .map(|unit| Ok((cx.files().metadata(unit), scrape_output_path(unit)?))) + .collect::>>()?, + ) + } else { + None + }; build_deps_args(&mut rustdoc, cx, unit)?; rustdoc::add_root_urls(cx, unit, &mut rustdoc)?; @@ -700,19 +708,45 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult { append_crate_version_flag(unit, &mut rustdoc); } + let target_desc = unit.target.description_named(); let name = unit.pkg.name().to_string(); let build_script_outputs = Arc::clone(&cx.build_script_outputs); let package_id = unit.pkg.package_id(); let manifest_path = PathBuf::from(unit.pkg.manifest_path()); + let relative_manifest_path = manifest_path + .strip_prefix(cx.bcx.ws.root()) + .unwrap_or(&manifest_path) + .to_owned(); let target = Target::clone(&unit.target); let mut output_options = OutputOptions::new(cx, unit); let script_metadata = cx.find_build_script_metadata(unit); + let failed_scrape_units = Arc::clone(&cx.failed_scrape_units); + let hide_diagnostics_for_scrape_unit = unit.mode.is_doc_scrape() + && unit.target.doc_scrape_examples().is_unset() + && !matches!(cx.bcx.config.shell().verbosity(), Verbosity::Verbose); + if hide_diagnostics_for_scrape_unit { + output_options.show_diagnostics = false; + } Ok(Work::new(move |state| { add_custom_flags( &mut rustdoc, &build_script_outputs.lock().unwrap(), script_metadata, )?; + + // Add the output of scraped examples to the rustdoc command. + // This action must happen after the unit's dependencies have finished, + // because some of those deps may be Docscrape units which have failed. + // So we dynamically determine which `--with-examples` flags to pass here. + if let Some(scrape_outputs) = scrape_outputs { + let failed_scrape_units = failed_scrape_units.lock().unwrap(); + for (metadata, output_path) in &scrape_outputs { + if !failed_scrape_units.contains(metadata) { + rustdoc.arg("--with-examples").arg(output_path); + } + } + } + let crate_dir = doc_dir.join(&crate_name); if crate_dir.exists() { // Remove output from a previous build. This ensures that stale @@ -722,7 +756,7 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult { } state.running(&rustdoc); - rustdoc + let result = rustdoc .exec_with_streaming( &mut |line| on_stdout_line(state, line, package_id, &target), &mut |line| { @@ -737,7 +771,23 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult { }, false, ) - .with_context(|| format!("could not document `{}`", name))?; + .with_context(|| format!("could not document `{}`", name)); + + if let Err(e) = result { + if hide_diagnostics_for_scrape_unit { + let diag = format!( + "\ +failed to scan {target_desc} in package `{name}` for example code usage + Try running with `--verbose` to see the error message. + If this example should not be scanned, consider adding `doc-scrape-examples = false` to the `[[example]]` definition in {}", + relative_manifest_path.display() + ); + state.warning(diag)?; + } + + return Err(e); + } + Ok(()) })) } diff --git a/src/cargo/core/compiler/rustdoc.rs b/src/cargo/core/compiler/rustdoc.rs index 107410226dff..70e15c62cb42 100644 --- a/src/cargo/core/compiler/rustdoc.rs +++ b/src/cargo/core/compiler/rustdoc.rs @@ -190,3 +190,22 @@ pub fn add_root_urls( } Ok(()) } + +/// Indicates whether a target should have examples scraped from it +/// by rustdoc. Configured within Cargo.toml. +#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Copy)] +pub enum RustdocScrapeExamples { + Enabled, + Disabled, + Unset, +} + +impl RustdocScrapeExamples { + pub fn is_enabled(&self) -> bool { + matches!(self, RustdocScrapeExamples::Enabled) + } + + pub fn is_unset(&self) -> bool { + matches!(self, RustdocScrapeExamples::Unset) + } +} diff --git a/src/cargo/core/compiler/unit_dependencies.rs b/src/cargo/core/compiler/unit_dependencies.rs index 9319a19e038b..69ac801ff516 100644 --- a/src/cargo/core/compiler/unit_dependencies.rs +++ b/src/cargo/core/compiler/unit_dependencies.rs @@ -718,13 +718,14 @@ fn compute_deps_doc( // Add all units being scraped for examples as a dependency of top-level Doc units. if state.ws.unit_needs_doc_scrape(unit) { for scrape_unit in state.scrape_units.iter() { - deps_of(scrape_unit, state, unit_for)?; + let scrape_unit_for = UnitFor::new_normal(scrape_unit.kind); + deps_of(scrape_unit, state, scrape_unit_for)?; ret.push(new_unit_dep( state, scrape_unit, &scrape_unit.pkg, &scrape_unit.target, - unit_for, + scrape_unit_for, scrape_unit.kind, scrape_unit.mode, IS_NO_ARTIFACT_DEP, @@ -1088,7 +1089,6 @@ impl<'a, 'cfg> State<'a, 'cfg> { if !dep.is_transitive() && !unit.target.is_test() && !unit.target.is_example() - && !unit.mode.is_doc_scrape() && !unit.mode.is_any_test() { return false; diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index a87785bb045e..f295f063be10 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -689,10 +689,8 @@ unstable_cli_options!( terminal_width: Option> = ("Provide a terminal width to rustc for error truncation"), publish_timeout: bool = ("Enable the `publish.timeout` key in .cargo/config.toml file"), unstable_options: bool = ("Allow the usage of unstable options"), - // TODO(wcrichto): move scrape example configuration into Cargo.toml before stabilization - // See: https://github.com/rust-lang/cargo/pull/9525#discussion_r728470927 - rustdoc_scrape_examples: Option = ("Allow rustdoc to scrape examples from reverse-dependencies for documentation"), skip_rustdoc_fingerprint: bool = (HIDDEN), + rustdoc_scrape_examples: bool = ("Allows Rustdoc to scrape code examples from reverse-dependencies"), ); const STABILIZED_COMPILE_PROGRESS: &str = "The progress bar is now always \ @@ -963,15 +961,7 @@ impl CliUnstable { "namespaced-features" => stabilized_warn(k, "1.60", STABILISED_NAMESPACED_FEATURES), "weak-dep-features" => stabilized_warn(k, "1.60", STABILIZED_WEAK_DEP_FEATURES), "credential-process" => self.credential_process = parse_empty(k, v)?, - "rustdoc-scrape-examples" => { - if let Some(s) = v { - self.rustdoc_scrape_examples = Some(s.to_string()) - } else { - bail!( - r#"-Z rustdoc-scrape-examples must take "all" or "examples" as an argument"# - ) - } - } + "rustdoc-scrape-examples" => self.rustdoc_scrape_examples = parse_empty(k, v)?, "skip-rustdoc-fingerprint" => self.skip_rustdoc_fingerprint = parse_empty(k, v)?, "compile-progress" => stabilized_warn(k, "1.30", STABILIZED_COMPILE_PROGRESS), "offline" => stabilized_err(k, "1.36", STABILIZED_OFFLINE)?, diff --git a/src/cargo/core/manifest.rs b/src/cargo/core/manifest.rs index bea5d1818390..c37dd1cb4558 100644 --- a/src/cargo/core/manifest.rs +++ b/src/cargo/core/manifest.rs @@ -12,6 +12,7 @@ use serde::Serialize; use toml_edit::easy as toml; use url::Url; +use crate::core::compiler::rustdoc::RustdocScrapeExamples; use crate::core::compiler::{CompileKind, CrateType}; use crate::core::resolver::ResolveBehavior; use crate::core::{Dependency, PackageId, PackageIdSpec, SourceId, Summary}; @@ -220,6 +221,7 @@ struct TargetInner { for_host: bool, proc_macro: bool, edition: Edition, + doc_scrape_examples: RustdocScrapeExamples, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -373,6 +375,7 @@ compact_debug! { for_host proc_macro edition + doc_scrape_examples )] } } @@ -648,6 +651,7 @@ impl Target { harness: true, for_host: false, proc_macro: false, + doc_scrape_examples: RustdocScrapeExamples::Unset, edition, tested: true, benched: true, @@ -699,7 +703,8 @@ impl Target { .set_name(name) .set_for_host(true) .set_benched(false) - .set_tested(false); + .set_tested(false) + .set_doc_scrape_examples(RustdocScrapeExamples::Disabled); target } @@ -710,7 +715,8 @@ impl Target { .set_name(name) .set_for_host(true) .set_benched(false) - .set_tested(false); + .set_tested(false) + .set_doc_scrape_examples(RustdocScrapeExamples::Disabled); target } @@ -804,6 +810,9 @@ impl Target { pub fn edition(&self) -> Edition { self.inner.edition } + pub fn doc_scrape_examples(&self) -> RustdocScrapeExamples { + self.inner.doc_scrape_examples + } pub fn benched(&self) -> bool { self.inner.benched } @@ -918,6 +927,13 @@ impl Target { Arc::make_mut(&mut self.inner).edition = edition; self } + pub fn set_doc_scrape_examples( + &mut self, + doc_scrape_examples: RustdocScrapeExamples, + ) -> &mut Target { + Arc::make_mut(&mut self.inner).doc_scrape_examples = doc_scrape_examples; + self + } pub fn set_harness(&mut self, harness: bool) -> &mut Target { Arc::make_mut(&mut self.inner).harness = harness; self diff --git a/src/cargo/ops/cargo_compile/compile_filter.rs b/src/cargo/ops/cargo_compile/compile_filter.rs index bf6dbbd7bf59..a537a9a8a82e 100644 --- a/src/cargo/ops/cargo_compile/compile_filter.rs +++ b/src/cargo/ops/cargo_compile/compile_filter.rs @@ -1,10 +1,12 @@ //! Filters and their rules to select which Cargo targets will be built. use crate::core::compiler::CompileMode; -use crate::core::{Target, TargetKind}; +use crate::core::dependency::DepKind; +use crate::core::resolver::HasDevUnits; +use crate::core::{Package, Target, TargetKind}; use crate::util::restricted_names::is_glob_pattern; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] /// Indicates whether or not the library target gets included. pub enum LibRule { /// Include the library, fail if not present @@ -15,7 +17,7 @@ pub enum LibRule { False, } -#[derive(Debug)] +#[derive(Debug, Clone)] /// Indicates which Cargo targets will be selected to be built. pub enum FilterRule { /// All included. @@ -299,4 +301,67 @@ impl CompileFilter { } } } + + /// Generate a CompileFilter that represents the maximal set of targets + /// that should be considered for scraped examples. + pub(super) fn refine_for_docscrape( + &self, + to_builds: &[&Package], + has_dev_units: HasDevUnits, + ) -> CompileFilter { + // In general, the goal is to scrape examples from (a) whatever targets + // the user is documenting, and (b) Example targets. However, if the user + // is documenting a library with dev-dependencies, those dev-deps are not + // needed for the library, while dev-deps are needed for the examples. + // + // If scrape-examples caused `cargo doc` to start requiring dev-deps, this + // would be a breaking change to crates whose dev-deps don't compile. + // Therefore we ONLY want to scrape Example targets if either: + // (1) No package has dev-dependencies, so this is a moot issue, OR + // (2) The provided CompileFilter requires dev-dependencies anyway. + // + // The next two variables represent these two conditions. + + let no_pkg_has_dev_deps = to_builds.iter().all(|pkg| { + pkg.summary() + .dependencies() + .iter() + .all(|dep| !matches!(dep.kind(), DepKind::Development)) + }); + + let reqs_dev_deps = matches!(has_dev_units, HasDevUnits::Yes); + + let example_filter = if no_pkg_has_dev_deps || reqs_dev_deps { + FilterRule::All + } else { + FilterRule::none() + }; + + match self { + CompileFilter::Only { + all_targets, + lib, + bins, + tests, + benches, + .. + } => CompileFilter::Only { + all_targets: *all_targets, + lib: lib.clone(), + bins: bins.clone(), + examples: example_filter, + tests: tests.clone(), + benches: benches.clone(), + }, + + CompileFilter::Default { .. } => CompileFilter::Only { + all_targets: false, + lib: LibRule::Default, + bins: FilterRule::none(), + examples: example_filter, + tests: FilterRule::none(), + benches: FilterRule::none(), + }, + } + } } diff --git a/src/cargo/ops/cargo_compile/mod.rs b/src/cargo/ops/cargo_compile/mod.rs index b7bf1dd94f4f..622255f893c2 100644 --- a/src/cargo/ops/cargo_compile/mod.rs +++ b/src/cargo/ops/cargo_compile/mod.rs @@ -34,6 +34,7 @@ use std::fmt::Write; use std::hash::{Hash, Hasher}; use std::sync::Arc; +use crate::core::compiler::rustdoc::RustdocScrapeExamples; use crate::core::compiler::unit_dependencies::{build_unit_dependencies, IsArtifact}; use crate::core::compiler::unit_graph::{self, UnitDep, UnitGraph}; use crate::core::compiler::{standard_lib, CrateType, TargetInfo}; @@ -233,27 +234,34 @@ pub fn create_bcx<'a, 'cfg>( let target_data = RustcTargetData::new(ws, &build_config.requested_kinds)?; - let all_packages = &Packages::All; - let rustdoc_scrape_examples = &config.cli_unstable().rustdoc_scrape_examples; - let need_reverse_dependencies = rustdoc_scrape_examples.is_some(); - let full_specs = if need_reverse_dependencies { - all_packages - } else { - spec - }; + let specs = spec.to_package_id_specs(ws)?; + let has_dev_units = { + // Rustdoc itself doesn't need dev-dependencies. But to scrape examples from packages in the + // workspace, if any of those packages need dev-dependencies, then we need include dev-dependencies + // to scrape those packages. + let any_pkg_has_scrape_enabled = ws + .members_with_features(&specs, cli_features)? + .iter() + .any(|(pkg, _)| { + pkg.targets() + .iter() + .any(|target| target.is_example() && target.doc_scrape_examples().is_enabled()) + }); - let resolve_specs = full_specs.to_package_id_specs(ws)?; - let has_dev_units = if filter.need_dev_deps(build_config.mode) || need_reverse_dependencies { - HasDevUnits::Yes - } else { - HasDevUnits::No + if filter.need_dev_deps(build_config.mode) + || (build_config.mode.is_doc() && any_pkg_has_scrape_enabled) + { + HasDevUnits::Yes + } else { + HasDevUnits::No + } }; let resolve = ops::resolve_ws_with_opts( ws, &target_data, &build_config.requested_kinds, cli_features, - &resolve_specs, + &specs, has_dev_units, crate::core::resolver::features::ForceAllTargets::No, )?; @@ -276,11 +284,6 @@ pub fn create_bcx<'a, 'cfg>( // Find the packages in the resolver that the user wants to build (those // passed in with `-p` or the defaults from the workspace), and convert // Vec to a Vec. - let specs = if need_reverse_dependencies { - spec.to_package_id_specs(ws)? - } else { - resolve_specs.clone() - }; let to_build_ids = resolve.specs_to_ids(&specs)?; // Now get the `Package` for each `PackageId`. This may trigger a download // if the user specified `-p` for a dependency that is not downloaded. @@ -364,47 +367,45 @@ pub fn create_bcx<'a, 'cfg>( override_rustc_crate_types(&mut units, args, interner)?; } - let mut scrape_units = match rustdoc_scrape_examples { - Some(arg) => { - let filter = match arg.as_str() { - "all" => CompileFilter::new_all_targets(), - "examples" => CompileFilter::new( - LibRule::False, - FilterRule::none(), - FilterRule::none(), - FilterRule::All, - FilterRule::none(), - ), - _ => { - anyhow::bail!( - r#"-Z rustdoc-scrape-examples must take "all" or "examples" as an argument"# - ) - } - }; - let to_build_ids = resolve.specs_to_ids(&resolve_specs)?; - let to_builds = pkg_set.get_many(to_build_ids)?; - let mode = CompileMode::Docscrape; - - generate_targets( - ws, - &to_builds, - &filter, - &build_config.requested_kinds, - explicit_host_kind, - mode, - &resolve, - &workspace_resolve, - &resolved_features, - &pkg_set, - &profiles, - interner, - )? - .into_iter() - // Proc macros should not be scraped for functions, since they only export macros - .filter(|unit| !unit.target.proc_macro()) - .collect::>() + let should_scrape = build_config.mode.is_doc() && config.cli_unstable().rustdoc_scrape_examples; + let mut scrape_units = if should_scrape { + let scrape_filter = filter.refine_for_docscrape(&to_builds, has_dev_units); + let all_units = generate_targets( + ws, + &to_builds, + &scrape_filter, + &build_config.requested_kinds, + explicit_host_kind, + CompileMode::Docscrape, + &resolve, + &workspace_resolve, + &resolved_features, + &pkg_set, + &profiles, + interner, + )?; + + // The set of scraped targets should be a strict subset of the set of documented targets, + // except in the special case of examples targets. + if cfg!(debug_assertions) { + let valid_targets = units.iter().map(|u| &u.target).collect::>(); + for unit in &all_units { + assert!(unit.target.is_example() || valid_targets.contains(&unit.target)); + } } - None => Vec::new(), + + let valid_units = all_units + .into_iter() + .filter(|unit| { + !matches!( + unit.target.doc_scrape_examples(), + RustdocScrapeExamples::Disabled + ) + }) + .collect::>(); + valid_units + } else { + Vec::new() }; let std_roots = if let Some(crates) = standard_lib::std_crates(config, Some(&units)) { @@ -1232,6 +1233,9 @@ fn rebuild_unit_graph_shared( traverse_and_share(interner, &mut memo, &mut result, &unit_graph, root, to_host) }) .collect(); + // If no unit in the unit graph ended up having scrape units attached as dependencies, + // then they won't have been discovered in traverse_and_share and hence won't be in + // memo. So we filter out missing scrape units. let new_scrape_units = scrape_units .iter() .map(|unit| memo.get(unit).unwrap().clone()) diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index a6f144cb5622..b5b1a583c30c 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -2862,12 +2862,12 @@ impl DetailedTomlDependency { } #[derive(Default, Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] struct TomlTarget { name: Option, // The intention was to only accept `crate-type` here but historical // versions of Cargo also accepted `crate_type`, so look for both. - #[serde(rename = "crate-type")] crate_type: Option>, #[serde(rename = "crate_type")] crate_type2: Option>, @@ -2880,12 +2880,12 @@ struct TomlTarget { bench: Option, doc: Option, plugin: Option, + doc_scrape_examples: Option, #[serde(rename = "proc-macro")] proc_macro_raw: Option, #[serde(rename = "proc_macro")] proc_macro_raw2: Option, harness: Option, - #[serde(rename = "required-features")] required_features: Option>, edition: Option, } diff --git a/src/cargo/util/toml/targets.rs b/src/cargo/util/toml/targets.rs index ac9bf468af55..a2f4153a8c4c 100644 --- a/src/cargo/util/toml/targets.rs +++ b/src/cargo/util/toml/targets.rs @@ -18,6 +18,7 @@ use super::{ PathValue, StringOrBool, StringOrVec, TomlBenchTarget, TomlBinTarget, TomlExampleTarget, TomlLibTarget, TomlManifest, TomlTarget, TomlTestTarget, }; +use crate::core::compiler::rustdoc::RustdocScrapeExamples; use crate::core::compiler::CrateType; use crate::core::{Edition, Feature, Features, Target}; use crate::util::errors::CargoResult; @@ -808,6 +809,11 @@ fn configure(toml: &TomlTarget, target: &mut Target) -> CargoResult<()> { .set_benched(toml.bench.unwrap_or_else(|| t2.benched())) .set_harness(toml.harness.unwrap_or_else(|| t2.harness())) .set_proc_macro(toml.proc_macro().unwrap_or_else(|| t2.proc_macro())) + .set_doc_scrape_examples(match toml.doc_scrape_examples { + None => RustdocScrapeExamples::Unset, + Some(false) => RustdocScrapeExamples::Disabled, + Some(true) => RustdocScrapeExamples::Enabled, + }) .set_for_host(match (toml.plugin, toml.proc_macro()) { (None, None) => t2.for_host(), (Some(true), _) | (_, Some(true)) => true, diff --git a/tests/testsuite/doc.rs b/tests/testsuite/doc.rs index aa04ba5f2352..bf99a21a9a54 100644 --- a/tests/testsuite/doc.rs +++ b/tests/testsuite/doc.rs @@ -2343,265 +2343,6 @@ fn doc_fingerprint_unusual_behavior() { assert!(real_doc.join("somefile").exists()); } -#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] -fn scrape_examples_basic() { - let p = project() - .file( - "Cargo.toml", - r#" - [package] - name = "foo" - version = "0.0.1" - authors = [] - "#, - ) - .file("examples/ex.rs", "fn main() { foo::foo(); }") - .file("src/lib.rs", "pub fn foo() {}\npub fn bar() { foo(); }") - .build(); - - p.cargo("doc -Zunstable-options -Z rustdoc-scrape-examples=all") - .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) - .with_stderr( - "\ -[..] foo v0.0.1 ([CWD]) -[..] foo v0.0.1 ([CWD]) -[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] -", - ) - .run(); - - let doc_html = p.read_file("target/doc/foo/fn.foo.html"); - assert!(doc_html.contains("Examples found in repository")); - assert!(doc_html.contains("More examples")); - - // Ensure that the reverse-dependency has its sources generated - assert!(p.build_dir().join("doc/src/ex/ex.rs.html").exists()); -} - -#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] -fn scrape_examples_avoid_build_script_cycle() { - let p = project() - // package with build dependency - .file( - "Cargo.toml", - r#" - [package] - name = "foo" - version = "0.0.1" - authors = [] - links = "foo" - - [workspace] - members = ["bar"] - - [build-dependencies] - bar = {path = "bar"} - "#, - ) - .file("src/lib.rs", "") - .file("build.rs", "fn main(){}") - // dependency - .file( - "bar/Cargo.toml", - r#" - [package] - name = "bar" - version = "0.0.1" - authors = [] - links = "bar" - "#, - ) - .file("bar/src/lib.rs", "") - .file("bar/build.rs", "fn main(){}") - .build(); - - p.cargo("doc --all -Zunstable-options -Z rustdoc-scrape-examples=all") - .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) - .run(); -} - -// FIXME: This test is broken with latest nightly 2022-08-02. -// The example is calling a function from a proc-macro, but proc-macros don't -// export functions. It is not clear what this test is trying to exercise. -// #[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] -#[ignore = "broken, needs fixing"] -#[cargo_test] -fn scrape_examples_complex_reverse_dependencies() { - let p = project() - .file( - "Cargo.toml", - r#" - [package] - name = "foo" - version = "0.0.1" - authors = [] - - [dev-dependencies] - a = {path = "a", features = ["feature"]} - b = {path = "b"} - - [workspace] - members = ["b"] - "#, - ) - .file("src/lib.rs", "") - .file("examples/ex.rs", "fn main() { a::f(); }") - .file( - "a/Cargo.toml", - r#" - [package] - name = "a" - version = "0.0.1" - authors = [] - - [lib] - proc-macro = true - - [dependencies] - b = {path = "../b"} - - [features] - feature = [] - "#, - ) - .file("a/src/lib.rs", "#[cfg(feature)] pub fn f();") - .file( - "b/Cargo.toml", - r#" - [package] - name = "b" - version = "0.0.1" - authors = [] - "#, - ) - .file("b/src/lib.rs", "") - .build(); - - p.cargo("doc -Zunstable-options -Z rustdoc-scrape-examples=all") - .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) - .run(); -} - -#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] -fn scrape_examples_crate_with_dash() { - let p = project() - .file( - "Cargo.toml", - r#" - [package] - name = "da-sh" - version = "0.0.1" - authors = [] - "#, - ) - .file("src/lib.rs", "pub fn foo() {}") - .file("examples/a.rs", "fn main() { da_sh::foo(); }") - .build(); - - p.cargo("doc -Zunstable-options -Z rustdoc-scrape-examples=all") - .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) - .run(); - - let doc_html = p.read_file("target/doc/da_sh/fn.foo.html"); - assert!(doc_html.contains("Examples found in repository")); -} - -#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] -fn scrape_examples_missing_flag() { - let p = project() - .file( - "Cargo.toml", - r#" - [package] - name = "foo" - version = "1.2.4" - authors = [] - "#, - ) - .file("src/lib.rs", "//! These are the docs!") - .build(); - p.cargo("doc -Zrustdoc-scrape-examples") - .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) - .with_status(101) - .with_stderr("error: -Z rustdoc-scrape-examples must take [..] an argument") - .run(); -} - -#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] -fn scrape_examples_configure_profile() { - let p = project() - .file( - "Cargo.toml", - r#" - [package] - name = "foo" - version = "0.0.1" - authors = [] - - [profile.dev] - panic = "abort" - "#, - ) - .file("examples/ex.rs", "fn main() { foo::foo(); }") - .file("src/lib.rs", "pub fn foo() {}\npub fn bar() { foo(); }") - .build(); - - p.cargo("doc -Zunstable-options -Z rustdoc-scrape-examples=all") - .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) - .run(); - - let doc_html = p.read_file("target/doc/foo/fn.foo.html"); - assert!(doc_html.contains("Examples found in repository")); - assert!(doc_html.contains("More examples")); -} - -#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] -fn scrape_examples_issue_10545() { - let p = project() - .file( - "Cargo.toml", - r#" - [workspace] - resolver = "2" - members = ["a", "b"] - "#, - ) - .file( - "a/Cargo.toml", - r#" - [package] - name = "a" - version = "0.0.1" - authors = [] - edition = "2021" - - [features] - default = ["foo"] - foo = [] - "#, - ) - .file("a/src/lib.rs", "") - .file( - "b/Cargo.toml", - r#" - [package] - name = "b" - version = "0.0.1" - authors = [] - edition = "2021" - - [lib] - proc-macro = true - "#, - ) - .file("b/src/lib.rs", "") - .build(); - - p.cargo("doc -Zunstable-options -Z rustdoc-scrape-examples=all") - .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) - .run(); -} - #[cargo_test] fn lib_before_bin() { // Checks that the library is documented before the binary. diff --git a/tests/testsuite/docscrape.rs b/tests/testsuite/docscrape.rs new file mode 100644 index 000000000000..f205e8b61e62 --- /dev/null +++ b/tests/testsuite/docscrape.rs @@ -0,0 +1,521 @@ +//! Tests for the `cargo doc` command with `-Zrustdoc-scrape-examples`. + +use cargo_test_support::project; + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn basic() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + "#, + ) + .file("examples/ex.rs", "fn main() { foo::foo(); }") + .file("src/lib.rs", "pub fn foo() {}\npub fn bar() { foo(); }") + .build(); + + p.cargo("doc -Zunstable-options -Zrustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .with_stderr( + "\ +[CHECKING] foo v0.0.1 ([CWD]) +[SCRAPING] foo v0.0.1 ([CWD]) +[DOCUMENTING] foo v0.0.1 ([CWD]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +", + ) + .run(); + + p.cargo("doc -Zunstable-options -Z rustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .with_stderr("[FINISHED] [..]") + .run(); + + let doc_html = p.read_file("target/doc/foo/fn.foo.html"); + assert!(doc_html.contains("Examples found in repository")); + assert!(doc_html.contains("More examples")); + + // Ensure that the reverse-dependency has its sources generated + assert!(p.build_dir().join("doc/src/ex/ex.rs.html").exists()); +} + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn avoid_build_script_cycle() { + let p = project() + // package with build dependency + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + links = "foo" + + [workspace] + members = ["bar"] + + [build-dependencies] + bar = {path = "bar"} + "#, + ) + .file("src/lib.rs", "") + .file("build.rs", "fn main(){}") + // dependency + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + links = "bar" + "#, + ) + .file("bar/src/lib.rs", "") + .file("bar/build.rs", "fn main(){}") + .build(); + + p.cargo("doc --workspace -Zunstable-options -Zrustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .run(); +} + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn complex_reverse_dependencies() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [dev-dependencies] + a = {path = "a", features = ["feature"]} + b = {path = "b"} + + [workspace] + members = ["b"] + "#, + ) + .file("src/lib.rs", "") + .file("examples/ex.rs", "fn main() {}") + .file( + "a/Cargo.toml", + r#" + [package] + name = "a" + version = "0.0.1" + authors = [] + + [lib] + proc-macro = true + + [dependencies] + b = {path = "../b"} + + [features] + feature = [] + "#, + ) + .file("a/src/lib.rs", "") + .file( + "b/Cargo.toml", + r#" + [package] + name = "b" + version = "0.0.1" + authors = [] + "#, + ) + .file("b/src/lib.rs", "") + .build(); + + p.cargo("doc --workspace --examples -Zunstable-options -Zrustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .run(); +} + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn crate_with_dash() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "da-sh" + version = "0.0.1" + authors = [] + "#, + ) + .file("src/lib.rs", "pub fn foo() {}") + .file("examples/a.rs", "fn main() { da_sh::foo(); }") + .build(); + + p.cargo("doc -Zunstable-options -Zrustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .run(); + + let doc_html = p.read_file("target/doc/da_sh/fn.foo.html"); + assert!(doc_html.contains("Examples found in repository")); +} + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn configure_target() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [lib] + doc-scrape-examples = false + + [[bin]] + name = "a_bin" + doc-scrape-examples = true + "#, + ) + .file( + "src/lib.rs", + "pub fn foo() {} fn lib_must_not_appear() { foo(); }", + ) + .file("examples/a.rs", "fn example_must_appear() { foo::foo(); }") + .file( + "src/bin/a_bin.rs", + "fn bin_must_appear() { foo::foo(); } fn main(){}", + ) + .build(); + + p.cargo("doc --lib --bins -Zunstable-options -Zrustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .run(); + + let doc_html = p.read_file("target/doc/foo/fn.foo.html"); + assert!(!doc_html.contains("lib_must_not_appear")); + assert!(doc_html.contains("example_must_appear")); + assert!(doc_html.contains("bin_must_appear")); +} + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn configure_profile_issue_10500() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [profile.dev] + panic = "abort" + "#, + ) + .file("examples/ex.rs", "fn main() { foo::foo(); }") + .file("src/lib.rs", "pub fn foo() {}\npub fn bar() { foo(); }") + .build(); + + p.cargo("doc -Zunstable-options -Zrustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .run(); + + let doc_html = p.read_file("target/doc/foo/fn.foo.html"); + assert!(doc_html.contains("Examples found in repository")); + assert!(doc_html.contains("More examples")); +} + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn issue_10545() { + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + resolver = "2" + members = ["a", "b"] + "#, + ) + .file( + "a/Cargo.toml", + r#" + [package] + name = "a" + version = "0.0.1" + authors = [] + edition = "2021" + + [features] + default = ["foo"] + foo = [] + "#, + ) + .file("a/src/lib.rs", "") + .file( + "b/Cargo.toml", + r#" + [package] + name = "b" + version = "0.0.1" + authors = [] + edition = "2021" + + [lib] + proc-macro = true + "#, + ) + .file("b/src/lib.rs", "") + .build(); + + p.cargo("doc --workspace -Zunstable-options -Zrustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .run(); +} + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn cache() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + "#, + ) + .file("examples/ex.rs", "fn main() { foo::foo(); }") + .file("src/lib.rs", "pub fn foo() {}\npub fn bar() { foo(); }") + .build(); + + p.cargo("doc -Zunstable-options -Zrustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .with_stderr( + "\ +[CHECKING] foo v0.0.1 ([CWD]) +[SCRAPING] foo v0.0.1 ([CWD]) +[DOCUMENTING] foo v0.0.1 ([CWD]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +", + ) + .run(); + + p.cargo("doc -Zunstable-options -Zrustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .with_stderr( + "\ +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +", + ) + .run(); +} + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn no_fail_bad_lib() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + "#, + ) + .file("src/lib.rs", "pub fn foo() { CRASH_THE_BUILD() }") + .build(); + + p.cargo("doc -Zunstable-options -Z rustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .with_stderr( + "\ +[SCRAPING] foo v0.0.1 ([CWD]) +warning: failed to scan lib in package `foo` for example code usage + Try running with `--verbose` to see the error message. + If this example should not be scanned, consider adding `doc-scrape-examples = false` to the `[[example]]` definition in Cargo.toml +warning: `foo` (lib) generated 1 warning +[DOCUMENTING] foo v0.0.1 ([CWD]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", + ) + .run(); +} + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn no_fail_bad_example() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + "#, + ) + .file("examples/ex1.rs", "DOES NOT COMPILE") + .file("examples/ex2.rs", "fn main() { foo::foo(); }") + .file("src/lib.rs", "pub fn foo(){}") + .build(); + + p.cargo("doc -Zunstable-options -Z rustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .with_stderr( + "\ +[CHECKING] foo v0.0.1 ([CWD]) +[SCRAPING] foo v0.0.1 ([CWD]) +warning: failed to scan example \"ex1\" in package `foo` for example code usage + Try running with `--verbose` to see the error message. + If this example should not be scanned, consider adding `doc-scrape-examples = false` to the `[[example]]` definition in Cargo.toml +warning: `foo` (example \"ex1\") generated 1 warning +[DOCUMENTING] foo v0.0.1 ([CWD]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", + ) + .run(); + + p.cargo("clean").run(); + + p.cargo("doc -v -Zunstable-options -Z rustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .with_stderr_unordered( + "\ +[CHECKING] foo v0.0.1 ([CWD]) +[RUNNING] `rustc --crate-name foo[..] +[SCRAPING] foo v0.0.1 ([CWD]) +[RUNNING] `rustdoc[..] --crate-name ex1[..] +[RUNNING] `rustdoc[..] --crate-name ex2[..] +[RUNNING] `rustdoc[..] --crate-name foo[..] +error: expected one of `!` or `::`, found `NOT` + --> examples/ex1.rs:1:6 + | +1 | DOES NOT COMPILE + | ^^^ expected one of `!` or `::` + +[DOCUMENTING] foo v0.0.1 ([CWD]) +[RUNNING] `rustdoc[..] --crate-name foo[..] +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", + ) + .run(); + + let doc_html = p.read_file("target/doc/foo/fn.foo.html"); + assert!(doc_html.contains("Examples found in repository")); +} + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn no_scrape_with_dev_deps() { + // Tests that a crate with dev-dependencies does not have its examples + // scraped unless explicitly prompted to check them. See + // `CompileFilter::refine_for_docscrape` for details on why. + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [lib] + doc-scrape-examples = false + + [dev-dependencies] + a = {path = "a"} + "#, + ) + .file("src/lib.rs", "") + .file("examples/ex.rs", "fn main() { a::f(); }") + .file( + "a/Cargo.toml", + r#" + [package] + name = "a" + version = "0.0.1" + authors = [] + "#, + ) + .file("a/src/lib.rs", "pub fn f() {}") + .build(); + + // If --examples is not provided, then the example is never scanned. + p.cargo("doc -Zunstable-options -Z rustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .with_stderr( + "\ +[DOCUMENTING] foo v0.0.1 ([CWD]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", + ) + .run(); + + // If --examples is provided, then the bad example is scanned and ignored. + p.cargo("doc --examples -Zunstable-options -Z rustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .with_stderr_unordered( + "\ +[CHECKING] a v0.0.1 ([CWD]/a) +[CHECKING] foo v0.0.1 ([CWD]) +[DOCUMENTING] a v0.0.1 ([CWD]/a) +[SCRAPING] foo v0.0.1 ([CWD]) +[DOCUMENTING] foo v0.0.1 ([CWD]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", + ) + .run(); +} + +#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")] +fn use_dev_deps_if_explicitly_enabled() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [lib] + doc-scrape-examples = false + + [[example]] + name = "ex" + doc-scrape-examples = true + + [dev-dependencies] + a = {path = "a"} + "#, + ) + .file("src/lib.rs", "") + .file("examples/ex.rs", "fn main() { a::f(); }") + .file( + "a/Cargo.toml", + r#" + [package] + name = "a" + version = "0.0.1" + authors = [] + "#, + ) + .file("a/src/lib.rs", "pub fn f() {}") + .build(); + + // If --examples is not provided, then the example is never scanned. + p.cargo("doc -Zunstable-options -Z rustdoc-scrape-examples") + .masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"]) + .with_stderr_unordered( + "\ +[CHECKING] foo v0.0.1 ([CWD]) +[CHECKING] a v0.0.1 ([CWD]/a) +[SCRAPING] foo v0.0.1 ([CWD]) +[DOCUMENTING] foo v0.0.1 ([CWD]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", + ) + .run(); +} diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index b1f0121beb66..b71a9a95ecef 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -45,6 +45,7 @@ mod death; mod dep_info; mod directory; mod doc; +mod docscrape; mod edition; mod error; mod features; diff --git a/tests/testsuite/package_features.rs b/tests/testsuite/package_features.rs index 6a237b1f3e7c..6cba65248049 100644 --- a/tests/testsuite/package_features.rs +++ b/tests/testsuite/package_features.rs @@ -468,9 +468,7 @@ fn non_member() { p.cargo("build -p dep --features f1") .with_status(101) - .with_stderr( - "[UPDATING][..]\n[ERROR] cannot specify features for packages outside of workspace", - ) + .with_stderr("[ERROR] cannot specify features for packages outside of workspace") .run(); p.cargo("build -p dep --all-features") @@ -486,6 +484,7 @@ fn non_member() { p.cargo("build -p dep") .with_stderr( "\ +[UPDATING] [..] [DOWNLOADING] [..] [DOWNLOADED] [..] [COMPILING] dep [..]