Collect and use #![doc(test(attr(..)))] at module level too

This commit is contained in:
Urgau 2025-05-01 18:15:04 +02:00
parent 80c6a08850
commit 9d9705f4c3
13 changed files with 229 additions and 52 deletions

View file

@ -45,8 +45,6 @@ pub(crate) struct GlobalTestOptions {
/// Whether inserting extra indent spaces in code block,
/// default is `false`, only `true` for generating code link of Rust playground
pub(crate) insert_indent_space: bool,
/// Additional crate-level attributes to add to doctests.
pub(crate) attrs: Vec<String>,
/// Path to file containing arguments for the invocation of rustc.
pub(crate) args_file: PathBuf,
}
@ -371,12 +369,9 @@ fn scrape_test_config(
attrs: &[hir::Attribute],
args_file: PathBuf,
) -> GlobalTestOptions {
use rustc_ast_pretty::pprust;
let mut opts = GlobalTestOptions {
crate_name,
no_crate_inject: false,
attrs: Vec::new(),
insert_indent_space: false,
args_file,
};
@ -393,13 +388,7 @@ fn scrape_test_config(
if attr.has_name(sym::no_crate_inject) {
opts.no_crate_inject = true;
}
if attr.has_name(sym::attr)
&& let Some(l) = attr.meta_item_list()
{
for item in l {
opts.attrs.push(pprust::meta_list_item_to_string(item));
}
}
// NOTE: `test(attr(..))` is handled when discovering the individual tests
}
opts
@ -848,6 +837,7 @@ pub(crate) struct ScrapedDocTest {
text: String,
name: String,
span: Span,
global_crate_attrs: Vec<String>,
}
impl ScrapedDocTest {
@ -858,6 +848,7 @@ impl ScrapedDocTest {
langstr: LangString,
text: String,
span: Span,
global_crate_attrs: Vec<String>,
) -> Self {
let mut item_path = logical_path.join("::");
item_path.retain(|c| c != ' ');
@ -867,7 +858,7 @@ impl ScrapedDocTest {
let name =
format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly());
Self { filename, line, langstr, text, name, span }
Self { filename, line, langstr, text, name, span, global_crate_attrs }
}
fn edition(&self, opts: &RustdocOptions) -> Edition {
self.langstr.edition.unwrap_or(opts.edition)
@ -949,6 +940,7 @@ impl CreateRunnableDocTests {
let edition = scraped_test.edition(&self.rustdoc_options);
let doctest = BuildDocTestBuilder::new(&scraped_test.text)
.crate_name(&self.opts.crate_name)
.global_crate_attrs(scraped_test.global_crate_attrs.clone())
.edition(edition)
.can_merge_doctests(self.can_merge_doctests)
.test_id(test_id)

View file

@ -35,13 +35,16 @@ impl ExtractedDocTests {
) {
let edition = scraped_test.edition(options);
let ScrapedDocTest { filename, line, langstr, text, name, .. } = scraped_test;
let ScrapedDocTest { filename, line, langstr, text, name, global_crate_attrs, .. } =
scraped_test;
let doctest = BuildDocTestBuilder::new(&text)
.crate_name(&opts.crate_name)
.global_crate_attrs(global_crate_attrs)
.edition(edition)
.lang_str(&langstr)
.build(None);
let (full_test_code, size) = doctest.generate_unique_doctest(
&text,
langstr.test_harness,

View file

@ -45,6 +45,7 @@ pub(crate) struct BuildDocTestBuilder<'a> {
test_id: Option<String>,
lang_str: Option<&'a LangString>,
span: Span,
global_crate_attrs: Vec<String>,
}
impl<'a> BuildDocTestBuilder<'a> {
@ -57,6 +58,7 @@ impl<'a> BuildDocTestBuilder<'a> {
test_id: None,
lang_str: None,
span: DUMMY_SP,
global_crate_attrs: Vec::new(),
}
}
@ -96,6 +98,12 @@ impl<'a> BuildDocTestBuilder<'a> {
self
}
#[inline]
pub(crate) fn global_crate_attrs(mut self, global_crate_attrs: Vec<String>) -> Self {
self.global_crate_attrs = global_crate_attrs;
self
}
pub(crate) fn build(self, dcx: Option<DiagCtxtHandle<'_>>) -> DocTestBuilder {
let BuildDocTestBuilder {
source,
@ -106,6 +114,7 @@ impl<'a> BuildDocTestBuilder<'a> {
test_id,
lang_str,
span,
global_crate_attrs,
} = self;
let can_merge_doctests = can_merge_doctests
&& lang_str.is_some_and(|lang_str| {
@ -133,6 +142,7 @@ impl<'a> BuildDocTestBuilder<'a> {
// If the AST returned an error, we don't want this doctest to be merged with the
// others.
return DocTestBuilder::invalid(
Vec::new(),
String::new(),
String::new(),
String::new(),
@ -155,6 +165,7 @@ impl<'a> BuildDocTestBuilder<'a> {
DocTestBuilder {
supports_color,
has_main_fn,
global_crate_attrs,
crate_attrs,
maybe_crate_attrs,
crates,
@ -173,6 +184,7 @@ pub(crate) struct DocTestBuilder {
pub(crate) supports_color: bool,
pub(crate) already_has_extern_crate: bool,
pub(crate) has_main_fn: bool,
pub(crate) global_crate_attrs: Vec<String>,
pub(crate) crate_attrs: String,
/// If this is a merged doctest, it will be put into `everything_else`, otherwise it will
/// put into `crate_attrs`.
@ -186,6 +198,7 @@ pub(crate) struct DocTestBuilder {
impl DocTestBuilder {
fn invalid(
global_crate_attrs: Vec<String>,
crate_attrs: String,
maybe_crate_attrs: String,
crates: String,
@ -195,6 +208,7 @@ impl DocTestBuilder {
Self {
supports_color: false,
has_main_fn: false,
global_crate_attrs,
crate_attrs,
maybe_crate_attrs,
crates,
@ -224,7 +238,8 @@ impl DocTestBuilder {
let mut line_offset = 0;
let mut prog = String::new();
let everything_else = self.everything_else.trim();
if opts.attrs.is_empty() {
if self.global_crate_attrs.is_empty() {
// If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
// lints that are commonly triggered in doctests. The crate-level test attributes are
// commonly used to make tests fail in case they trigger warnings, so having this there in
@ -233,8 +248,8 @@ impl DocTestBuilder {
line_offset += 1;
}
// Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
for attr in &opts.attrs {
// Next, any attributes that came from #![doc(test(attr(...)))].
for attr in &self.global_crate_attrs {
prog.push_str(&format!("#![{attr}]\n"));
line_offset += 1;
}

View file

@ -31,6 +31,7 @@ impl DocTestVisitor for MdCollector {
config,
test,
DUMMY_SP,
Vec::new(),
));
}
@ -96,7 +97,6 @@ pub(crate) fn test(input: &Input, options: Options) -> Result<(), String> {
crate_name,
no_crate_inject: true,
insert_indent_space: false,
attrs: vec![],
args_file,
};

View file

@ -12,6 +12,7 @@ use crate::html::markdown::{Ignore, LangString};
/// Convenient type to merge compatible doctests into one.
pub(crate) struct DocTestRunner {
crate_attrs: FxIndexSet<String>,
global_crate_attrs: FxIndexSet<String>,
ids: String,
output: String,
output_merged_tests: String,
@ -23,6 +24,7 @@ impl DocTestRunner {
pub(crate) fn new() -> Self {
Self {
crate_attrs: FxIndexSet::default(),
global_crate_attrs: FxIndexSet::default(),
ids: String::new(),
output: String::new(),
output_merged_tests: String::new(),
@ -46,6 +48,9 @@ impl DocTestRunner {
for line in doctest.crate_attrs.split('\n') {
self.crate_attrs.insert(line.to_string());
}
for line in &doctest.global_crate_attrs {
self.global_crate_attrs.insert(line.to_string());
}
}
self.ids.push_str(&format!(
"tests.push({}::TEST);\n",
@ -85,7 +90,7 @@ impl DocTestRunner {
code_prefix.push('\n');
}
if opts.attrs.is_empty() {
if self.global_crate_attrs.is_empty() {
// If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
// lints that are commonly triggered in doctests. The crate-level test attributes are
// commonly used to make tests fail in case they trigger warnings, so having this there in
@ -93,8 +98,8 @@ impl DocTestRunner {
code_prefix.push_str("#![allow(unused)]\n");
}
// Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
for attr in &opts.attrs {
// Next, any attributes that came from #![doc(test(attr(...)))].
for attr in &self.global_crate_attrs {
code_prefix.push_str(&format!("#![{attr}]\n"));
}

View file

@ -4,6 +4,7 @@ use std::cell::Cell;
use std::env;
use std::sync::Arc;
use rustc_ast_pretty::pprust;
use rustc_data_structures::fx::FxHashSet;
use rustc_hir::def_id::{CRATE_DEF_ID, LocalDefId};
use rustc_hir::{self as hir, CRATE_HIR_ID, intravisit};
@ -11,7 +12,7 @@ use rustc_middle::hir::nested_filter;
use rustc_middle::ty::TyCtxt;
use rustc_resolve::rustdoc::span_of_fragments;
use rustc_span::source_map::SourceMap;
use rustc_span::{BytePos, DUMMY_SP, FileName, Pos, Span};
use rustc_span::{BytePos, DUMMY_SP, FileName, Pos, Span, sym};
use super::{DocTestVisitor, ScrapedDocTest};
use crate::clean::{Attributes, extract_cfg_from_attrs};
@ -22,6 +23,7 @@ struct RustCollector {
tests: Vec<ScrapedDocTest>,
cur_path: Vec<String>,
position: Span,
global_crate_attrs: Vec<String>,
}
impl RustCollector {
@ -75,6 +77,7 @@ impl DocTestVisitor for RustCollector {
config,
test,
span,
self.global_crate_attrs.clone(),
));
}
@ -94,6 +97,7 @@ impl<'tcx> HirCollector<'tcx> {
cur_path: vec![],
position: DUMMY_SP,
tests: vec![],
global_crate_attrs: Vec::new(),
};
Self { codes, tcx, collector }
}
@ -170,6 +174,40 @@ impl<'tcx> intravisit::Visitor<'tcx> for HirCollector<'tcx> {
self.tcx
}
fn visit_mod(&mut self, m: &'tcx hir::Mod<'tcx>, _s: Span, hir_id: hir::HirId) {
let attrs = self.tcx.hir_attrs(hir_id);
if !attrs.is_empty() {
// Try collecting `#![doc(test(attr(...)))]` from the attribute module
let old_len = self.collector.global_crate_attrs.len();
for doc_test_attrs in attrs
.iter()
.filter(|a| a.has_name(sym::doc))
.flat_map(|a| a.meta_item_list().unwrap_or_default())
.filter(|a| a.has_name(sym::test))
{
let Some(doc_test_attrs) = doc_test_attrs.meta_item_list() else { continue };
for attr in doc_test_attrs
.iter()
.filter(|a| a.has_name(sym::attr))
.flat_map(|a| a.meta_item_list().unwrap_or_default())
.map(|i| pprust::meta_list_item_to_string(i))
{
// Add the additional attributes to the global_crate_attrs vector
self.collector.global_crate_attrs.push(attr);
}
}
let r = intravisit::walk_mod(self, m);
// Restore global_crate_attrs to it's previous size/content
self.collector.global_crate_attrs.truncate(old_len);
r
} else {
intravisit::walk_mod(self, m)
}
}
fn visit_item(&mut self, item: &'tcx hir::Item<'_>) {
let name = match &item.kind {
hir::ItemKind::Impl(impl_) => {

View file

@ -7,9 +7,11 @@ fn make_test(
crate_name: Option<&str>,
dont_insert_main: bool,
opts: &GlobalTestOptions,
global_crate_attrs: Vec<&str>,
test_id: Option<&str>,
) -> (String, usize) {
let mut builder = BuildDocTestBuilder::new(test_code);
let mut builder = BuildDocTestBuilder::new(test_code)
.global_crate_attrs(global_crate_attrs.into_iter().map(|a| a.to_string()).collect());
if let Some(crate_name) = crate_name {
builder = builder.crate_name(crate_name);
}
@ -28,7 +30,6 @@ fn default_global_opts(crate_name: impl Into<String>) -> GlobalTestOptions {
crate_name: crate_name.into(),
no_crate_inject: false,
insert_indent_space: false,
attrs: vec![],
args_file: PathBuf::new(),
}
}
@ -43,7 +44,7 @@ fn main() {
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, None, false, &opts, None);
let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}
@ -58,7 +59,7 @@ fn main() {
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}
@ -77,7 +78,7 @@ use asdf::qwop;
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 3));
}
@ -94,7 +95,7 @@ use asdf::qwop;
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}
@ -112,7 +113,7 @@ use std::*;
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, Some("std"), false, &opts, None);
let (output, len) = make_test(input, Some("std"), false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}
@ -131,7 +132,7 @@ use asdf::qwop;
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}
@ -148,7 +149,7 @@ use asdf::qwop;
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}
@ -156,8 +157,7 @@ assert_eq!(2+2, 4);
fn make_test_opts_attrs() {
// If you supplied some doctest attributes with `#![doc(test(attr(...)))]`, it will use
// those instead of the stock `#![allow(unused)]`.
let mut opts = default_global_opts("asdf");
opts.attrs.push("feature(sick_rad)".to_string());
let opts = default_global_opts("asdf");
let input = "use asdf::qwop;
assert_eq!(2+2, 4);";
let expected = "#![feature(sick_rad)]
@ -168,11 +168,10 @@ use asdf::qwop;
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
let (output, len) =
make_test(input, Some("asdf"), false, &opts, vec!["feature(sick_rad)"], None);
assert_eq!((output, len), (expected, 3));
// Adding more will also bump the returned line offset.
opts.attrs.push("feature(hella_dope)".to_string());
let expected = "#![feature(sick_rad)]
#![feature(hella_dope)]
#[allow(unused_extern_crates)]
@ -182,7 +181,18 @@ use asdf::qwop;
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
let (output, len) = make_test(
input,
Some("asdf"),
false,
&opts,
vec![
"feature(sick_rad)",
// Adding more will also bump the returned line offset.
"feature(hella_dope)",
],
None,
);
assert_eq!((output, len), (expected, 4));
}
@ -200,7 +210,7 @@ fn main() {
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, None, false, &opts, None);
let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}
@ -216,7 +226,7 @@ fn main() {
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, None, false, &opts, None);
let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 1));
}
@ -232,7 +242,7 @@ fn main() {
assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, None, false, &opts, None);
let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}
@ -246,7 +256,7 @@ assert_eq!(2+2, 4);";
//Ceci n'est pas une `fn main`
assert_eq!(2+2, 4);"
.to_string();
let (output, len) = make_test(input, None, true, &opts, None);
let (output, len) = make_test(input, None, true, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 1));
}
@ -264,7 +274,7 @@ assert_eq!(2+2, 4);
}"
.to_string();
let (output, len) = make_test(input, None, false, &opts, None);
let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}
@ -284,7 +294,7 @@ assert_eq!(asdf::foo, 4);
}"
.to_string();
let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 3));
}
@ -302,7 +312,7 @@ test_wrapper! {
}"
.to_string();
let (output, len) = make_test(input, Some("my_crate"), false, &opts, None);
let (output, len) = make_test(input, Some("my_crate"), false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 1));
}
@ -322,7 +332,7 @@ io::stdin().read_line(&mut input)?;
Ok::<(), io:Error>(())
} _inner().unwrap() }"
.to_string();
let (output, len) = make_test(input, None, false, &opts, None);
let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}
@ -336,7 +346,7 @@ fn main() { #[allow(non_snake_case)] fn _doctest_main__some_unique_name() {
assert_eq!(2+2, 4);
} _doctest_main__some_unique_name() }"
.to_string();
let (output, len) = make_test(input, None, false, &opts, Some("_some_unique_name"));
let (output, len) = make_test(input, None, false, &opts, Vec::new(), Some("_some_unique_name"));
assert_eq!((output, len), (expected, 2));
}
@ -355,7 +365,7 @@ fn main() {
eprintln!(\"hello anan\");
}"
.to_string();
let (output, len) = make_test(input, None, false, &opts, None);
let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}
@ -375,7 +385,7 @@ fn main() {
eprintln!(\"hello anan\");
}"
.to_string();
let (output, len) = make_test(input, None, false, &opts, None);
let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 1));
}
@ -400,7 +410,7 @@ fn main() {
}"
.to_string();
let (output, len) = make_test(input, None, false, &opts, None);
let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
// And same, if there is a `main` function provided by the user, we ensure that it's
@ -420,7 +430,7 @@ fn main() {}";
fn main() {}"
.to_string();
let (output, len) = make_test(input, None, false, &opts, None);
let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 1));
}
@ -448,6 +458,6 @@ pub mod outer_module {
}
}"
.to_string();
let (output, len) = make_test(input, None, false, &opts, None);
let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
assert_eq!((output, len), (expected, 2));
}

