blob: a0d39ce749d8350db00af8578db80711a53b1fb8 [file] [log] [blame] [edit]
//! Doctest functionality used only for doctests in `.md` Markdown files.
use std::fs::read_to_string;
use std::sync::{Arc, Mutex};
use rustc_session::config::Input;
use rustc_span::FileName;
use tempfile::tempdir;
use super::{
CreateRunnableDocTests, DocTestVisitor, GlobalTestOptions, ScrapedDocTest, generate_args_file,
};
use crate::config::Options;
use crate::html::markdown::{ErrorCodes, LangString, MdRelLine, find_testable_code};
struct MdCollector {
tests: Vec<ScrapedDocTest>,
cur_path: Vec<String>,
filename: FileName,
}
impl DocTestVisitor for MdCollector {
fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine) {
let filename = self.filename.clone();
// First line of Markdown is line 1.
let line = 1 + rel_line.offset();
self.tests.push(ScrapedDocTest::new(filename, line, self.cur_path.clone(), config, test));
}
fn visit_header(&mut self, name: &str, level: u32) {
// We use these headings as test names, so it's good if
// they're valid identifiers.
let name = name
.chars()
.enumerate()
.map(|(i, c)| {
if (i == 0 && rustc_lexer::is_id_start(c))
|| (i != 0 && rustc_lexer::is_id_continue(c))
{
c
} else {
'_'
}
})
.collect::<String>();
// Here we try to efficiently assemble the header titles into the
// test name in the form of `h1::h2::h3::h4::h5::h6`.
//
// Suppose that originally `self.cur_path` contains `[h1, h2, h3]`...
let level = level as usize;
if level <= self.cur_path.len() {
// ... Consider `level == 2`. All headers in the lower levels
// are irrelevant in this new level. So we should reset
// `self.names` to contain headers until <h2>, and replace that
// slot with the new name: `[h1, name]`.
self.cur_path.truncate(level);
self.cur_path[level - 1] = name;
} else {
// ... On the other hand, consider `level == 5`. This means we
// need to extend `self.names` to contain five headers. We fill
// in the missing level (<h4>) with `_`. Thus `self.names` will
// become `[h1, h2, h3, "_", name]`.
if level - 1 > self.cur_path.len() {
self.cur_path.resize(level - 1, "_".to_owned());
}
self.cur_path.push(name);
}
}
}
/// Runs any tests/code examples in the markdown file `options.input`.
pub(crate) fn test(input: &Input, options: Options) -> Result<(), String> {
let input_str = match input {
Input::File(path) => {
read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?
}
Input::Str { name: _, input } => input.clone(),
};
// Obviously not a real crate name, but close enough for purposes of doctests.
let crate_name = input.filestem().to_string();
let temp_dir =
tempdir().map_err(|error| format!("failed to create temporary directory: {error:?}"))?;
let args_file = temp_dir.path().join("rustdoc-cfgs");
generate_args_file(&args_file, &options)?;
let opts = GlobalTestOptions {
crate_name,
no_crate_inject: true,
insert_indent_space: false,
attrs: vec![],
args_file,
};
let mut md_collector = MdCollector {
tests: vec![],
cur_path: vec![],
filename: input
.opt_path()
.map(ToOwned::to_owned)
.map(FileName::from)
.unwrap_or(FileName::Custom("input".to_owned())),
};
let codes = ErrorCodes::from(options.unstable_features.is_nightly_build());
find_testable_code(
&input_str,
&mut md_collector,
codes,
options.enable_per_target_ignores,
None,
);
let mut collector = CreateRunnableDocTests::new(options.clone(), opts);
md_collector.tests.into_iter().for_each(|t| collector.add_test(t));
let CreateRunnableDocTests { opts, rustdoc_options, standalone_tests, mergeable_tests, .. } =
collector;
crate::doctest::run_tests(
opts,
&rustdoc_options,
&Arc::new(Mutex::new(Vec::new())),
standalone_tests,
mergeable_tests,
);
Ok(())
}