1use crate::markdown::{CodeBlock, Doc};
7
8pub const GENERATED_HEADER: &str = "// @generated by `cargo soothfast docs gen-tests`";
11
12pub fn sanitized_stem(rel_path: &str) -> String {
14 let stem = rel_path.trim_end_matches(".md");
15 let san: String = stem
16 .chars()
17 .map(|c| {
18 if c.is_ascii_alphanumeric() {
19 c.to_ascii_lowercase()
20 } else {
21 '_'
22 }
23 })
24 .collect();
25 san.trim_matches('_').to_string()
26}
27
28fn indent(code: &str, pad: &str) -> String {
29 code.lines()
30 .map(|l| {
31 if l.is_empty() {
32 String::new()
33 } else {
34 format!("{pad}{l}")
35 }
36 })
37 .collect::<Vec<_>>()
38 .join("\n")
39}
40
41#[derive(Debug, Clone, PartialEq)]
43pub struct Generated {
44 pub path: String,
46 pub content: String,
48}
49
50const VERBATIM: &str =
55 "macro_rules! verbatim {\n ($($tokens:tt)*) => {\n $($tokens)*\n };\n}\n";
56
57pub const DOC_TESTS_DIR: &str = "tests/soothfast_docs";
59
60pub const OWNED: &[(&str, &str, Option<&str>)] = &[
67 ("tests", "soothfast_doc_", Some(DOC_TESTS_DIR)),
68 (DOC_TESTS_DIR, "", None),
69 ("examples", "soothfast_capture_", Some(CAPTURE_DIR)),
70 (CAPTURE_DIR, "", None),
71];
72
73pub fn recorded_source(content: &str) -> Option<(&str, Option<usize>)> {
76 let source = content.lines().nth(1)?.strip_prefix("// source: ")?;
77 match source.rsplit_once(" line ") {
78 Some((page, line)) => match line.parse::<usize>() {
79 Ok(line) => Some((page, Some(line))),
80 Err(_) => Some((source, None)),
81 },
82 None => Some((source, None)),
83 }
84}
85
86pub fn doc_test_set<'a>(
91 docs: impl IntoIterator<Item = (&'a str, &'a Doc)>,
92) -> Result<Vec<Generated>, String> {
93 let docs: Vec<(&str, &Doc)> = docs.into_iter().collect();
94 one_global_allocator(&docs, CodeBlock::is_testable_rust)?;
95 let mut pages: Vec<(String, &str, String)> = Vec::new();
96 for (rel_path, doc) in docs {
97 if let Some(content) = test_file(rel_path, doc) {
98 pages.push((
99 format!("doc_{}", sanitized_stem(rel_path)),
100 rel_path,
101 content,
102 ));
103 }
104 }
105 pages.sort_by(|a, b| a.1.cmp(b.1));
106 for (i, (module, path, _)) in pages.iter().enumerate() {
107 if let Some((_, other, _)) = pages[..i].iter().find(|(m, _, _)| m == module) {
108 return Err(format!(
109 "{other} and {path} both generate doc-test module `{module}`; rename one"
110 ));
111 }
112 }
113 if pages.is_empty() {
114 return Ok(Vec::new());
115 }
116 let mods: String = pages
117 .iter()
118 .map(|(m, _, _)| format!("mod {m};\n"))
119 .collect();
120 let main =
121 format!("{GENERATED_HEADER}\n#![allow(unused)]\n\n{VERBATIM}\nverbatim! {{\n{mods}}}\n");
122 let mut set = vec![Generated {
123 path: format!("{DOC_TESTS_DIR}/main.rs"),
124 content: main,
125 }];
126 set.extend(pages.into_iter().map(|(module, _, content)| Generated {
127 path: format!("{DOC_TESTS_DIR}/{module}.rs"),
128 content,
129 }));
130 Ok(set)
131}
132
133fn one_global_allocator(
136 docs: &[(&str, &Doc)],
137 in_target: fn(&CodeBlock) -> bool,
138) -> Result<(), String> {
139 let mut found = docs.iter().flat_map(|(page, doc)| {
140 doc.blocks
141 .iter()
142 .filter(move |b| in_target(b))
143 .filter(|b| b.code.contains("#[global_allocator]") || b.code.contains("bench_main!"))
144 .map(move |b| format!("{page} line {}", b.line))
145 });
146 match (found.next(), found.next()) {
147 (Some(first), Some(second)) => Err(format!(
148 "{first} and {second} both install a global allocator, and the blocks of a package compile into one crate; mark one `ignore` or `no_run` in another package"
149 )),
150 _ => Ok(()),
151 }
152}
153
154pub fn owning_target(rel_path: &str) -> Option<(&'static str, bool)> {
159 let (dir, name) = rel_path.rsplit_once('/').unwrap_or(("", rel_path));
160 if !name.ends_with(".rs") {
161 return None;
162 }
163 OWNED
164 .iter()
165 .find(|(d, prefix, _)| *d == dir && name.starts_with(prefix))
166 .map(|(dir, _, replaced_by)| (replaced_by.unwrap_or(dir), replaced_by.is_some()))
167}
168
169pub fn test_file(rel_path: &str, doc: &Doc) -> Option<String> {
172 let testable: Vec<&CodeBlock> = doc.blocks.iter().filter(|b| b.is_testable_rust()).collect();
173 if testable.is_empty() {
174 return None;
175 }
176 let mut out = format!("{GENERATED_HEADER}\n// source: {rel_path}\n#![allow(unused)]\n");
177 for block in testable {
178 let body = indent(&block.code, " ");
179 let call_main = if block.code.lines().any(|l| top_level_main(l).is_some()) {
181 "\n main();"
182 } else {
183 ""
184 };
185 let cfg = match block.feature_cfg() {
187 Some(pred) => format!("#[cfg({pred})]\n"),
188 None => String::new(),
189 };
190 if block.has_tag("no_run") {
194 out.push_str(&format!(
195 "\n// line {}: compile-only (no_run)\n{cfg}#[rustfmt::skip]\n#[allow(dead_code)]\nfn doc_block_line_{}() {{\n{body}\n}}\n",
196 block.line, block.line
197 ));
198 } else {
199 out.push_str(&format!(
200 "\n// line {}\n{cfg}#[rustfmt::skip]\n#[test]\nfn doc_block_line_{}() {{\n{body}{call_main}\n}}\n",
201 block.line, block.line
202 ));
203 }
204 }
205 Some(out)
206}
207
208pub const CAPTURE_DIR: &str = "examples/soothfast_capture";
210
211pub const CAPTURE_EXAMPLE: &str = "soothfast_capture";
213
214pub const CAPTURE_UNKNOWN: (i32, &str) = (2, "soothfast_capture: unknown block");
216
217pub const CAPTURE_DISABLED: (i32, &str) = (3, "soothfast_capture: disabled block");
220
221pub fn capture_ids(rel_path: &str, doc: &Doc) -> Vec<String> {
224 let stem = sanitized_stem(rel_path);
225 let count = doc.blocks.iter().filter(|b| b.is_capture()).count();
226 (1..=count).map(|n| format!("{stem}_{n}")).collect()
227}
228
229pub fn capture_set<'a>(
242 docs: impl IntoIterator<Item = (&'a str, &'a Doc)>,
243) -> Result<Vec<Generated>, String> {
244 let docs: Vec<(&str, &Doc)> = docs.into_iter().collect();
245 one_global_allocator(&docs, CodeBlock::is_capture)?;
246 let mut blocks: Vec<(String, &str, Option<String>, String)> = Vec::new();
247 let mut stems: Vec<(String, &str)> = Vec::new();
248 for (rel_path, doc) in docs {
249 let ids = capture_ids(rel_path, doc);
250 if ids.is_empty() {
251 continue;
252 }
253 let stem = sanitized_stem(rel_path);
254 if let Some((_, other)) = stems.iter().find(|(s, _)| *s == stem) {
255 return Err(format!(
256 "{other} and {rel_path} both generate capture blocks `{stem}_N`; rename one"
257 ));
258 }
259 stems.push((stem, rel_path));
260 let captures = doc.blocks.iter().filter(|b| b.is_capture());
261 for (id, block) in ids.into_iter().zip(captures) {
262 let body = capture_module(block)
263 .map_err(|e| format!("{rel_path} line {}: {e}", block.line))?;
264 let content = format!(
265 "{GENERATED_HEADER}\n// source: {rel_path} line {}\n{body}",
266 block.line
267 );
268 blocks.push((id, rel_path, block.feature_cfg(), content));
269 }
270 }
271 if blocks.is_empty() {
272 return Ok(Vec::new());
273 }
274 blocks.sort_by(|a, b| a.1.cmp(b.1));
275 let mut set = vec![Generated {
276 path: format!("{CAPTURE_DIR}/main.rs"),
277 content: capture_main(&blocks),
278 }];
279 set.extend(blocks.into_iter().map(|(id, _, _, content)| Generated {
280 path: format!("{CAPTURE_DIR}/block_{id}.rs"),
281 content,
282 }));
283 Ok(set)
284}
285
286fn capture_main(blocks: &[(String, &str, Option<String>, String)]) -> String {
287 let mut mods = String::new();
288 let mut arms = String::new();
289 for (id, _, cfg, _) in blocks {
290 let run = format!("\"{id}\" => run(block_{id}::main),\n");
291 match cfg {
292 Some(pred) => {
293 mods.push_str(&format!("#[cfg({pred})]\nmod block_{id};\n"));
294 arms.push_str(&format!(" #[cfg({pred})]\n {run}"));
295 arms.push_str(&format!(
296 " #[cfg(not({pred}))]\n \"{id}\" => exit(CAPTURE_DISABLED, &id),\n"
297 ));
298 }
299 None => {
300 mods.push_str(&format!("mod block_{id};\n"));
301 arms.push_str(&format!(" {run}"));
302 }
303 }
304 }
305 let (unknown, unknown_msg) = CAPTURE_UNKNOWN;
306 let (disabled, disabled_msg) = CAPTURE_DISABLED;
307 format!(
308 "{GENERATED_HEADER}\n#![allow(dead_code, unused_imports)]\n\n\
309 use std::process::{{ExitCode, Termination}};\n\n\
310 {VERBATIM}\n\
311 verbatim! {{\n{mods}}}\n\n\
312 const CAPTURE_UNKNOWN: (u8, &str) = ({unknown}, \"{unknown_msg}\");\n\
313 const CAPTURE_DISABLED: (u8, &str) = ({disabled}, \"{disabled_msg}\");\n\n\
314 fn main() -> ExitCode {{\n \
315 let id = std::env::args().nth(1).unwrap_or_default();\n \
316 verbatim! {{\n \
317 match id.as_str() {{\n{arms} \
318 _ => exit(CAPTURE_UNKNOWN, &id),\n \
319 }}\n \
320 }}\n\
321 }}\n\n\
322 fn run<T: Termination>(main: fn() -> T) -> ExitCode {{\n \
323 main().report()\n\
324 }}\n\n\
325 fn exit((code, sentinel): (u8, &str), id: &str) -> ExitCode {{\n \
326 eprintln!(\"{{sentinel}} {{id}}\");\n \
327 ExitCode::from(code)\n\
328 }}\n"
329 )
330}
331
332fn capture_module(block: &CodeBlock) -> Result<String, String> {
334 let lines: Vec<&str> = block.code.lines().collect();
335 let Some(at) = lines.iter().position(|l| top_level_main(l).is_some()) else {
336 return Ok(format!(
337 "pub fn main() {{\n{}\n}}\n",
338 indent(&block.code, " ")
339 ));
340 };
341 for (i, line) in lines.iter().enumerate() {
342 let next = lines[i + 1..].iter().find(|l| !l.trim().is_empty());
343 if line.starts_with("#![") && !is_lint_attribute(line) {
344 return Err(
345 "a capture block cannot use a crate-level attribute (`#![...]`) other than a lint level"
346 .into(),
347 );
348 }
349 if line.starts_with("#[macro_use]") && next.is_some_and(|l| l.starts_with("extern crate")) {
350 return Err("a capture block cannot use `#[macro_use] extern crate`".into());
351 }
352 }
353 let mut out: Vec<String> = lines.iter().map(|l| l.to_string()).collect();
354 if top_level_main(lines[at]) == Some(false) {
355 out[at] = format!("pub {}", lines[at]);
356 }
357 Ok(format!("{}\n", out.join("\n")))
358}
359
360fn is_lint_attribute(line: &str) -> bool {
363 ["allow", "warn", "deny", "forbid", "expect"]
364 .iter()
365 .any(|lint| {
366 line.strip_prefix("#![")
367 .is_some_and(|a| a.starts_with(&format!("{lint}(")))
368 })
369}
370
371fn top_level_main(line: &str) -> Option<bool> {
374 let restricted = line
375 .strip_prefix("pub(")
376 .and_then(|rest| rest.split_once(") ").map(|(_, rest)| rest));
377 let (public, rest) = match restricted.or_else(|| line.strip_prefix("pub ")) {
378 Some(rest) => (true, rest),
379 None => (false, line),
380 };
381 let rest = rest.strip_prefix("async ").unwrap_or(rest);
382 let after = rest.strip_prefix("fn main")?;
383 (!after.starts_with(|c: char| c.is_alphanumeric() || c == '_')).then_some(public)
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::markdown;
390
391 #[test]
392 fn generates_tests_and_examples() {
393 let md = "```rust\nlet x = 1;\n```\n```rust no_run\nloop {}\n```\n```rust capture-output\nprintln!(\"h\");\n```\n";
394 let doc = markdown::scan(md).unwrap();
395 let tests = test_file("docs/a.md", &doc).unwrap();
396 assert!(tests.contains("#[test]"));
397 assert!(tests.contains("compile-only"));
398 assert!(!tests.contains("println!")); assert_eq!(capture_ids("docs/a.md", &doc), ["docs_a_1"]);
400 }
401
402 #[test]
403 fn doc_tests_compile_into_one_target_with_a_module_per_page() {
404 let a = markdown::scan("```rust\nlet x = 1;\n```\n").unwrap();
405 let b = markdown::scan("```rust\nlet y = 2;\n```\n").unwrap();
406 let none = markdown::scan("```rust ignore\nnope();\n```\n").unwrap();
407 let set = doc_test_set([
408 ("docs/b.md", &b),
409 ("README.md", &a),
410 ("docs/skip.md", &none),
411 ])
412 .unwrap();
413 let paths: Vec<&str> = set.iter().map(|g| g.path.as_str()).collect();
414 assert_eq!(
415 paths,
416 [
417 "tests/soothfast_docs/main.rs",
418 "tests/soothfast_docs/doc_readme.rs",
419 "tests/soothfast_docs/doc_docs_b.rs"
420 ]
421 );
422 assert!(set[0].content.starts_with(GENERATED_HEADER));
423 assert!(
424 set[0]
425 .content
426 .ends_with("verbatim! {\nmod doc_readme;\nmod doc_docs_b;\n}\n")
427 );
428 assert_eq!(set[1].content, test_file("README.md", &a).unwrap());
429 }
430
431 #[test]
432 fn no_testable_page_means_no_doc_test_target() {
433 let none = markdown::scan("```rust ignore\nnope();\n```\n").unwrap();
434 assert!(doc_test_set([("README.md", &none)]).unwrap().is_empty());
435 }
436
437 #[test]
438 fn two_pages_on_one_module_name_are_an_error_naming_both() {
439 let a = markdown::scan("```rust\nlet x = 1;\n```\n").unwrap();
440 let e = doc_test_set([("docs/a-b.md", &a), ("docs/a_b.md", &a)]).unwrap_err();
441 assert!(
442 e.contains("docs/a-b.md") && e.contains("docs/a_b.md"),
443 "{e}"
444 );
445 }
446
447 #[test]
448 fn owned_places_cover_both_layouts_and_nothing_else() {
449 assert_eq!(
450 owning_target("tests/soothfast_doc_readme.rs"),
451 Some((DOC_TESTS_DIR, true))
452 );
453 assert_eq!(
454 owning_target("tests/soothfast_docs/main.rs"),
455 Some((DOC_TESTS_DIR, false))
456 );
457 assert_eq!(
458 owning_target("examples/soothfast_capture_readme_1.rs"),
459 Some((CAPTURE_DIR, true))
460 );
461 assert_eq!(
462 owning_target("examples/soothfast_capture/block_readme_1.rs"),
463 Some((CAPTURE_DIR, false))
464 );
465 assert_eq!(owning_target("tests/integration.rs"), None);
466 assert_eq!(owning_target("tests/soothfast_docs/data.json"), None);
467 assert_eq!(owning_target("src/soothfast_doc_x.rs"), None);
468 }
469
470 #[test]
471 fn all_ignored_means_no_test_file() {
472 let doc = markdown::scan("```rust ignore\nfictional();\n```\n").unwrap();
473 assert!(test_file("README.md", &doc).is_none());
474 }
475
476 #[test]
477 fn feature_tag_gates_generated_test() {
478 let md =
479 "```rust feature=risk\nlet x = 1;\n```\n```rust no_run feature=risk\nloop {}\n```\n";
480 let doc = markdown::scan(md).unwrap();
481 let tests = test_file("docs/r.md", &doc).unwrap();
482 assert_eq!(tests.matches("#[cfg(feature = \"risk\")]").count(), 2);
483 }
484
485 #[test]
486 fn comma_separated_feature_tag_gates_on_all_of_them() {
487 let md = "```rust no_run feature=risk,polygon\nloop {}\n```\n";
491 let doc = markdown::scan(md).unwrap();
492 let tests = test_file("docs/r.md", &doc).unwrap();
493 assert!(tests.contains("#[cfg(all(feature = \"risk\", feature = \"polygon\"))]"));
494 }
495
496 fn capture(md: &str) -> Vec<Generated> {
497 capture_set([("docs/c.md", &markdown::scan(md).unwrap())]).unwrap()
498 }
499
500 #[test]
501 fn capture_blocks_compile_into_one_example_with_a_module_per_block() {
502 let set = capture(
503 "```rust capture-output\nprintln!(\"a\");\n```\n```rust capture-output\nprintln!(\"b\");\n```\n",
504 );
505 let paths: Vec<&str> = set.iter().map(|g| g.path.as_str()).collect();
506 assert_eq!(
507 paths,
508 [
509 "examples/soothfast_capture/main.rs",
510 "examples/soothfast_capture/block_docs_c_1.rs",
511 "examples/soothfast_capture/block_docs_c_2.rs",
512 ]
513 );
514 let main = &set[0].content;
515 assert!(main.contains("mod block_docs_c_1;\nmod block_docs_c_2;\n"));
516 assert!(main.contains("\"docs_c_2\" => run(block_docs_c_2::main),"));
517 assert!(main.contains("_ => exit(CAPTURE_UNKNOWN, &id),"));
518 assert!(
519 set[1]
520 .content
521 .contains("pub fn main() {\n println!(\"a\");\n}")
522 );
523 assert_eq!(
524 recorded_source(&set[1].content),
525 Some(("docs/c.md", Some(1)))
526 );
527 }
528
529 #[test]
530 fn no_capture_block_means_no_capture_example() {
531 assert!(capture("```rust\nlet x = 1;\n```\n").is_empty());
532 }
533
534 #[test]
535 fn feature_tag_gates_the_module_and_dispatches_disabled_without_it() {
536 let set = capture("```rust capture-output feature=risk,polygon\nprintln!(\"hi\");\n```\n");
537 let main = &set[0].content;
538 let pred = "all(feature = \"risk\", feature = \"polygon\")";
539 assert!(main.contains(&format!("#[cfg({pred})]\nmod block_docs_c_1;")));
540 assert!(main.contains(&format!(
541 "#[cfg({pred})]\n \"docs_c_1\" => run(block_docs_c_1::main),"
542 )));
543 assert!(main.contains(&format!(
544 "#[cfg(not({pred}))]\n \"docs_c_1\" => exit(CAPTURE_DISABLED, &id),"
545 )));
546 assert!(main.contains(
547 "const CAPTURE_DISABLED: (u8, &str) = (3, \"soothfast_capture: disabled block\");"
548 ));
549 }
550
551 #[test]
552 fn a_blocks_own_top_level_main_becomes_pub_and_keeps_its_attributes() {
553 let md = "```rust capture-output\n// fn main is below\nuse std::fmt;\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n fn main_helper() {}\n Ok(())\n}\n```\n";
554 let block = &capture(md)[1].content;
555 assert!(block.contains("// fn main is below\nuse std::fmt;"));
556 assert!(block.contains("#[tokio::main]\npub async fn main() -> Result<"));
557 assert!(block.contains(" fn main_helper() {}"));
558 assert_eq!(block.matches("fn main(").count(), 1);
559 }
560
561 #[test]
562 fn an_already_pub_main_is_left_alone() {
563 let block = &capture("```rust capture-output\npub fn main() {}\n```\n")[1].content;
564 assert!(block.ends_with("\npub fn main() {}\n"), "{block}");
565 }
566
567 #[test]
568 fn crate_level_attributes_in_a_capture_block_are_an_error() {
569 for code in [
570 "#![feature(test)]\nfn main() {}",
571 "#[macro_use]\nextern crate serde;\nfn main() {}",
572 ] {
573 let doc = markdown::scan(&format!("```rust capture-output\n{code}\n```\n")).unwrap();
574 let e = capture_set([("docs/c.md", &doc)]).unwrap_err();
575 assert!(
576 e.starts_with("docs/c.md line 1: a capture block cannot"),
577 "{e}"
578 );
579 }
580 }
581
582 #[test]
583 fn two_pages_on_one_capture_stem_are_an_error_naming_both() {
584 let a = markdown::scan("```rust capture-output\nlet x = 1;\n```\n").unwrap();
585 let e = capture_set([("docs/a-b.md", &a), ("docs/a_b.md", &a)]).unwrap_err();
586 assert!(
587 e.contains("docs/a-b.md") && e.contains("docs/a_b.md"),
588 "{e}"
589 );
590 }
591
592 #[test]
593 fn recorded_source_reads_page_with_or_without_a_line() {
594 let h = GENERATED_HEADER;
595 assert_eq!(
596 recorded_source(&format!("{h}\n// source: README.md\n")),
597 Some(("README.md", None))
598 );
599 assert_eq!(
600 recorded_source(&format!("{h}\n// source: docs/a line b.md line 41\n")),
601 Some(("docs/a line b.md", Some(41)))
602 );
603 assert_eq!(
604 recorded_source(&format!("{h}\n// source: docs/a line b.md\n")),
605 Some(("docs/a line b.md", None))
606 );
607 assert_eq!(recorded_source(&format!("{h}\nfn main() {{}}\n")), None);
608 }
609
610 #[test]
611 fn a_lint_level_inner_attribute_is_allowed_in_a_capture_block() {
612 for attr in ["allow(dead_code)", "warn(missing_docs)", "expect(unused)"] {
613 let md = format!("```rust capture-output\n#![{attr}]\nfn main() {{}}\n```\n");
614 let block = &capture(&md)[1].content;
615 assert!(
616 block.contains(&format!("#![{attr}]\npub fn main() {{}}")),
617 "{block}"
618 );
619 }
620 }
621
622 #[test]
623 fn a_restricted_visibility_main_is_the_blocks_own_main() {
624 for vis in ["pub(crate)", "pub(super)"] {
625 let md = format!("```rust capture-output\n{vis} fn main() {{}}\n```\n");
626 let block = &capture(&md)[1].content;
627 assert!(
628 block.ends_with(&format!("\n{vis} fn main() {{}}\n")),
629 "{block}"
630 );
631 }
632 }
633
634 #[test]
635 fn only_a_top_level_main_is_called_by_a_doc_test() {
636 let md = "```rust\n// fn main is not here\nfn main_loop() {}\nlet s = \"fn main\";\n```\n```rust\nfn main() {}\n```\n";
637 let tests = test_file("docs/m.md", &markdown::scan(md).unwrap()).unwrap();
638 assert_eq!(tests.matches(" main();").count(), 1, "{tests}");
639 }
640
641 #[test]
642 fn two_blocks_installing_a_global_allocator_are_an_error_naming_both() {
643 let a = markdown::scan("```rust no_run\nsoothfast::bench_main!();\n```\n").unwrap();
644 let b =
645 markdown::scan("```rust no_run\n#[global_allocator]\nstatic A: X = X;\n```\n").unwrap();
646 let e = doc_test_set([("docs/a.md", &a), ("docs/b.md", &b)]).unwrap_err();
647 assert!(
648 e.starts_with("docs/a.md line 1 and docs/b.md line 1 both install"),
649 "{e}"
650 );
651 assert!(doc_test_set([("docs/a.md", &a)]).is_ok());
652 }
653
654 fn assert_rustfmt_leaves_alone(name: &str, set: &[Generated], root: &str) {
657 let dir = std::env::temp_dir().join(format!("soothfast-fmt-{name}-{}", std::process::id()));
658 let _ = std::fs::remove_dir_all(&dir);
659 for g in set {
660 let path = dir.join(&g.path);
661 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
662 std::fs::write(path, &g.content).unwrap();
663 }
664 for edition in ["2015", "2021", "2024"] {
665 let out = std::process::Command::new("rustfmt")
666 .args(["--check", "--edition", edition])
667 .arg(dir.join(root).join("main.rs"))
668 .output()
669 .unwrap();
670 assert!(
671 out.status.success(),
672 "edition {edition}:\n{}",
673 String::from_utf8_lossy(&out.stdout)
674 );
675 }
676 let _ = std::fs::remove_dir_all(&dir);
677 }
678
679 fn rustfmt_present() -> bool {
680 std::process::Command::new("rustfmt")
681 .arg("--version")
682 .output()
683 .is_ok_and(|o| o.status.success())
684 }
685
686 #[test]
687 fn generated_targets_are_already_rustfmt_clean() {
688 if !rustfmt_present() {
689 return;
690 }
691 let long =
692 "docs/library/an_unusually_long_page_name_for_a_reference_section_of_the_library.md";
693 let mut capture_md = String::new();
694 for n in 0..12 {
695 let tag = if n % 3 == 0 {
696 " feature=risk,polygon,indicators,backtesting"
697 } else {
698 ""
699 };
700 capture_md.push_str(&format!(
701 "```rust capture-output{tag}\nprintln!(\"{n}\");\n```\n"
702 ));
703 }
704 let md = format!("```rust\nlet x = 1;\n```\n{capture_md}");
705 let pages: Vec<(&str, Doc)> = [
706 long,
707 "docs/b.md",
708 "docs/a_10.md",
709 "docs/a_2.md",
710 "README.md",
711 ]
712 .into_iter()
713 .map(|p| (p, markdown::scan(&md).unwrap()))
714 .collect();
715 let docs = || pages.iter().map(|(p, d)| (*p, d));
716 assert_rustfmt_leaves_alone("tests", &doc_test_set(docs()).unwrap(), DOC_TESTS_DIR);
717 assert_rustfmt_leaves_alone("capture", &capture_set(docs()).unwrap(), CAPTURE_DIR);
718 }
719}