rustdoc/doctest/
markdown.rs1use std::fs::read_to_string;
4use std::sync::{Arc, Mutex};
5
6use rustc_errors::DiagCtxtHandle;
7use rustc_session::config::Input;
8use rustc_span::source_map::FilePathMapping;
9use rustc_span::{DUMMY_SP, FileName, RealFileName};
10use tempfile::tempdir;
11
12use super::{
13 CreateRunnableDocTests, DocTestVisitor, GlobalTestOptions, ScrapedDocTest, generate_args_file,
14};
15use crate::config::Options;
16use crate::html::markdown::{
17 CodeBlockPosition, CodeLineMapping, ErrorCodes, LangString, find_testable_code,
18};
19
20struct MdCollector {
21 tests: Vec<ScrapedDocTest>,
22 cur_path: Vec<String>,
23 filename: FileName,
24}
25
26impl DocTestVisitor for MdCollector {
27 fn visit_test(
28 &mut self,
29 test: String,
30 config: LangString,
31 position: CodeBlockPosition,
32 code_mappings: Vec<CodeLineMapping>,
33 ) {
34 let filename = self.filename.clone();
35 let line = match position {
36 CodeBlockPosition::Relative(rel_line) => {
37 1 + rel_line.offset()
39 }
40 CodeBlockPosition::Span(_) => {
41 rustc_span::bug!("markdown collector doesn't produce spans")
42 }
43 };
44 self.tests.push(ScrapedDocTest::new(
45 filename,
46 line,
47 self.cur_path.clone(),
48 config,
49 test,
50 DUMMY_SP,
51 code_mappings,
52 Vec::new(),
53 ));
54 }
55
56 fn visit_header(&mut self, name: &str, level: u32) {
57 let name = name
60 .chars()
61 .enumerate()
62 .map(|(i, c)| {
63 if (i == 0 && rustc_lexer::is_id_start(c))
64 || (i != 0 && rustc_lexer::is_id_continue(c))
65 {
66 c
67 } else {
68 '_'
69 }
70 })
71 .collect::<String>();
72
73 let level = level as usize;
78 if level <= self.cur_path.len() {
79 self.cur_path.truncate(level);
84 self.cur_path[level - 1] = name;
85 } else {
86 if level - 1 > self.cur_path.len() {
91 self.cur_path.resize(level - 1, "_".to_owned());
92 }
93 self.cur_path.push(name);
94 }
95 }
96}
97
98pub(crate) fn test(input: &Input, options: Options, dcx: DiagCtxtHandle<'_>) -> Result<(), String> {
100 let input_str = match input {
101 Input::File(path) => {
102 read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?
103 }
104 Input::Str { name: _, input } | Input::DocTestStr { input, .. } => input.clone(),
105 };
106
107 let crate_name = input.filestem().to_string();
109 let temp_dir =
110 tempdir().map_err(|error| format!("failed to create temporary directory: {error:?}"))?;
111 let args_file = temp_dir.path().join("rustdoc-cfgs");
112 generate_args_file(&args_file, &options)?;
113
114 let opts = GlobalTestOptions {
115 crate_name,
116 no_crate_inject: true,
117 insert_indent_space: false,
118 args_file,
119 };
120
121 let mut md_collector = MdCollector {
122 tests: vec![],
123 cur_path: vec![],
124 filename: input
125 .opt_path()
126 .map(|f| {
127 let file_mapping = FilePathMapping::empty();
130 FileName::Real(file_mapping.to_real_filename(&RealFileName::empty(), f))
131 })
132 .unwrap_or(FileName::Custom("input".to_owned())),
133 };
134 let codes = ErrorCodes::from(options.unstable_features.is_nightly_build());
135
136 find_testable_code(&input_str, &mut md_collector, codes, None);
137
138 let mut collector = CreateRunnableDocTests::new(options.clone(), opts);
139 md_collector.tests.into_iter().for_each(|t| collector.add_test(t, None));
140 let CreateRunnableDocTests { opts, rustdoc_options, standalone_tests, mergeable_tests, .. } =
141 collector;
142 crate::doctest::run_tests(
143 dcx,
144 opts,
145 &rustdoc_options,
146 &Arc::new(Mutex::new(Vec::new())),
147 standalone_tests,
148 mergeable_tests,
149 None,
150 );
151 Ok(())
152}