View file

@ -300,7 +300,6 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
crate_name: krate.map(String::from).unwrap_or_default(),
no_crate_inject: false,
insert_indent_space: true,
attrs: vec![],
args_file: PathBuf::new(),
};
let mut builder = doctest::BuildDocTestBuilder::new(&test).edition(edition);

View file

@ -0,0 +1,27 @@
// Same test as dead-code-module but with 2 doc(test(attr())) at different levels.
//@ edition: 2024
//@ compile-flags:--test
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
//@ failure-status: 101
#![doc(test(attr(allow(unused_variables))))]
mod my_mod {
#![doc(test(attr(deny(warnings))))]
/// Example
///
/// ```rust,no_run
/// trait T { fn f(); }
/// ```
pub fn f() {}
}
/// Example
///
/// ```rust,no_run
/// trait OnlyWarning { fn no_deny_warnings(); }
/// ```
pub fn g() {}

View file

@ -0,0 +1,30 @@
running 2 tests
test $DIR/dead-code-module-2.rs - my_mod::f (line 16) - compile ... FAILED
test $DIR/dead-code-module-2.rs - g (line 24) - compile ... ok
failures:
---- $DIR/dead-code-module-2.rs - my_mod::f (line 16) stdout ----
error: trait `T` is never used
--> $DIR/dead-code-module-2.rs:17:7
|
LL | trait T { fn f(); }
| ^
|
note: the lint level is defined here
--> $DIR/dead-code-module-2.rs:15:9
|
LL | #![deny(warnings)]
| ^^^^^^^^
= note: `#[deny(dead_code)]` implied by `#[deny(warnings)]`
error: aborting due to 1 previous error
Couldn't compile the test.
failures:
$DIR/dead-code-module-2.rs - my_mod::f (line 16)
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME

