This test illustrates that the test function is not working correctly when there is a custom preprocessor.
491 lines
15 KiB
Rust
491 lines
15 KiB
Rust
//! Tests for custom preprocessors.
|
|
|
|
use crate::book_test::list_all_files;
|
|
use crate::prelude::*;
|
|
use anyhow::Result;
|
|
use mdbook_core::book::{Book, BookItem, Chapter};
|
|
use mdbook_driver::builtin_preprocessors::CmdPreprocessor;
|
|
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
|
use snapbox::IntoData;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
struct Spy(Arc<Mutex<Inner>>);
|
|
|
|
#[derive(Debug, Default)]
|
|
struct Inner {
|
|
run_count: usize,
|
|
rendered_with: Vec<String>,
|
|
}
|
|
|
|
impl Preprocessor for Spy {
|
|
fn name(&self) -> &str {
|
|
"dummy"
|
|
}
|
|
|
|
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
|
|
let mut inner = self.0.lock().unwrap();
|
|
inner.run_count += 1;
|
|
inner.rendered_with.push(ctx.renderer.clone());
|
|
Ok(book)
|
|
}
|
|
}
|
|
|
|
// Test that preprocessor gets run.
|
|
#[test]
|
|
fn runs_preprocessors() {
|
|
let test = BookTest::init(|_| {});
|
|
let spy: Arc<Mutex<Inner>> = Default::default();
|
|
let mut book = test.load_book();
|
|
book.with_preprocessor(Spy(Arc::clone(&spy)));
|
|
book.build().unwrap();
|
|
|
|
let inner = spy.lock().unwrap();
|
|
assert_eq!(inner.run_count, 1);
|
|
assert_eq!(inner.rendered_with, ["html"]);
|
|
}
|
|
|
|
// Run tests with a custom preprocessor.
|
|
#[test]
|
|
fn test_with_custom_preprocessor() {
|
|
let test = BookTest::init(|_| {});
|
|
let spy: Arc<Mutex<Inner>> = Default::default();
|
|
let mut book = test.load_book();
|
|
book.with_preprocessor(Spy(Arc::clone(&spy)));
|
|
book.test(vec![]).unwrap();
|
|
|
|
let inner = spy.lock().unwrap();
|
|
assert_eq!(inner.run_count, 0);
|
|
assert_eq!(inner.rendered_with, Vec::<String>::new());
|
|
}
|
|
|
|
// No-op preprocessor works.
|
|
#[test]
|
|
fn nop_preprocessor() {
|
|
BookTest::from_dir("preprocessor/nop_preprocessor").run("build", |cmd| {
|
|
cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#"
|
|
INFO Book building has started
|
|
INFO Running the html backend
|
|
INFO HTML book written to `[ROOT]/book`
|
|
|
|
"#]]);
|
|
});
|
|
}
|
|
|
|
// Failing preprocessor generates an error.
|
|
#[test]
|
|
fn failing_preprocessor() {
|
|
BookTest::from_dir("preprocessor/failing_preprocessor").run("build", |cmd| {
|
|
cmd.expect_failure()
|
|
.expect_stdout(str![[""]])
|
|
.expect_stderr(str![[r#"
|
|
INFO Book building has started
|
|
Boom!!1!
|
|
ERROR The "nop-preprocessor" preprocessor exited unsuccessfully with [EXIT_STATUS]: 1 status
|
|
|
|
"#]]);
|
|
});
|
|
}
|
|
|
|
fn example() -> CmdPreprocessor {
|
|
CmdPreprocessor::new(
|
|
"nop-preprocessor".to_string(),
|
|
"cargo run --quiet --example nop-preprocessor --".to_string(),
|
|
std::env::current_dir().unwrap(),
|
|
false,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn example_supports_whatever() {
|
|
let cmd = example();
|
|
|
|
let got = cmd.supports_renderer("whatever").unwrap();
|
|
|
|
assert_eq!(got, true);
|
|
}
|
|
|
|
#[test]
|
|
fn example_doesnt_support_not_supported() {
|
|
let cmd = example();
|
|
|
|
let got = cmd.supports_renderer("not-supported").unwrap();
|
|
|
|
assert_eq!(got, false);
|
|
}
|
|
|
|
// Checks the behavior of a relative path to a preprocessor.
|
|
#[test]
|
|
fn relative_command_path() {
|
|
let mut test = BookTest::init(|_| {});
|
|
test.rust_program(
|
|
"preprocessors/my-preprocessor",
|
|
r#"
|
|
fn main() {
|
|
let mut args = std::env::args().skip(1);
|
|
if args.next().as_deref() == Some("supports") {
|
|
std::fs::write("support-check", args.next().unwrap()).unwrap();
|
|
return;
|
|
}
|
|
use std::io::Read;
|
|
let mut s = String::new();
|
|
std::io::stdin().read_to_string(&mut s).unwrap();
|
|
std::fs::write("preprocessor-ran", "test").unwrap();
|
|
println!("{{\"items\": []}}");
|
|
}
|
|
"#,
|
|
)
|
|
.change_file(
|
|
"book.toml",
|
|
"[preprocessor.my-preprocessor]\n\
|
|
command = 'preprocessors/my-preprocessor'\n",
|
|
)
|
|
.run("build", |cmd| {
|
|
cmd.expect_stdout(str![""]).expect_stderr(str![[r#"
|
|
INFO Book building has started
|
|
INFO Running the html backend
|
|
INFO HTML book written to `[ROOT]/book`
|
|
|
|
"#]]);
|
|
})
|
|
.check_file("support-check", "html")
|
|
.check_file("preprocessor-ran", "test")
|
|
// Try again, but outside of the book root to check relative path behavior.
|
|
.rm_r("support-check")
|
|
.rm_r("preprocessor-ran")
|
|
.run("build ..", |cmd| {
|
|
cmd.current_dir(cmd.dir.join("src"))
|
|
.expect_stdout(str![""])
|
|
.expect_stderr(str![[r#"
|
|
INFO Book building has started
|
|
INFO Running the html backend
|
|
INFO HTML book written to `[ROOT]/src/../book`
|
|
|
|
"#]]);
|
|
})
|
|
.check_file("support-check", "html")
|
|
.check_file("preprocessor-ran", "test");
|
|
}
|
|
|
|
// Preprocessor command is missing.
|
|
#[test]
|
|
fn missing_preprocessor() {
|
|
BookTest::from_dir("preprocessor/missing_preprocessor").run("build", |cmd| {
|
|
cmd.expect_failure()
|
|
.expect_stdout(str![[""]])
|
|
.expect_stderr(str![[r#"
|
|
INFO Book building has started
|
|
ERROR The command `trduyvbhijnorgevfuhn` wasn't found, is the `missing` preprocessor installed? If you want to ignore this error when the `missing` preprocessor is not installed, set `optional = true` in the `[preprocessor.missing]` section of the book.toml configuration file.
|
|
ERROR Unable to run the preprocessor `missing`
|
|
[TAB]Caused by: [NOT_FOUND]
|
|
|
|
"#]]);
|
|
});
|
|
}
|
|
|
|
// Optional missing is not an error.
|
|
#[test]
|
|
fn missing_optional_not_fatal() {
|
|
BookTest::from_dir("preprocessor/missing_optional_not_fatal").run("build", |cmd| {
|
|
cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#"
|
|
INFO Book building has started
|
|
WARN The command `trduyvbhijnorgevfuhn` for preprocessor `missing` was not found, but is marked as optional.
|
|
INFO Running the html backend
|
|
INFO HTML book written to `[ROOT]/book`
|
|
|
|
"#]]);
|
|
});
|
|
}
|
|
|
|
// with_preprocessor of an existing name.
|
|
#[test]
|
|
fn with_preprocessor_same_name() {
|
|
let mut test = BookTest::init(|_| {});
|
|
test.change_file(
|
|
"book.toml",
|
|
"[preprocessor.dummy]\n\
|
|
command = 'mdbook-preprocessor-does-not-exist'\n",
|
|
);
|
|
let spy: Arc<Mutex<Inner>> = Default::default();
|
|
let mut book = test.load_book();
|
|
book.with_preprocessor(Spy(Arc::clone(&spy)));
|
|
// Unfortunately this is unable to capture the output when using the API.
|
|
book.build().unwrap();
|
|
let inner = spy.lock().unwrap();
|
|
assert_eq!(inner.run_count, 1);
|
|
assert_eq!(inner.rendered_with, ["html"]);
|
|
}
|
|
|
|
// Checks that the interface stays backwards compatible. The interface here
|
|
// should not be changed to fix a compatibility issue unless there is a
|
|
// major-semver version update to mdbook.
|
|
//
|
|
// Note that this tests both preprocessors and renderers. It's in this module
|
|
// for lack of a better location.
|
|
#[test]
|
|
fn extension_compatibility() {
|
|
// This is here to force you to look at this test if you alter any of
|
|
// these types such as adding new fields/variants. This test should be
|
|
// updated accordingly. For example, new `BookItem` variants should be
|
|
// added to the extension_compatibility book, or new fields should be
|
|
// added to the expected input/output. This is also a check that these
|
|
// should only be changed in a semver-breaking release
|
|
let chapter = Chapter {
|
|
name: "example".to_string(),
|
|
content: "content".to_string(),
|
|
number: None,
|
|
sub_items: Vec::new(),
|
|
path: None,
|
|
source_path: None,
|
|
parent_names: Vec::new(),
|
|
};
|
|
let item = BookItem::Chapter(chapter);
|
|
match &item {
|
|
BookItem::Chapter(_) => {}
|
|
BookItem::Separator => {}
|
|
BookItem::PartTitle(_) => {}
|
|
}
|
|
let items = vec![item];
|
|
let _book = Book { items };
|
|
|
|
let mut test = BookTest::from_dir("preprocessor/extension_compatibility");
|
|
// Run it once with the preprocessor disabled so that we can verify
|
|
// that the built book is identical with the preprocessor enabled.
|
|
test.run("build", |cmd| {
|
|
cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#"
|
|
INFO Book building has started
|
|
WARN The command `./my-preprocessor` for preprocessor `my-preprocessor` was not found, but is marked as optional.
|
|
INFO Running the html backend
|
|
INFO HTML book written to `[ROOT]/book/html`
|
|
WARN The command `./my-preprocessor` for preprocessor `my-preprocessor` was not found, but is marked as optional.
|
|
INFO Running the my-renderer backend
|
|
INFO Invoking the "my-renderer" renderer
|
|
WARN The command `./my-renderer` for backend `my-renderer` was not found, but is marked as optional.
|
|
|
|
"#]]);
|
|
});
|
|
let orig_dir = test.dir.join("book.orig");
|
|
let pre_dir = test.dir.join("book");
|
|
std::fs::rename(&pre_dir, &orig_dir).unwrap();
|
|
|
|
// **CAUTION** DO NOT modify this value unless this is a major-semver change.
|
|
let book_output = serde_json::json!({
|
|
"items": [
|
|
{
|
|
"Chapter": {
|
|
"content": "# Prefix chapter\n",
|
|
"name": "Prefix chapter",
|
|
"number": null,
|
|
"parent_names": [],
|
|
"path": "prefix.md",
|
|
"source_path": "prefix.md",
|
|
"sub_items": []
|
|
}
|
|
},
|
|
{
|
|
"Chapter": {
|
|
"content": "# Chapter 1\n",
|
|
"name": "Chapter 1",
|
|
"number": [
|
|
1
|
|
],
|
|
"parent_names": [],
|
|
"path": "chapter_1.md",
|
|
"source_path": "chapter_1.md",
|
|
"sub_items": []
|
|
}
|
|
},
|
|
{
|
|
"Chapter": {
|
|
"content": "",
|
|
"name": "Draft chapter",
|
|
"number": [
|
|
2
|
|
],
|
|
"parent_names": [],
|
|
"path": null,
|
|
"source_path": null,
|
|
"sub_items": []
|
|
}
|
|
},
|
|
{
|
|
"PartTitle": "Part title"
|
|
},
|
|
{
|
|
"Chapter": {
|
|
"content": "# Part chapter\n",
|
|
"name": "Part chapter",
|
|
"number": [
|
|
3
|
|
],
|
|
"parent_names": [],
|
|
"path": "part/chapter.md",
|
|
"source_path": "part/chapter.md",
|
|
"sub_items": [
|
|
{
|
|
"Chapter": {
|
|
"content": "# Part sub chapter\n",
|
|
"name": "Part sub chapter",
|
|
"number": [
|
|
3,
|
|
1
|
|
],
|
|
"parent_names": [
|
|
"Part chapter"
|
|
],
|
|
"path": "part/sub-chapter.md",
|
|
"source_path": "part/sub-chapter.md",
|
|
"sub_items": []
|
|
}
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"Separator",
|
|
{
|
|
"Chapter": {
|
|
"content": "# Suffix chapter\n",
|
|
"name": "Suffix chapter",
|
|
"number": null,
|
|
"parent_names": [],
|
|
"path": "suffix.md",
|
|
"source_path": "suffix.md",
|
|
"sub_items": []
|
|
}
|
|
}
|
|
]
|
|
});
|
|
let output_str = serde_json::to_string(&book_output).unwrap();
|
|
// **CAUTION** The only updates allowed here in a semver-compatible
|
|
// release is to add new fields.
|
|
let expected_config = serde_json::json!({
|
|
"book": {
|
|
"authors": [],
|
|
"description": null,
|
|
"language": "en",
|
|
"text-direction": null,
|
|
"title": "extension_compatibility"
|
|
},
|
|
"output": {
|
|
"html": {},
|
|
"my-renderer": {
|
|
"command": "./my-renderer",
|
|
"custom-config": "renderer settings",
|
|
"custom-table": {
|
|
"extra": "xyz"
|
|
},
|
|
"optional": true
|
|
}
|
|
},
|
|
"preprocessor": {
|
|
"my-preprocessor": {
|
|
"command": "./my-preprocessor",
|
|
"custom-config": true,
|
|
"custom-table": {
|
|
"extra": "abc"
|
|
},
|
|
"optional": true
|
|
}
|
|
}
|
|
});
|
|
|
|
// **CAUTION** The only updates allowed here in a semver-compatible
|
|
// release is to add new fields. The output should not change.
|
|
let expected_preprocessor_input = serde_json::json!([
|
|
{
|
|
"config": expected_config,
|
|
"mdbook_version": "[VERSION]",
|
|
"renderer": "html",
|
|
"root": "[ROOT]"
|
|
},
|
|
book_output
|
|
]);
|
|
let expected_renderer_input = serde_json::json!(
|
|
{
|
|
"version": "[VERSION]",
|
|
"root": "[ROOT]",
|
|
"book": book_output,
|
|
"config": expected_config,
|
|
"destination": "[ROOT]/book/my-renderer",
|
|
}
|
|
);
|
|
|
|
// This preprocessor writes its input to some files, and writes the
|
|
// hard-coded output specified above.
|
|
test.rust_program(
|
|
"my-preprocessor",
|
|
&r###"
|
|
use std::fs::OpenOptions;
|
|
use std::io::{Read, Write};
|
|
fn main() {
|
|
let mut args = std::env::args().skip(1);
|
|
if args.next().as_deref() == Some("supports") {
|
|
let mut file = OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open("support-check")
|
|
.unwrap();
|
|
let renderer = args.next().unwrap();
|
|
writeln!(file, "{renderer}").unwrap();
|
|
if renderer != "html" {
|
|
std::process::exit(1);
|
|
}
|
|
return;
|
|
}
|
|
let mut s = String::new();
|
|
std::io::stdin().read_to_string(&mut s).unwrap();
|
|
std::fs::write("preprocessor-input", &s).unwrap();
|
|
let output = r##"OUTPUT_REPLACE"##;
|
|
println!("{output}");
|
|
}
|
|
"###
|
|
.replace("OUTPUT_REPLACE", &output_str),
|
|
)
|
|
// This renderer writes its input to a file.
|
|
.rust_program(
|
|
"my-renderer",
|
|
&r#"
|
|
fn main() {
|
|
use std::io::Read;
|
|
let mut s = String::new();
|
|
std::io::stdin().read_to_string(&mut s).unwrap();
|
|
std::fs::write("renderer-input", &s).unwrap();
|
|
}
|
|
"#,
|
|
)
|
|
.run("build", |cmd| {
|
|
cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#"
|
|
INFO Book building has started
|
|
INFO Running the html backend
|
|
INFO HTML book written to `[ROOT]/book/html`
|
|
INFO Running the my-renderer backend
|
|
INFO Invoking the "my-renderer" renderer
|
|
|
|
"#]]);
|
|
})
|
|
.check_file("support-check", "html\nmy-renderer\n")
|
|
.check_file(
|
|
"preprocessor-input",
|
|
serde_json::to_string(&expected_preprocessor_input)
|
|
.unwrap()
|
|
.is_json(),
|
|
)
|
|
.check_file(
|
|
"book/my-renderer/renderer-input",
|
|
serde_json::to_string(&expected_renderer_input)
|
|
.unwrap()
|
|
.is_json(),
|
|
);
|
|
// Verify both directories have the exact same output.
|
|
test.rm_r("book/my-renderer/renderer-input");
|
|
let orig_files = list_all_files(&orig_dir);
|
|
let pre_files = list_all_files(&pre_dir);
|
|
assert_eq!(orig_files, pre_files);
|
|
for file in &orig_files {
|
|
let orig_path = orig_dir.join(file);
|
|
if orig_path.is_file() {
|
|
let orig = std::fs::read(&orig_path).unwrap();
|
|
let pre = std::fs::read(&pre_dir.join(file)).unwrap();
|
|
test.assert.eq(pre, orig);
|
|
}
|
|
}
|
|
}
|