Auto merge of #60387 - Goirad:test-expansion, r=ollie27

Allow cross-compiling doctests

This PR allows doctest to receive a --runtool argument, as well as possibly many --runtool-arg arguments, which are then used to run cross compiled doctests.
Also, functionality has been added to rustdoc to allow it to skip testing doctests on a per-target basis, in the same way that compiletest does it. For example, tagging the doctest with "ignore-sgx" disables testing on any targets that contain "sgx". A plain "ignore" still skips testing on all targets.

See [here](https://github.com/rust-lang/cargo/pull/6892) for the companion PR in the cargo project that extends functionality in Cargo so that it passes the appropriate parameters to rustdoc when cross compiling and testing doctests.

Part of [#6460](https://github.com/rust-lang/cargo/issues/6460)
This commit is contained in:
bors 2019-09-10 12:19:41 +00:00
commit 87b0c9036f
9 changed files with 190 additions and 47 deletions

View File

@ -471,3 +471,53 @@ Some methodology notes about what rustdoc counts in this metric:
Public items that are not documented can be seen with the built-in `missing_docs` lint. Private
items that are not documented can be seen with Clippy's `missing_docs_in_private_items` lint.
### `--enable-per-target-ignores`: allow `ignore-foo` style filters for doctests
Using this flag looks like this:
```bash
$ rustdoc src/lib.rs -Z unstable-options --enable-per-target-ignores
```
This flag allows you to tag doctests with compiltest style `ignore-foo` filters that prevent
rustdoc from running that test if the target triple string contains foo. For example:
```rust
///```ignore-foo,ignore-bar
///assert!(2 == 2);
///```
struct Foo;
```
This will not be run when the build target is `super-awesome-foo` or `less-bar-awesome`.
If the flag is not enabled, then rustdoc will consume the filter, but do nothing with it, and
the above example will be run for all targets.
If you want to preserve backwards compatibility for older versions of rustdoc, you can use
```rust
///```ignore,ignore-foo
///assert!(2 == 2);
///```
struct Foo;
```
In older versions, this will be ignored on all targets, but on newer versions `ignore-gnu` will
override `ignore`.
### `--runtool`, `--runtool-arg`: program to run tests with; args to pass to it
Using thses options looks like this:
```bash
$ rustdoc src/lib.rs -Z unstable-options --runtool runner --runtool-arg --do-thing --runtool-arg --do-other-thing
```
These options can be used to run the doctest under a program, and also pass arguments to
that program. For example, if you want to run your doctests under valgrind you might run
```bash
$ rustdoc src/lib.rs -Z unstable-options --runtool valgrind
```
Another use case would be to run a test inside an emulator, or through a Virtual Machine.

View File

@ -9,7 +9,7 @@ use rustc::session;
use rustc::session::config::{CrateType, parse_crate_types_from_list};
use rustc::session::config::{CodegenOptions, DebuggingOptions, ErrorOutputType, Externs};
use rustc::session::config::{nightly_options, build_codegen_options, build_debugging_options,
get_cmd_lint_options, ExternEntry};
get_cmd_lint_options, host_triple, ExternEntry};
use rustc::session::search_paths::SearchPath;
use rustc_driver;
use rustc_target::spec::TargetTriple;
@ -54,7 +54,7 @@ pub struct Options {
/// Debugging (`-Z`) options to pass to the compiler.
pub debugging_options: DebuggingOptions,
/// The target used to compile the crate against.
pub target: Option<TargetTriple>,
pub target: TargetTriple,
/// Edition used when reading the crate. Defaults to "2015". Also used by default when
/// compiling doctests from the crate.
pub edition: Edition,
@ -77,6 +77,14 @@ pub struct Options {
/// Optional path to persist the doctest executables to, defaults to a
/// temporary directory if not set.
pub persist_doctests: Option<PathBuf>,
/// Runtool to run doctests with
pub runtool: Option<String>,
/// Arguments to pass to the runtool
pub runtool_args: Vec<String>,
/// Whether to allow ignoring doctests on a per-target basis
/// For example, using ignore-foo to ignore running the doctest on any target that
/// contains "foo" as a substring
pub enable_per_target_ignores: bool,
// Options that affect the documentation process
@ -140,6 +148,9 @@ impl fmt::Debug for Options {
.field("show_coverage", &self.show_coverage)
.field("crate_version", &self.crate_version)
.field("render_options", &self.render_options)
.field("runtool", &self.runtool)
.field("runtool_args", &self.runtool_args)
.field("enable-per-target-ignores", &self.enable_per_target_ignores)
.finish()
}
}
@ -414,7 +425,9 @@ impl Options {
}
}
let target = matches.opt_str("target").map(|target| {
let target = matches.opt_str("target").map_or(
TargetTriple::from_triple(host_triple()),
|target| {
if target.ends_with(".json") {
TargetTriple::TargetPath(PathBuf::from(target))
} else {
@ -466,6 +479,9 @@ impl Options {
let codegen_options_strs = matches.opt_strs("C");
let lib_strs = matches.opt_strs("L");
let extern_strs = matches.opt_strs("extern");
let runtool = matches.opt_str("runtool");
let runtool_args = matches.opt_strs("runtool-arg");
let enable_per_target_ignores = matches.opt_present("enable-per-target-ignores");
let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(matches, error_format);
@ -496,6 +512,9 @@ impl Options {
show_coverage,
crate_version,
persist_doctests,
runtool,
runtool_args,
enable_per_target_ignores,
render_options: RenderOptions {
output,
external_html,

View File

@ -13,7 +13,6 @@ use rustc_interface::interface;
use rustc_driver::abort_on_err;
use rustc_resolve as resolve;
use rustc_metadata::cstore::CStore;
use rustc_target::spec::TargetTriple;
use syntax::source_map;
use syntax::attr;
@ -294,7 +293,6 @@ pub fn run_core(options: RustdocOptions) -> (clean::Crate, RenderInfo, RenderOpt
}
}).collect();
let host_triple = TargetTriple::from_triple(config::host_triple());
let crate_types = if proc_macro_crate {
vec![config::CrateType::ProcMacro]
} else {
@ -313,7 +311,7 @@ pub fn run_core(options: RustdocOptions) -> (clean::Crate, RenderInfo, RenderOpt
lint_cap: Some(lint_cap.unwrap_or_else(|| lint::Forbid)),
cg: codegen_options,
externs,
target_triple: target.unwrap_or(host_triple),
target_triple: target,
// Ensure that rustdoc works even if rustc is feature-staged
unstable_features: UnstableFeatures::Allow,
actually_rustdoc: true,

View File

@ -199,7 +199,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
let ignore;
let edition;
if let Some(Event::Start(Tag::CodeBlock(lang))) = event {
let parse_result = LangString::parse(&lang, self.check_error_codes);
let parse_result = LangString::parse(&lang, self.check_error_codes, false);
if !parse_result.rust {
return Some(Event::Start(Tag::CodeBlock(lang)));
}
@ -272,7 +272,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
))
});
let tooltip = if ignore {
let tooltip = if ignore != Ignore::None {
Some(("This example is not tested".to_owned(), "ignore"))
} else if compile_fail {
Some(("This example deliberately fails to compile".to_owned(), "compile_fail"))
@ -286,7 +286,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
s.push_str(&highlight::render_with_highlighting(
&text,
Some(&format!("rust-example-rendered{}",
if ignore { " ignore" }
if ignore != Ignore::None { " ignore" }
else if compile_fail { " compile_fail" }
else if explicit_edition { " edition " }
else { "" })),
@ -297,7 +297,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
s.push_str(&highlight::render_with_highlighting(
&text,
Some(&format!("rust-example-rendered{}",
if ignore { " ignore" }
if ignore != Ignore::None { " ignore" }
else if compile_fail { " compile_fail" }
else if explicit_edition { " edition " }
else { "" })),
@ -551,7 +551,8 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for Footnotes<'a, I> {
}
}
pub fn find_testable_code<T: test::Tester>(doc: &str, tests: &mut T, error_codes: ErrorCodes) {
pub fn find_testable_code<T: test::Tester>(doc: &str, tests: &mut T, error_codes: ErrorCodes,
enable_per_target_ignores: bool) {
let mut parser = Parser::new(doc);
let mut prev_offset = 0;
let mut nb_lines = 0;
@ -564,7 +565,7 @@ pub fn find_testable_code<T: test::Tester>(doc: &str, tests: &mut T, error_codes
let block_info = if s.is_empty() {
LangString::all_false()
} else {
LangString::parse(&*s, error_codes)
LangString::parse(&*s, error_codes, enable_per_target_ignores)
};
if !block_info.rust {
continue;
@ -607,7 +608,7 @@ pub struct LangString {
original: String,
pub should_panic: bool,
pub no_run: bool,
pub ignore: bool,
pub ignore: Ignore,
pub rust: bool,
pub test_harness: bool,
pub compile_fail: bool,
@ -616,13 +617,20 @@ pub struct LangString {
pub edition: Option<Edition>
}
#[derive(Eq, PartialEq, Clone, Debug)]
pub enum Ignore {
All,
None,
Some(Vec<String>),
}
impl LangString {
fn all_false() -> LangString {
LangString {
original: String::new(),
should_panic: false,
no_run: false,
ignore: false,
ignore: Ignore::None,
rust: true, // NB This used to be `notrust = false`
test_harness: false,
compile_fail: false,
@ -632,11 +640,16 @@ impl LangString {
}
}
fn parse(string: &str, allow_error_code_check: ErrorCodes) -> LangString {
fn parse(
string: &str,
allow_error_code_check: ErrorCodes,
enable_per_target_ignores: bool
) -> LangString {
let allow_error_code_check = allow_error_code_check.as_bool();
let mut seen_rust_tags = false;
let mut seen_other_tags = false;
let mut data = LangString::all_false();
let mut ignores = vec![];
data.original = string.to_owned();
let tokens = string.split(|c: char|
@ -651,7 +664,11 @@ impl LangString {
seen_rust_tags = seen_other_tags == false;
}
"no_run" => { data.no_run = true; seen_rust_tags = !seen_other_tags; }
"ignore" => { data.ignore = true; seen_rust_tags = !seen_other_tags; }
"ignore" => { data.ignore = Ignore::All; seen_rust_tags = !seen_other_tags; }
x if x.starts_with("ignore-") => if enable_per_target_ignores {
ignores.push(x.trim_start_matches("ignore-").to_owned());
seen_rust_tags = !seen_other_tags;
}
"allow_fail" => { data.allow_fail = true; seen_rust_tags = !seen_other_tags; }
"rust" => { data.rust = true; seen_rust_tags = true; }
"test_harness" => {
@ -679,6 +696,10 @@ impl LangString {
_ => { seen_other_tags = true }
}
}
// ignore-foo overrides ignore
if !ignores.is_empty() {
data.ignore = Ignore::Some(ignores);
}
data.rust &= !seen_other_tags || seen_rust_tags;
@ -919,7 +940,7 @@ crate fn rust_code_blocks(md: &str) -> Vec<RustCodeBlock> {
let lang_string = if syntax.is_empty() {
LangString::all_false()
} else {
LangString::parse(&*syntax, ErrorCodes::Yes)
LangString::parse(&*syntax, ErrorCodes::Yes, false)
};
if lang_string.rust {

View File

@ -1,4 +1,4 @@
use super::{ErrorCodes, LangString, Markdown, MarkdownHtml, IdMap};
use super::{ErrorCodes, LangString, Markdown, MarkdownHtml, IdMap, Ignore};
use super::plain_summary_line;
use std::cell::RefCell;
use syntax::edition::{Edition, DEFAULT_EDITION};
@ -26,10 +26,10 @@ fn test_unique_id() {
#[test]
fn test_lang_string_parse() {
fn t(s: &str,
should_panic: bool, no_run: bool, ignore: bool, rust: bool, test_harness: bool,
should_panic: bool, no_run: bool, ignore: Ignore, rust: bool, test_harness: bool,
compile_fail: bool, allow_fail: bool, error_codes: Vec<String>,
edition: Option<Edition>) {
assert_eq!(LangString::parse(s, ErrorCodes::Yes), LangString {
edition: Option<Edition>) {
assert_eq!(LangString::parse(s, ErrorCodes::Yes, true), LangString {
should_panic,
no_run,
ignore,
@ -42,6 +42,7 @@ fn test_lang_string_parse() {
edition,
})
}
let ignore_foo = Ignore::Some(vec!("foo".to_string()));
fn v() -> Vec<String> {
Vec::new()
@ -50,23 +51,24 @@ fn test_lang_string_parse() {
// ignore-tidy-linelength
// marker | should_panic | no_run | ignore | rust | test_harness
// | compile_fail | allow_fail | error_codes | edition
t("", false, false, false, true, false, false, false, v(), None);
t("rust", false, false, false, true, false, false, false, v(), None);
t("sh", false, false, false, false, false, false, false, v(), None);
t("ignore", false, false, true, true, false, false, false, v(), None);
t("should_panic", true, false, false, true, false, false, false, v(), None);
t("no_run", false, true, false, true, false, false, false, v(), None);
t("test_harness", false, false, false, true, true, false, false, v(), None);
t("compile_fail", false, true, false, true, false, true, false, v(), None);
t("allow_fail", false, false, false, true, false, false, true, v(), None);
t("{.no_run .example}", false, true, false, true, false, false, false, v(), None);
t("{.sh .should_panic}", true, false, false, false, false, false, false, v(), None);
t("{.example .rust}", false, false, false, true, false, false, false, v(), None);
t("{.test_harness .rust}", false, false, false, true, true, false, false, v(), None);
t("text, no_run", false, true, false, false, false, false, false, v(), None);
t("text,no_run", false, true, false, false, false, false, false, v(), None);
t("edition2015", false, false, false, true, false, false, false, v(), Some(Edition::Edition2015));
t("edition2018", false, false, false, true, false, false, false, v(), Some(Edition::Edition2018));
t("", false, false, Ignore::None, true, false, false, false, v(), None);
t("rust", false, false, Ignore::None, true, false, false, false, v(), None);
t("sh", false, false, Ignore::None, false, false, false, false, v(), None);
t("ignore", false, false, Ignore::All, true, false, false, false, v(), None);
t("ignore-foo", false, false, ignore_foo, true, false, false, false, v(), None);
t("should_panic", true, false, Ignore::None, true, false, false, false, v(), None);
t("no_run", false, true, Ignore::None, true, false, false, false, v(), None);
t("test_harness", false, false, Ignore::None, true, true, false, false, v(), None);
t("compile_fail", false, true, Ignore::None, true, false, true, false, v(), None);
t("allow_fail", false, false, Ignore::None, true, false, false, true, v(), None);
t("{.no_run .example}", false, true, Ignore::None, true, false, false, false, v(), None);
t("{.sh .should_panic}", true, false, Ignore::None, false, false, false, false, v(), None);
t("{.example .rust}", false, false, Ignore::None, true, false, false, false, v(), None);
t("{.test_harness .rust}", false, false, Ignore::None, true, true, false, false, v(), None);
t("text, no_run", false, true, Ignore::None, false, false, false, false, v(), None);
t("text,no_run", false, true, Ignore::None, false, false, false, false, v(), None);
t("edition2015", false, false, Ignore::None, true, false, false, false, v(), Some(Edition::Edition2015));
t("edition2018", false, false, Ignore::None, true, false, false, false, v(), Some(Edition::Edition2018));
}
#[test]

View File

@ -356,6 +356,23 @@ fn opts() -> Vec<RustcOptGroup> {
"show-coverage",
"calculate percentage of public items with documentation")
}),
unstable("enable-per-target-ignores", |o| {
o.optflag("",
"enable-per-target-ignores",
"parse ignore-foo for ignoring doctests on a per-target basis")
}),
unstable("runtool", |o| {
o.optopt("",
"runtool",
"",
"The tool to run tests with when building for a different target than host")
}),
unstable("runtool-arg", |o| {
o.optmulti("",
"runtool-arg",
"",
"One (of possibly many) arguments to pass to the runtool")
}),
]
}

View File

@ -143,11 +143,12 @@ pub fn test(mut options: Options, diag: &errors::Handler) -> i32 {
opts.no_crate_inject = true;
opts.display_warnings = options.display_warnings;
let mut collector = Collector::new(options.input.display().to_string(), options.clone(),
true, opts, None, Some(options.input));
true, opts, None, Some(options.input),
options.enable_per_target_ignores);
collector.set_position(DUMMY_SP);
let codes = ErrorCodes::from(UnstableFeatures::from_environment().is_nightly_build());
find_testable_code(&input_str, &mut collector, codes);
find_testable_code(&input_str, &mut collector, codes, options.enable_per_target_ignores);
options.test_args.insert(0, "rustdoctest".to_string());
testing::test_main(&options.test_args, collector.tests,

View File

@ -336,7 +336,7 @@ pub fn look_for_tests<'tcx>(
found_tests: 0,
};
find_testable_code(&dox, &mut tests, ErrorCodes::No);
find_testable_code(&dox, &mut tests, ErrorCodes::No, false);
if check_missing_code == true && tests.found_tests == 0 {
let sp = span_of_attrs(&item.attrs).unwrap_or(item.source.span());

View File

@ -1,5 +1,6 @@
use rustc_data_structures::sync::Lrc;
use rustc_interface::interface;
use rustc_target::spec::TargetTriple;
use rustc::hir;
use rustc::hir::intravisit;
use rustc::session::{self, config, DiagnosticOutput};
@ -22,7 +23,7 @@ use testing;
use crate::clean::Attributes;
use crate::config::Options;
use crate::html::markdown::{self, ErrorCodes, LangString};
use crate::html::markdown::{self, ErrorCodes, LangString, Ignore};
#[derive(Clone, Default)]
pub struct TestOptions {
@ -57,6 +58,7 @@ pub fn run(options: Options) -> i32 {
..config::basic_debugging_options()
},
edition: options.edition,
target_triple: options.target.clone(),
..config::Options::default()
};
@ -82,6 +84,7 @@ pub fn run(options: Options) -> i32 {
let mut opts = scrape_test_config(lower_to_hir.peek().0.borrow().krate());
opts.display_warnings |= options.display_warnings;
let enable_per_target_ignores = options.enable_per_target_ignores;
let mut collector = Collector::new(
compiler.crate_name()?.peek().to_string(),
options,
@ -89,6 +92,7 @@ pub fn run(options: Options) -> i32 {
opts,
Some(compiler.source_map().clone()),
None,
enable_per_target_ignores,
);
let mut global_ctxt = compiler.global_ctxt()?.take();
@ -181,6 +185,9 @@ fn run_test(
should_panic: bool,
no_run: bool,
as_test_harness: bool,
runtool: Option<String>,
runtool_args: Vec<String>,
target: TargetTriple,
compile_fail: bool,
mut error_codes: Vec<String>,
opts: &TestOptions,
@ -270,6 +277,7 @@ fn run_test(
if no_run {
compiler.arg("--emit=metadata");
}
compiler.arg("--target").arg(target.to_string());
compiler.arg("-");
compiler.stdin(Stdio::piped());
@ -315,7 +323,15 @@ fn run_test(
}
// Run the code!
let mut cmd = Command::new(output_file);
let mut cmd;
if let Some(tool) = runtool {
cmd = Command::new(tool);
cmd.arg(output_file);
cmd.args(runtool_args);
} else {
cmd = Command::new(output_file);
}
match cmd.output() {
Err(e) => return Err(TestFailure::ExecutionError(e)),
@ -603,6 +619,7 @@ pub struct Collector {
options: Options,
use_headers: bool,
enable_per_target_ignores: bool,
cratename: String,
opts: TestOptions,
position: Span,
@ -612,12 +629,14 @@ pub struct Collector {
impl Collector {
pub fn new(cratename: String, options: Options, use_headers: bool, opts: TestOptions,
source_map: Option<Lrc<SourceMap>>, filename: Option<PathBuf>,) -> Collector {
source_map: Option<Lrc<SourceMap>>, filename: Option<PathBuf>,
enable_per_target_ignores: bool) -> Collector {
Collector {
tests: Vec::new(),
names: Vec::new(),
options,
use_headers,
enable_per_target_ignores,
cratename,
opts,
position: DUMMY_SP,
@ -661,12 +680,22 @@ impl Tester for Collector {
let opts = self.opts.clone();
let edition = config.edition.unwrap_or(self.options.edition.clone());
let options = self.options.clone();
let runtool = self.options.runtool.clone();
let runtool_args = self.options.runtool_args.clone();
let target = self.options.target.clone();
let target_str = target.to_string();
debug!("creating test {}: {}", name, test);
self.tests.push(testing::TestDescAndFn {
desc: testing::TestDesc {
name: testing::DynTestName(name),
ignore: config.ignore,
name: testing::DynTestName(name.clone()),
ignore: match config.ignore {
Ignore::All => true,
Ignore::None => false,
Ignore::Some(ref ignores) => {
ignores.iter().any(|s| target_str.contains(s))
},
},
// compiler failures are test failures
should_panic: testing::ShouldPanic::No,
allow_fail: config.allow_fail,
@ -681,6 +710,9 @@ impl Tester for Collector {
config.should_panic,
config.no_run,
config.test_harness,
runtool,
runtool_args,
target,
config.compile_fail,
config.error_codes,
&opts,
@ -827,7 +859,10 @@ impl<'a, 'hir> HirCollector<'a, 'hir> {
// anything else, this will combine them for us.
if let Some(doc) = attrs.collapsed_doc_value() {
self.collector.set_position(attrs.span.unwrap_or(DUMMY_SP));
markdown::find_testable_code(&doc, self.collector, self.codes);
markdown::find_testable_code(&doc,
self.collector,
self.codes,
self.collector.enable_per_target_ignores);
}
nested(self);