2025-04-21 19:28:17 -07:00
|
|
|
//! Tests for custom renderers.
|
2018-09-10 18:55:58 +08:00
|
|
|
|
2025-04-21 19:28:17 -07:00
|
|
|
use crate::prelude::*;
|
2025-07-21 11:37:46 -07:00
|
|
|
use anyhow::Result;
|
2025-07-21 15:30:51 -07:00
|
|
|
use mdbook_renderer::{RenderContext, Renderer};
|
2025-04-22 08:50:10 -07:00
|
|
|
use snapbox::IntoData;
|
|
|
|
|
use std::fs::File;
|
2018-09-10 18:55:58 +08:00
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
|
|
|
|
|
|
struct Spy(Arc<Mutex<Inner>>);
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Default)]
|
|
|
|
|
struct Inner {
|
|
|
|
|
run_count: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Renderer for Spy {
|
|
|
|
|
fn name(&self) -> &str {
|
|
|
|
|
"dummy"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn render(&self, _ctx: &RenderContext) -> Result<()> {
|
|
|
|
|
let mut inner = self.0.lock().unwrap();
|
|
|
|
|
inner.run_count += 1;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-21 19:28:17 -07:00
|
|
|
// Test that renderer gets run.
|
2018-09-10 18:55:58 +08:00
|
|
|
#[test]
|
2025-04-21 19:28:17 -07:00
|
|
|
fn runs_renderers() {
|
|
|
|
|
let test = BookTest::init(|_| {});
|
2018-09-10 18:55:58 +08:00
|
|
|
let spy: Arc<Mutex<Inner>> = Default::default();
|
2025-04-21 19:28:17 -07:00
|
|
|
let mut book = test.load_book();
|
2018-09-10 18:55:58 +08:00
|
|
|
book.with_renderer(Spy(Arc::clone(&spy)));
|
|
|
|
|
book.build().unwrap();
|
|
|
|
|
|
|
|
|
|
let inner = spy.lock().unwrap();
|
|
|
|
|
assert_eq!(inner.run_count, 1);
|
|
|
|
|
}
|
2025-04-21 19:31:25 -07:00
|
|
|
|
|
|
|
|
// Test renderer with a failing command fails.
|
|
|
|
|
#[test]
|
|
|
|
|
fn failing_command() {
|
|
|
|
|
BookTest::init(|_| {})
|
|
|
|
|
.rust_program(
|
|
|
|
|
"failing",
|
|
|
|
|
r#"
|
|
|
|
|
fn main() {
|
|
|
|
|
// Read from stdin to avoid random pipe failures on Linux.
|
|
|
|
|
use std::io::Read;
|
|
|
|
|
let mut s = String::new();
|
|
|
|
|
std::io::stdin().read_to_string(&mut s).unwrap();
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
.change_file(
|
|
|
|
|
"book.toml",
|
|
|
|
|
"[output.failing]\n\
|
|
|
|
|
command = './failing'\n",
|
|
|
|
|
)
|
|
|
|
|
.run("build", |cmd| {
|
|
|
|
|
cmd.expect_failure()
|
|
|
|
|
.expect_stdout(str![[""]])
|
|
|
|
|
.expect_stderr(str![[r#"
|
2025-07-21 21:42:58 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
|
|
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the failing backend
|
2025-07-21 21:12:43 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "failing" renderer
|
|
|
|
|
[TIMESTAMP] [ERROR] (mdbook_driver::builtin_renderers): Renderer exited with non-zero return code.
|
2025-07-21 12:20:21 -07:00
|
|
|
[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Rendering failed
|
|
|
|
|
[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: The "failing" renderer failed
|
2025-04-21 19:31:25 -07:00
|
|
|
|
|
|
|
|
"#]]);
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-04-21 19:33:43 -07:00
|
|
|
|
|
|
|
|
// Renderer command is missing.
|
|
|
|
|
#[test]
|
|
|
|
|
fn missing_renderer() {
|
|
|
|
|
BookTest::from_dir("renderer/missing_renderer").run("build", |cmd| {
|
|
|
|
|
cmd.expect_failure()
|
|
|
|
|
.expect_stdout(str![[""]])
|
|
|
|
|
.expect_stderr(str![[r#"
|
2025-07-21 21:42:58 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
|
|
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the missing backend
|
2025-07-21 21:12:43 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "missing" renderer
|
|
|
|
|
[TIMESTAMP] [ERROR] (mdbook_driver::builtin_renderers): The command `trduyvbhijnorgevfuhn` wasn't found, is the "missing" backend installed? If you want to ignore this error when the "missing" backend is not installed, set `optional = true` in the `[output.missing]` section of the book.toml configuration file.
|
2025-07-21 12:20:21 -07:00
|
|
|
[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Rendering failed
|
|
|
|
|
[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: Unable to start the backend
|
|
|
|
|
[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: [NOT_FOUND]
|
2025-04-21 19:33:43 -07:00
|
|
|
|
|
|
|
|
"#]]);
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-04-21 19:35:35 -07:00
|
|
|
|
|
|
|
|
// Optional missing is not an error.
|
|
|
|
|
#[test]
|
|
|
|
|
fn missing_optional_not_fatal() {
|
|
|
|
|
BookTest::from_dir("renderer/missing_optional_not_fatal").run("build", |cmd| {
|
|
|
|
|
cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#"
|
2025-07-21 21:42:58 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
|
|
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the missing backend
|
2025-07-21 21:12:43 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "missing" renderer
|
|
|
|
|
[TIMESTAMP] [WARN] (mdbook_driver::builtin_renderers): The command `trduyvbhijnorgevfuhn` for backend `missing` was not found, but was marked as optional.
|
2025-04-21 19:35:35 -07:00
|
|
|
|
|
|
|
|
"#]]);
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-04-21 19:44:23 -07:00
|
|
|
|
|
|
|
|
// Command can include arguments.
|
|
|
|
|
#[test]
|
|
|
|
|
fn renderer_with_arguments() {
|
|
|
|
|
BookTest::from_dir("renderer/renderer_with_arguments")
|
|
|
|
|
.rust_program(
|
|
|
|
|
"arguments",
|
|
|
|
|
r#"
|
|
|
|
|
fn main() {
|
|
|
|
|
let args: Vec<_> = std::env::args().skip(1).collect();
|
|
|
|
|
assert_eq!(args, &["arg1", "arg2"]);
|
|
|
|
|
println!("Hello World!");
|
|
|
|
|
use std::io::Read;
|
|
|
|
|
let mut s = String::new();
|
|
|
|
|
std::io::stdin().read_to_string(&mut s).unwrap();
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
.run("build", |cmd| {
|
|
|
|
|
cmd.expect_stdout(str![[r#"
|
|
|
|
|
Hello World!
|
|
|
|
|
|
|
|
|
|
"#]])
|
|
|
|
|
.expect_stderr(str![[r#"
|
2025-07-21 21:42:58 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
|
|
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the arguments backend
|
2025-07-21 21:12:43 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "arguments" renderer
|
2025-04-21 19:44:23 -07:00
|
|
|
|
|
|
|
|
"#]]);
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-04-22 08:50:10 -07:00
|
|
|
|
|
|
|
|
// Checks the render context received by the renderer.
|
|
|
|
|
#[test]
|
|
|
|
|
fn backends_receive_render_context_via_stdin() {
|
|
|
|
|
let mut test = BookTest::from_dir("renderer/backends_receive_render_context_via_stdin");
|
|
|
|
|
test.rust_program(
|
|
|
|
|
"cat-to-file",
|
|
|
|
|
r#"
|
|
|
|
|
fn main() {
|
|
|
|
|
use std::io::Read;
|
|
|
|
|
let mut s = String::new();
|
|
|
|
|
std::io::stdin().read_to_string(&mut s).unwrap();
|
|
|
|
|
std::fs::write("out.txt", s).unwrap();
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
.run("build", |cmd| {
|
|
|
|
|
cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#"
|
2025-07-21 21:42:58 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
|
|
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the cat-to-file backend
|
2025-07-21 21:12:43 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "cat-to-file" renderer
|
2025-04-22 08:50:10 -07:00
|
|
|
|
|
|
|
|
"#]]);
|
|
|
|
|
})
|
|
|
|
|
.check_file(
|
|
|
|
|
"book/out.txt",
|
|
|
|
|
str![[r##"
|
|
|
|
|
{
|
|
|
|
|
"book": {
|
|
|
|
|
"sections": [
|
|
|
|
|
{
|
|
|
|
|
"Chapter": {
|
|
|
|
|
"content": "# Chapter 1\n",
|
|
|
|
|
"name": "Chapter 1",
|
|
|
|
|
"number": [
|
|
|
|
|
1
|
|
|
|
|
],
|
|
|
|
|
"parent_names": [],
|
|
|
|
|
"path": "chapter_1.md",
|
|
|
|
|
"source_path": "chapter_1.md",
|
|
|
|
|
"sub_items": []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
"config": {
|
|
|
|
|
"book": {
|
|
|
|
|
"authors": [],
|
|
|
|
|
"language": "en",
|
|
|
|
|
"src": "src"
|
|
|
|
|
},
|
|
|
|
|
"output": {
|
|
|
|
|
"cat-to-file": {
|
|
|
|
|
"command": "./cat-to-file"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"destination": "[ROOT]/book",
|
|
|
|
|
"root": "[ROOT]",
|
|
|
|
|
"version": "[VERSION]"
|
|
|
|
|
}
|
|
|
|
|
"##]]
|
|
|
|
|
.is_json(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Can round-trip.
|
|
|
|
|
let f = File::open(test.dir.join("book/out.txt")).unwrap();
|
|
|
|
|
RenderContext::from_json(f).unwrap();
|
|
|
|
|
}
|
2025-04-22 08:52:34 -07:00
|
|
|
|
|
|
|
|
// Legacy relative renderer paths.
|
|
|
|
|
//
|
|
|
|
|
// https://github.com/rust-lang/mdBook/pull/1418
|
|
|
|
|
#[test]
|
|
|
|
|
fn legacy_relative_command_path() {
|
|
|
|
|
let mut test = BookTest::init(|_| {});
|
|
|
|
|
test.rust_program(
|
|
|
|
|
"renderers/myrenderer",
|
|
|
|
|
r#"
|
|
|
|
|
fn main() {
|
|
|
|
|
use std::io::Read;
|
|
|
|
|
let mut s = String::new();
|
|
|
|
|
std::io::stdin().read_to_string(&mut s).unwrap();
|
|
|
|
|
std::fs::write("output", "test").unwrap();
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
// Test with a modern path, relative to the book directory.
|
|
|
|
|
.change_file(
|
|
|
|
|
"book.toml",
|
|
|
|
|
"[output.myrenderer]\n\
|
|
|
|
|
command = 'renderers/myrenderer'\n",
|
|
|
|
|
)
|
|
|
|
|
.run("build", |cmd| {
|
|
|
|
|
cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#"
|
2025-07-21 21:42:58 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
|
|
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the myrenderer backend
|
2025-07-21 21:12:43 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "myrenderer" renderer
|
2025-04-22 08:52:34 -07:00
|
|
|
|
|
|
|
|
"#]]);
|
|
|
|
|
})
|
|
|
|
|
.check_file("book/output", "test");
|
|
|
|
|
std::fs::remove_file(test.dir.join("book/output")).unwrap();
|
|
|
|
|
// Test with legacy path, relative to the output directory.
|
|
|
|
|
test.change_file(
|
|
|
|
|
"book.toml",
|
|
|
|
|
&format!(
|
|
|
|
|
"[output.myrenderer]\n\
|
|
|
|
|
command = '../renderers/myrenderer{exe}'\n",
|
|
|
|
|
exe = std::env::consts::EXE_SUFFIX
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.run("build", |cmd| {
|
|
|
|
|
cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#"
|
2025-07-21 21:42:58 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
|
|
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the myrenderer backend
|
2025-07-21 21:12:43 -07:00
|
|
|
[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "myrenderer" renderer
|
|
|
|
|
[TIMESTAMP] [WARN] (mdbook_driver::builtin_renderers): Renderer command `../renderers/myrenderer[EXE]` uses a path relative to the renderer output directory `[ROOT]/book`. This was previously accepted, but has been deprecated. Relative executable paths should be relative to the book root.
|
2025-04-22 08:52:34 -07:00
|
|
|
|
|
|
|
|
"#]]);
|
|
|
|
|
})
|
|
|
|
|
.check_file("book/output", "test");
|
|
|
|
|
}
|