View file

@ -0,0 +1,18 @@
// Same test as dead-code but inside a module.
//@ edition: 2024
//@ compile-flags:--test
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
//@ failure-status: 101
mod my_mod {
#![doc(test(attr(allow(unused_variables), deny(warnings))))]
/// Example
///
/// ```rust,no_run
/// trait T { fn f(); }
/// ```
pub fn f() {}
}

View file

@ -0,0 +1,29 @@
running 1 test
test $DIR/dead-code-module.rs - my_mod::f (line 14) - compile ... FAILED
failures:
---- $DIR/dead-code-module.rs - my_mod::f (line 14) stdout ----
error: trait `T` is never used
--> $DIR/dead-code-module.rs:15:7
|
LL | trait T { fn f(); }
| ^
|
note: the lint level is defined here
--> $DIR/dead-code-module.rs:13:9
|
LL | #![deny(warnings)]
| ^^^^^^^^
= note: `#[deny(dead_code)]` implied by `#[deny(warnings)]`
error: aborting due to 1 previous error
Couldn't compile the test.
failures:
$DIR/dead-code-module.rs - my_mod::f (line 14)
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME

View file

@ -0,0 +1,11 @@
//@ check-pass
#![crate_type = "lib"]
#![deny(invalid_doc_attributes)]
#![doc(test(no_crate_inject))]
mod my_mod {
#![doc(test(attr(deny(warnings))))]
pub fn foo() {}
}