Skip to content

Commit c5e8f58

Browse files
committed
Move book tests and error-index into their own book_tests module
1 parent 7e31dac commit c5e8f58

File tree

2 files changed

+331
-310
lines changed

2 files changed

+331
-310
lines changed
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
//! Collection of book-related tests.
2+
//!
3+
//! There are two general categories here:
4+
//! 1. Tests that try to run documentation tests (through `mdbook test` via `rustdoc`, or via
5+
//! `rustdoc` on individual markdown files directly).
6+
//! 2. `src/tools/error_index_generator` is special in that its test step involves building a
7+
//! suitably-staged sysroot (which matches what is ordinarily used to build `rustdoc`) to run
8+
//! tests in the error index.
9+
10+
use std::collections::HashSet;
11+
use std::ffi::{OsStr, OsString};
12+
use std::path::{Path, PathBuf};
13+
use std::{env, fs, iter};
14+
15+
use crate::core::build_steps::compile::{self, run_cargo};
16+
use crate::core::build_steps::tool::{self, SourceType, Tool};
17+
use crate::core::build_steps::toolstate::ToolState;
18+
use crate::core::builder::{Builder, Compiler, Kind, RunConfig, ShouldRun, Step};
19+
use crate::utils::build_stamp::BuildStamp;
20+
use crate::utils::helpers;
21+
use crate::{Mode, t};
22+
23+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24+
struct BookTest {
25+
compiler: Compiler,
26+
path: PathBuf,
27+
name: &'static str,
28+
is_ext_doc: bool,
29+
dependencies: Vec<&'static str>,
30+
}
31+
32+
impl Step for BookTest {
33+
type Output = ();
34+
const ONLY_HOSTS: bool = true;
35+
36+
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
37+
run.never()
38+
}
39+
40+
/// Runs the documentation tests for a book in `src/doc`.
41+
///
42+
/// This uses the `rustdoc` that sits next to `compiler`.
43+
fn run(self, builder: &Builder<'_>) {
44+
// External docs are different from local because:
45+
// - Some books need pre-processing by mdbook before being tested.
46+
// - They need to save their state to toolstate.
47+
// - They are only tested on the "checktools" builders.
48+
//
49+
// The local docs are tested by default, and we don't want to pay the cost of building
50+
// mdbook, so they use `rustdoc --test` directly. Also, the unstable book is special because
51+
// SUMMARY.md is generated, so it is easier to just run `rustdoc` on its files.
52+
if self.is_ext_doc {
53+
self.run_ext_doc(builder);
54+
} else {
55+
self.run_local_doc(builder);
56+
}
57+
}
58+
}
59+
60+
impl BookTest {
61+
/// This runs the equivalent of `mdbook test` (via the rustbook wrapper) which in turn runs
62+
/// `rustdoc --test` on each file in the book.
63+
fn run_ext_doc(self, builder: &Builder<'_>) {
64+
let compiler = self.compiler;
65+
66+
builder.ensure(compile::Std::new(compiler, compiler.host));
67+
68+
// mdbook just executes a binary named "rustdoc", so we need to update PATH so that it
69+
// points to our rustdoc.
70+
let mut rustdoc_path = builder.rustdoc(compiler);
71+
rustdoc_path.pop();
72+
let old_path = env::var_os("PATH").unwrap_or_default();
73+
let new_path = env::join_paths(iter::once(rustdoc_path).chain(env::split_paths(&old_path)))
74+
.expect("could not add rustdoc to PATH");
75+
76+
let mut rustbook_cmd = builder.tool_cmd(Tool::Rustbook);
77+
let path = builder.src.join(&self.path);
78+
// Books often have feature-gated example text.
79+
rustbook_cmd.env("RUSTC_BOOTSTRAP", "1");
80+
rustbook_cmd.env("PATH", new_path).arg("test").arg(path);
81+
82+
// Books may also need to build dependencies. For example, `TheBook` has code samples which
83+
// use the `trpl` crate. For the `rustdoc` invocation to find them them successfully, they
84+
// need to be built first and their paths used to generate the
85+
let libs = if !self.dependencies.is_empty() {
86+
let mut lib_paths = vec![];
87+
for dep in self.dependencies {
88+
let mode = Mode::ToolRustc;
89+
let target = builder.config.build;
90+
let cargo = tool::prepare_tool_cargo(
91+
builder,
92+
compiler,
93+
mode,
94+
target,
95+
Kind::Build,
96+
dep,
97+
SourceType::Submodule,
98+
&[],
99+
);
100+
101+
let stamp = BuildStamp::new(&builder.cargo_out(compiler, mode, target))
102+
.with_prefix(PathBuf::from(dep).file_name().and_then(|v| v.to_str()).unwrap());
103+
104+
let output_paths = run_cargo(builder, cargo, vec![], &stamp, vec![], false, false);
105+
let directories = output_paths
106+
.into_iter()
107+
.filter_map(|p| p.parent().map(ToOwned::to_owned))
108+
.fold(HashSet::new(), |mut set, dir| {
109+
set.insert(dir);
110+
set
111+
});
112+
113+
lib_paths.extend(directories);
114+
}
115+
lib_paths
116+
} else {
117+
vec![]
118+
};
119+
120+
if !libs.is_empty() {
121+
let paths = libs
122+
.into_iter()
123+
.map(|path| path.into_os_string())
124+
.collect::<Vec<OsString>>()
125+
.join(OsStr::new(","));
126+
rustbook_cmd.args([OsString::from("--library-path"), paths]);
127+
}
128+
129+
builder.add_rust_test_threads(&mut rustbook_cmd);
130+
let _guard = builder.msg(
131+
Kind::Test,
132+
compiler.stage,
133+
format_args!("mdbook {}", self.path.display()),
134+
compiler.host,
135+
compiler.host,
136+
);
137+
let _time = helpers::timeit(builder);
138+
let toolstate = if rustbook_cmd.delay_failure().run(builder) {
139+
ToolState::TestPass
140+
} else {
141+
ToolState::TestFail
142+
};
143+
builder.save_toolstate(self.name, toolstate);
144+
}
145+
146+
/// This runs `rustdoc --test` on all `.md` files in the path.
147+
fn run_local_doc(self, builder: &Builder<'_>) {
148+
let compiler = self.compiler;
149+
let host = self.compiler.host;
150+
151+
builder.ensure(compile::Std::new(compiler, host));
152+
153+
let _guard =
154+
builder.msg(Kind::Test, compiler.stage, format!("book {}", self.name), host, host);
155+
156+
// Do a breadth-first traversal of the `src/doc` directory and just run tests for all files
157+
// that end in `*.md`
158+
let mut stack = vec![builder.src.join(self.path)];
159+
let _time = helpers::timeit(builder);
160+
let mut files = Vec::new();
161+
while let Some(p) = stack.pop() {
162+
if p.is_dir() {
163+
stack.extend(t!(p.read_dir()).map(|p| t!(p).path()));
164+
continue;
165+
}
166+
167+
if p.extension().and_then(|s| s.to_str()) != Some("md") {
168+
continue;
169+
}
170+
171+
files.push(p);
172+
}
173+
174+
files.sort();
175+
176+
for file in files {
177+
markdown_test(builder, compiler, &file);
178+
}
179+
}
180+
}
181+
182+
macro_rules! test_book {
183+
($(
184+
$name:ident, $path:expr, $book_name:expr,
185+
default=$default:expr
186+
$(,submodules = $submodules:expr)?
187+
$(,dependencies=$dependencies:expr)?
188+
;
189+
)+) => {
190+
$(
191+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
192+
pub struct $name {
193+
compiler: Compiler,
194+
}
195+
196+
impl Step for $name {
197+
type Output = ();
198+
const DEFAULT: bool = $default;
199+
const ONLY_HOSTS: bool = true;
200+
201+
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
202+
run.path($path)
203+
}
204+
205+
fn make_run(run: RunConfig<'_>) {
206+
run.builder.ensure($name {
207+
compiler: run.builder.compiler(run.builder.top_stage, run.target),
208+
});
209+
}
210+
211+
fn run(self, builder: &Builder<'_>) {
212+
$(
213+
for submodule in $submodules {
214+
builder.require_submodule(submodule, None);
215+
}
216+
)*
217+
218+
let dependencies = vec![];
219+
$(
220+
let mut dependencies = dependencies;
221+
for dep in $dependencies {
222+
dependencies.push(dep);
223+
}
224+
)?
225+
226+
builder.ensure(BookTest {
227+
compiler: self.compiler,
228+
path: PathBuf::from($path),
229+
name: $book_name,
230+
is_ext_doc: !$default,
231+
dependencies,
232+
});
233+
}
234+
}
235+
)+
236+
}
237+
}
238+
239+
test_book!(
240+
Nomicon, "src/doc/nomicon", "nomicon", default=false, submodules=["src/doc/nomicon"];
241+
Reference, "src/doc/reference", "reference", default=false, submodules=["src/doc/reference"];
242+
RustdocBook, "src/doc/rustdoc", "rustdoc", default=true;
243+
RustcBook, "src/doc/rustc", "rustc", default=true;
244+
RustByExample, "src/doc/rust-by-example", "rust-by-example", default=false, submodules=["src/doc/rust-by-example"];
245+
EmbeddedBook, "src/doc/embedded-book", "embedded-book", default=false, submodules=["src/doc/embedded-book"];
246+
TheBook, "src/doc/book", "book", default=false, submodules=["src/doc/book"], dependencies=["src/doc/book/packages/trpl"];
247+
UnstableBook, "src/doc/unstable-book", "unstable-book", default=true;
248+
EditionGuide, "src/doc/edition-guide", "edition-guide", default=false, submodules=["src/doc/edition-guide"];
249+
);
250+
251+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
252+
pub struct ErrorIndex {
253+
compiler: Compiler,
254+
}
255+
256+
impl Step for ErrorIndex {
257+
type Output = ();
258+
const DEFAULT: bool = true;
259+
const ONLY_HOSTS: bool = true;
260+
261+
fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
262+
// Also add `error-index` here since that is what appears in the error message
263+
// when this fails.
264+
run.path("src/tools/error_index_generator").alias("error-index")
265+
}
266+
267+
fn make_run(run: RunConfig<'_>) {
268+
// error_index_generator depends on librustdoc. Use the compiler that is normally used to
269+
// build rustdoc for other tests (like compiletest tests in tests/rustdoc) so that it shares
270+
// the same artifacts.
271+
let compiler = run.builder.compiler(run.builder.top_stage, run.builder.config.build);
272+
run.builder.ensure(ErrorIndex { compiler });
273+
}
274+
275+
/// Runs the error index generator tool to execute the tests located in the error index.
276+
///
277+
/// The `error_index_generator` tool lives in `src/tools` and is used to generate a markdown
278+
/// file from the error indexes of the code base which is then passed to `rustdoc --test`.
279+
fn run(self, builder: &Builder<'_>) {
280+
let compiler = self.compiler;
281+
282+
let dir = builder.test_out(compiler.host);
283+
t!(fs::create_dir_all(&dir));
284+
let output = dir.join("error-index.md");
285+
286+
let mut tool = tool::ErrorIndex::command(builder);
287+
tool.arg("markdown").arg(&output);
288+
289+
let guard =
290+
builder.msg(Kind::Test, compiler.stage, "error-index", compiler.host, compiler.host);
291+
let _time = helpers::timeit(builder);
292+
tool.run_capture(builder);
293+
drop(guard);
294+
// The tests themselves need to link to std, so make sure it is available.
295+
builder.ensure(compile::Std::new(compiler, compiler.host));
296+
markdown_test(builder, compiler, &output);
297+
}
298+
}
299+
300+
fn markdown_test(builder: &Builder<'_>, compiler: Compiler, markdown: &Path) -> bool {
301+
if let Ok(contents) = fs::read_to_string(markdown) {
302+
if !contents.contains("```") {
303+
return true;
304+
}
305+
}
306+
307+
builder.verbose(|| println!("doc tests for: {}", markdown.display()));
308+
let mut cmd = builder.rustdoc_cmd(compiler);
309+
builder.add_rust_test_threads(&mut cmd);
310+
// allow for unstable options such as new editions
311+
cmd.arg("-Z");
312+
cmd.arg("unstable-options");
313+
cmd.arg("--test");
314+
cmd.arg(markdown);
315+
cmd.env("RUSTC_BOOTSTRAP", "1");
316+
317+
let test_args = builder.config.test_args().join(" ");
318+
cmd.arg("--test-args").arg(test_args);
319+
320+
cmd = cmd.delay_failure();
321+
if !builder.config.verbose_tests {
322+
cmd.run_capture(builder).is_success()
323+
} else {
324+
cmd.run(builder)
325+
}
326+
}

0 commit comments

Comments
 (0)