Merge pull request #2676 from ehuss/booktest

Introduce new testsuite infrastructure
This commit is contained in:
Eric Huss 2025-04-23 04:18:53 +00:00 committed by GitHub
commit 9b12c5130f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
195 changed files with 2902 additions and 10601 deletions

1
.gitignore vendored
View file

@ -9,6 +9,7 @@ guide/book
.vscode
tests/dummy_book/book/
test_book/book/
tests/testsuite/*/*/book/
# Ignore Jetbrains specific files.
.idea/

174
Cargo.lock generated
View file

@ -75,6 +75,15 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-lossy"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "934ff8719effd2023a48cf63e69536c1c3ced9d3895068f6f5cc9a4ff845e59b"
dependencies = [
"anstyle",
]
[[package]]
name = "anstyle-parse"
version = "0.2.6"
@ -93,6 +102,19 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-svg"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3607949e9f6de49ea4bafe12f5e4fd73613ebf24795e48587302a8cc0e4bb35"
dependencies = [
"anstream",
"anstyle",
"anstyle-lossy",
"html-escape",
"unicode-width",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
@ -110,22 +132,6 @@ version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
[[package]]
name = "assert_cmd"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d"
dependencies = [
"anstyle",
"bstr",
"doc-comment",
"libc",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "autocfg"
version = "1.4.0"
@ -288,6 +294,15 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "content_inspector"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
dependencies = [
"memchr",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@ -427,12 +442,6 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.10.7"
@ -455,10 +464,10 @@ dependencies = [
]
[[package]]
name = "doc-comment"
version = "0.3.3"
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "elasticlunr-rs"
@ -529,15 +538,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "float-cmp"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
dependencies = [
"num-traits",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -737,6 +737,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "html-escape"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
dependencies = [
"utf8-width",
]
[[package]]
name = "html5ever"
version = "0.26.0"
@ -1209,7 +1218,6 @@ version = "0.4.48"
dependencies = [
"ammonia",
"anyhow",
"assert_cmd",
"chrono",
"clap",
"clap_complete",
@ -1226,7 +1234,6 @@ dependencies = [
"once_cell",
"opener",
"pathdiff",
"predicates",
"pretty_assertions",
"pulldown-cmark 0.10.3",
"regex",
@ -1236,6 +1243,7 @@ dependencies = [
"serde_json",
"sha2",
"shlex",
"snapbox",
"tempfile",
"tokio",
"toml",
@ -1615,36 +1623,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "predicates"
version = "3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
dependencies = [
"anstyle",
"difflib",
"float-cmp",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates-core"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
[[package]]
name = "predicates-tree"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
@ -1920,6 +1898,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "siphasher"
version = "0.3.11"
@ -1947,6 +1931,37 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "snapbox"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96dcfc4581e3355d70ac2ee14cfdf81dce3d85c85f1ed9e2c1d3013f53b3436b"
dependencies = [
"anstream",
"anstyle",
"anstyle-svg",
"content_inspector",
"dunce",
"filetime",
"normalize-line-endings",
"regex",
"serde",
"serde_json",
"similar",
"snapbox-macros",
"tempfile",
"walkdir",
]
[[package]]
name = "snapbox-macros"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af"
dependencies = [
"anstream",
]
[[package]]
name = "socket2"
version = "0.5.8"
@ -2063,12 +2078,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "thiserror"
version = "1.0.69"
@ -2261,6 +2270,12 @@ version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "url"
version = "2.5.4"
@ -2284,6 +2299,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@ -2302,15 +2323,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "walkdir"
version = "2.5.0"

View file

@ -64,10 +64,9 @@ elasticlunr-rs = { version = "3.0.2", optional = true }
ammonia = { version = "4.0.0", optional = true }
[dev-dependencies]
assert_cmd = "2.0.11"
predicates = "3.0.3"
select = "0.6.0"
semver = "1.0.17"
snapbox = { version = "0.6.21", features = ["diff", "dir", "term-svg", "regex", "json"] }
pretty_assertions = "1.3.0"
walkdir = "2.3.3"

View file

@ -26,7 +26,7 @@ fn main() {
if let Some(sub_args) = matches.subcommand_matches("supports") {
handle_supports(&preprocessor, sub_args);
} else if let Err(e) = handle_preprocessing(&preprocessor) {
eprintln!("{e}");
eprintln!("{e:?}");
process::exit(1);
}
}

View file

@ -1,163 +0,0 @@
//! Integration tests to make sure alternative backends work.
use mdbook::config::Config;
use mdbook::MDBook;
use std::fs;
use std::path::Path;
use tempfile::{Builder as TempFileBuilder, TempDir};
#[test]
fn passing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("passing", success_cmd(), false);
md.build().unwrap();
}
#[test]
fn failing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd(), false);
md.build().unwrap_err();
}
#[test]
fn missing_backends_are_fatal() {
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", false);
let got = md.build();
assert!(got.is_err());
let error_message = got.err().unwrap().to_string();
assert_eq!(error_message, "Rendering failed");
}
#[test]
fn missing_optional_backends_are_not_fatal() {
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", true);
assert!(md.build().is_ok());
}
#[test]
fn alternate_backend_with_arguments() {
let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!", false);
md.build().unwrap();
}
#[test]
fn backends_receive_render_context_via_stdin() {
use mdbook::renderer::RenderContext;
use std::fs::File;
let (md, temp) = dummy_book_with_backend("cat-to-file", "renderers/myrenderer", false);
let renderers = temp.path().join("renderers");
fs::create_dir(&renderers).unwrap();
rust_exe(
&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("out.txt", s).unwrap();
}"#,
);
let out_file = temp.path().join("book/out.txt");
assert!(!out_file.exists());
md.build().unwrap();
assert!(out_file.exists());
let got = RenderContext::from_json(File::open(&out_file).unwrap());
assert!(got.is_ok());
}
#[test]
fn relative_command_path() {
// Checks behavior of relative paths for the `command` setting.
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let renderers = temp.path().join("renderers");
fs::create_dir(&renderers).unwrap();
rust_exe(
&renderers,
"myrenderer",
r#"fn main() {
std::fs::write("output", "test").unwrap();
}"#,
);
let do_test = |cmd_path| {
let mut config = Config::default();
config
.set("output.html", toml::value::Table::new())
.unwrap();
config.set("output.myrenderer.command", cmd_path).unwrap();
let md = MDBook::init(temp.path())
.with_config(config)
.build()
.unwrap();
let output = temp.path().join("book/myrenderer/output");
assert!(!output.exists());
md.build().unwrap();
assert!(output.exists());
fs::remove_file(output).unwrap();
};
// Legacy paths work, relative to the output directory.
if cfg!(windows) {
do_test("../../renderers/myrenderer.exe");
} else {
do_test("../../renderers/myrenderer");
}
// Modern path, relative to the book directory.
do_test("renderers/myrenderer");
}
fn dummy_book_with_backend(
name: &str,
command: &str,
backend_is_optional: bool,
) -> (MDBook, TempDir) {
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let mut config = Config::default();
config
.set(format!("output.{name}.command"), command)
.unwrap();
if backend_is_optional {
config.set(format!("output.{name}.optional"), true).unwrap();
}
let md = MDBook::init(temp.path())
.with_config(config)
.build()
.unwrap();
(md, temp)
}
fn fail_cmd() -> &'static str {
if cfg!(windows) {
r#"cmd.exe /c "exit 1""#
} else {
"false"
}
}
fn success_cmd() -> &'static str {
if cfg!(windows) {
r#"cmd.exe /c "exit 0""#
} else {
"true"
}
}
fn rust_exe(temp: &Path, name: &str, src: &str) {
let rs = temp.join(name).with_extension("rs");
fs::write(&rs, src).unwrap();
let status = std::process::Command::new("rustc")
.arg(rs)
.current_dir(temp)
.status()
.expect("rustc should run");
assert!(status.success());
}

View file

@ -1,78 +0,0 @@
mod dummy_book;
use crate::dummy_book::DummyBook;
use mdbook::book::Book;
use mdbook::config::Config;
use mdbook::errors::*;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::renderer::{RenderContext, Renderer};
use mdbook::MDBook;
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)
}
}
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(())
}
}
#[test]
fn mdbook_runs_preprocessors() {
let spy: Arc<Mutex<Inner>> = Default::default();
let temp = DummyBook::new().build().unwrap();
let cfg = Config::default();
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
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.len(), 1);
assert_eq!(
"html", inner.rendered_with[0],
"We should have been run with the default HTML renderer"
);
}
#[test]
fn mdbook_runs_renderers() {
let spy: Arc<Mutex<Inner>> = Default::default();
let temp = DummyBook::new().build().unwrap();
let cfg = Config::default();
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
book.with_renderer(Spy(Arc::clone(&spy)));
book.build().unwrap();
let inner = spy.lock().unwrap();
assert_eq!(inner.run_count, 1);
}

View file

@ -1,28 +0,0 @@
use crate::cli::cmd::mdbook_cmd;
use crate::dummy_book::DummyBook;
#[test]
fn mdbook_cli_dummy_book_generates_index_html() {
let temp = DummyBook::new().build().unwrap();
// doesn't exist before
assert!(!temp.path().join("book").exists());
let mut cmd = mdbook_cmd();
cmd.arg("build").current_dir(temp.path());
cmd.assert()
.success()
.stderr(
predicates::str::is_match(r##"Stack depth exceeded in first[\\/]recursive.md."##)
.unwrap(),
)
.stderr(predicates::str::contains(
r##"[INFO] (mdbook::book): Running the html backend"##,
));
// exists afterward
assert!(temp.path().join("book").exists());
let index_file = temp.path().join("book/index.html");
assert!(index_file.exists());
}

View file

@ -1,7 +0,0 @@
use assert_cmd::Command;
pub(crate) fn mdbook_cmd() -> Command {
let mut cmd = Command::cargo_bin("mdbook").unwrap();
cmd.env_remove("RUST_LOG");
cmd
}

View file

@ -1,47 +0,0 @@
use crate::cli::cmd::mdbook_cmd;
use crate::dummy_book::DummyBook;
use mdbook::config::Config;
/// Run `mdbook init` with `--force` to skip the confirmation prompts
#[test]
fn base_mdbook_init_can_skip_confirmation_prompts() {
let temp = DummyBook::new().build().unwrap();
// doesn't exist before
assert!(!temp.path().join("book").exists());
let mut cmd = mdbook_cmd();
cmd.args(["init", "--force"]).current_dir(temp.path());
cmd.assert()
.success()
.stdout(predicates::str::contains("\nAll done, no errors...\n"));
let config = Config::from_disk(temp.path().join("book.toml")).unwrap();
assert_eq!(config.book.title, None);
assert!(!temp.path().join(".gitignore").exists());
}
/// Run `mdbook init` with `--title` without git config.
///
/// Regression test for https://github.com/rust-lang/mdBook/issues/2485
#[test]
fn no_git_config_with_title() {
let temp = DummyBook::new().build().unwrap();
// doesn't exist before
assert!(!temp.path().join("book").exists());
let mut cmd = mdbook_cmd();
cmd.args(["init", "--title", "Example title"])
.current_dir(temp.path())
.env("GIT_CONFIG_GLOBAL", "")
.env("GIT_CONFIG_NOSYSTEM", "1");
cmd.assert()
.success()
.stdout(predicates::str::contains("\nAll done, no errors...\n"));
let config = Config::from_disk(temp.path().join("book.toml")).unwrap();
assert_eq!(config.book.title.as_deref(), Some("Example title"));
}

View file

@ -1,4 +0,0 @@
mod build;
mod cmd;
mod init;
mod test;

View file

@ -1,46 +0,0 @@
use crate::cli::cmd::mdbook_cmd;
use crate::dummy_book::DummyBook;
use predicates::boolean::PredicateBooleanExt;
#[test]
fn mdbook_cli_can_correctly_test_a_passing_book() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut cmd = mdbook_cmd();
cmd.arg("test").current_dir(temp.path());
cmd.assert().success()
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "README.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "intro.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]index.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]nested.md""##).unwrap())
.stderr(predicates::str::is_match(r##"returned an error:\n\n"##).unwrap().not())
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap().not());
}
#[test]
fn mdbook_cli_detects_book_with_failing_tests() {
let temp = DummyBook::new().with_passing_test(false).build().unwrap();
let mut cmd = mdbook_cmd();
cmd.arg("test").current_dir(temp.path());
cmd.assert().failure()
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "README.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "intro.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]index.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]nested.md""##).unwrap())
.stderr(predicates::str::is_match(r##"returned an error:\n\n"##).unwrap())
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap());
}
#[test]
fn empty_cli() {
let mut cmd = mdbook_cmd();
cmd.assert()
.failure()
.code(2)
.stdout(predicates::str::is_empty())
.stderr(predicates::str::contains(
"Creates a book from markdown files",
));
}

View file

@ -1,2 +0,0 @@
mod cli;
mod dummy_book;

View file

@ -1,68 +0,0 @@
mod dummy_book;
use crate::dummy_book::DummyBook;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
use mdbook::MDBook;
fn example() -> CmdPreprocessor {
CmdPreprocessor::new(
"nop-preprocessor".to_string(),
"cargo run --example nop-preprocessor --".to_string(),
)
}
#[test]
fn example_supports_whatever() {
let cmd = example();
let got = cmd.supports_renderer("whatever");
assert_eq!(got, true);
}
#[test]
fn example_doesnt_support_not_supported() {
let cmd = example();
let got = cmd.supports_renderer("not-supported");
assert_eq!(got, false);
}
#[test]
fn ask_the_preprocessor_to_blow_up() {
let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
md.with_preprocessor(example());
md.config
.set("preprocessor.nop-preprocessor.blow-up", true)
.unwrap();
let got = md.build();
assert!(got.is_err());
let error_message = got.err().unwrap().to_string();
let status = if cfg!(windows) {
"exit code: 1"
} else {
"exit status: 1"
};
assert_eq!(
error_message,
format!(
r#"The "nop-preprocessor" preprocessor exited unsuccessfully with {status} status"#
)
);
}
#[test]
fn process_the_dummy_book() {
let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
md.with_preprocessor(example());
md.build().unwrap();
}

View file

@ -1,145 +0,0 @@
//! This will create an entire book in a temporary directory using some
//! dummy contents from the `tests/dummy-book/` directory.
// Not all features are used in all test crates, so...
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
use anyhow::Context;
use mdbook::errors::*;
use mdbook::MDBook;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use tempfile::{Builder as TempFileBuilder, TempDir};
use walkdir::WalkDir;
/// Create a dummy book in a temporary directory, using the contents of
/// `SUMMARY_MD` as a guide.
///
/// The "Nested Chapter" file contains a code block with a single
/// `assert!($TEST_STATUS)`. If you want to check MDBook's testing
/// functionality, `$TEST_STATUS` can be substitute for either `true` or
/// `false`. This is done using the `passing_test` parameter.
#[derive(Clone, Debug, PartialEq)]
pub struct DummyBook {
passing_test: bool,
}
impl DummyBook {
/// Create a new `DummyBook` with all the defaults.
pub fn new() -> DummyBook {
DummyBook { passing_test: true }
}
/// Whether the doc-test included in the "Nested Chapter" should pass or
/// fail (it passes by default).
pub fn with_passing_test(&mut self, test_passes: bool) -> &mut DummyBook {
self.passing_test = test_passes;
self
}
/// Write a book to a temporary directory using the provided settings.
pub fn build(&self) -> Result<TempDir> {
let temp = TempFileBuilder::new()
.prefix("dummy_book-")
.tempdir()
.with_context(|| "Unable to create temp directory")?;
let dummy_book_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/dummy_book");
recursive_copy(&dummy_book_root, temp.path()).with_context(|| {
"Couldn't copy files into a \
temporary directory"
})?;
let sub_pattern = if self.passing_test { "true" } else { "false" };
let files_containing_tests = [
"src/first/nested.md",
"src/first/nested-test.rs",
"src/first/nested-test-with-anchors.rs",
"src/first/partially-included-test.rs",
"src/first/partially-included-test-with-anchors.rs",
];
for file in &files_containing_tests {
let path_containing_tests = temp.path().join(file);
replace_pattern_in_file(&path_containing_tests, "$TEST_STATUS", sub_pattern)?;
}
Ok(temp)
}
}
fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()> {
let contents = fs::read_to_string(filename)?;
File::create(filename)?.write_all(contents.replace(from, to).as_bytes())?;
Ok(())
}
/// Read the contents of the provided file into memory and then iterate through
/// the list of strings asserting that the file contains all of them.
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
let filename = filename.as_ref();
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(
content.contains(s),
"Searching for {:?} in {}\n\n{}",
s,
filename.display(),
content
);
}
}
pub fn assert_doesnt_contain_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
let filename = filename.as_ref();
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(
!content.contains(s),
"Found {:?} in {}\n\n{}",
s,
filename.display(),
content
);
}
}
/// Recursively copy an entire directory tree to somewhere else (a la `cp -r`).
fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()> {
let from = from.as_ref();
let to = to.as_ref();
for entry in WalkDir::new(from) {
let entry = entry.with_context(|| "Unable to inspect directory entry")?;
let original_location = entry.path();
let relative = original_location
.strip_prefix(from)
.expect("`original_location` is inside the `from` directory");
let new_location = to.join(relative);
if original_location.is_file() {
if let Some(parent) = new_location.parent() {
fs::create_dir_all(parent).with_context(|| "Couldn't create directory")?;
}
fs::copy(original_location, &new_location)
.with_context(|| "Unable to copy file contents")?;
}
}
Ok(())
}
pub fn new_copy_of_example_book() -> Result<TempDir> {
let temp = TempFileBuilder::new().prefix("guide").tempdir()?;
let guide = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
recursive_copy(guide, temp.path())?;
Ok(temp)
}

View file

@ -1,5 +0,0 @@
# Dummy Book
This file is just here to cause the index preprocessor to run.
Does a pretty good job, too.

View file

@ -1,3 +0,0 @@
# Includes
{{#include ../SUMMARY.md::}}

View file

@ -1,11 +0,0 @@
// The next line will cause a `testing` test to fail if the anchor feature is broken in such a way
// that the whole file gets mistakenly included.
assert!(!$TEST_STATUS);
// ANCHOR: myanchor
// ANCHOR: unendinganchor
// The next line will cause a `rendered_output` test to fail if the anchor feature is broken in
// such a way that the content between anchors isn't included.
// unique-string-for-anchor-test
assert!($TEST_STATUS);
// ANCHOR_END: myanchor

View file

@ -1 +0,0 @@
assert!($TEST_STATUS);

View file

@ -1,31 +0,0 @@
# Nested Chapter
This file has some testable code.
```rust
assert!($TEST_STATUS);
```
## Some Section
```rust
{{#include nested-test.rs}}
```
## Anchors include the part of a file between special comments
```rust
{{#include nested-test-with-anchors.rs:myanchor}}
```
## Rustdoc include adds the rest of the file as hidden
```rust
{{#rustdoc_include partially-included-test.rs:5:7}}
```
## Rustdoc include works with anchors too
```rust
{{#rustdoc_include partially-included-test-with-anchors.rs:rustdoc-include-anchor}}
```

View file

@ -1,3 +0,0 @@
# Introduction
Here's some interesting text...

View file

@ -1,5 +0,0 @@
# Second Chapter
This makes sure you can insert runnable Rust files.
{{#playground example.rs}}

View file

@ -1 +0,0 @@
# Root README

View file

@ -1,7 +0,0 @@
# This dummy book is for testing the conversion of README.md to index.html by IndexPreprocessor
[Root README](README.md)
- [1st README](first/README.md)
- [2nd README](second/README.md)
- [2nd index](second/index.md)

View file

@ -1 +0,0 @@
# First README

View file

@ -1 +0,0 @@
# Second README

View file

@ -1 +0,0 @@
# Second index

View file

@ -1,157 +0,0 @@
use mdbook::config::Config;
use mdbook::MDBook;
use pretty_assertions::assert_eq;
use std::fs;
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use tempfile::Builder as TempFileBuilder;
/// Run `mdbook init` in an empty directory and make sure the default files
/// are created.
#[test]
fn base_mdbook_init_should_create_default_content() {
let created_files = vec!["book", "src", "src/SUMMARY.md", "src/chapter_1.md"];
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
for file in &created_files {
assert!(!temp.path().join(file).exists());
}
MDBook::init(temp.path()).build().unwrap();
for file in &created_files {
let target = temp.path().join(file);
println!("{}", target.display());
assert!(target.exists(), "{file} doesn't exist");
}
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
assert_eq!(
contents,
"[book]\nauthors = []\nlanguage = \"en\"\nsrc = \"src\"\n"
);
}
/// Run `mdbook init` in a directory containing a SUMMARY.md should create the
/// files listed in the summary.
#[test]
fn run_mdbook_init_should_create_content_from_summary() {
let created_files = vec!["intro.md", "first.md", "outro.md"];
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let src_dir = temp.path().join("src");
fs::create_dir_all(src_dir.clone()).unwrap();
static SUMMARY: &str = r#"# Summary
[intro](intro.md)
- [First chapter](first.md)
[outro](outro.md)
"#;
let mut summary = File::create(src_dir.join("SUMMARY.md")).unwrap();
summary.write_all(SUMMARY.as_bytes()).unwrap();
MDBook::init(temp.path()).build().unwrap();
for file in &created_files {
let target = src_dir.join(file);
println!("{}", target.display());
assert!(target.exists(), "{file} doesn't exist");
}
}
/// Set some custom arguments for where to place the source and destination
/// files, then call `mdbook init`.
#[test]
fn run_mdbook_init_with_custom_book_and_src_locations() {
let created_files = vec!["out", "in", "in/SUMMARY.md", "in/chapter_1.md"];
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
for file in &created_files {
assert!(
!temp.path().join(file).exists(),
"{file} shouldn't exist yet!"
);
}
let mut cfg = Config::default();
cfg.book.src = PathBuf::from("in");
cfg.build.build_dir = PathBuf::from("out");
MDBook::init(temp.path()).with_config(cfg).build().unwrap();
for file in &created_files {
let target = temp.path().join(file);
assert!(
target.exists(),
"{file} should have been created by `mdbook init`"
);
}
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
assert_eq!(
contents,
"[book]\nauthors = []\nlanguage = \"en\"\nsrc = \"in\"\n\n[build]\nbuild-dir = \"out\"\ncreate-missing = true\nextra-watch-dirs = []\nuse-default-preprocessors = true\n"
);
}
#[test]
fn book_toml_isnt_required() {
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let md = MDBook::init(temp.path()).build().unwrap();
let _ = fs::remove_file(temp.path().join("book.toml"));
md.build().unwrap();
}
#[test]
fn copy_theme() {
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
MDBook::init(temp.path()).copy_theme(true).build().unwrap();
let expected = vec![
"book.js",
"css/chrome.css",
"css/general.css",
"css/print.css",
"css/variables.css",
"favicon.png",
"favicon.svg",
"fonts/OPEN-SANS-LICENSE.txt",
"fonts/SOURCE-CODE-PRO-LICENSE.txt",
"fonts/fonts.css",
"fonts/open-sans-v17-all-charsets-300.woff2",
"fonts/open-sans-v17-all-charsets-300italic.woff2",
"fonts/open-sans-v17-all-charsets-600.woff2",
"fonts/open-sans-v17-all-charsets-600italic.woff2",
"fonts/open-sans-v17-all-charsets-700.woff2",
"fonts/open-sans-v17-all-charsets-700italic.woff2",
"fonts/open-sans-v17-all-charsets-800.woff2",
"fonts/open-sans-v17-all-charsets-800italic.woff2",
"fonts/open-sans-v17-all-charsets-italic.woff2",
"fonts/open-sans-v17-all-charsets-regular.woff2",
"fonts/source-code-pro-v11-all-charsets-500.woff2",
"highlight.css",
"highlight.js",
"index.hbs",
];
let theme_dir = temp.path().join("theme");
let mut actual: Vec<_> = walkdir::WalkDir::new(&theme_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| !e.file_type().is_dir())
.map(|e| {
e.path()
.strip_prefix(&theme_dir)
.unwrap()
.to_str()
.unwrap()
.replace('\\', "/")
})
.collect();
actual.sort();
assert_eq!(actual, expected);
}

View file

@ -1,43 +0,0 @@
//! Some integration tests to make sure the `SUMMARY.md` parser can deal with
//! some real-life examples.
use mdbook::book;
use std::fs::File;
use std::io::Read;
use std::path::Path;
macro_rules! summary_md_test {
($name:ident, $filename:expr) => {
#[test]
fn $name() {
env_logger::try_init().ok();
let filename = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("summary_md_files")
.join($filename);
if !filename.exists() {
panic!("{} Doesn't exist", filename.display());
}
let mut content = String::new();
File::open(&filename)
.unwrap()
.read_to_string(&mut content)
.unwrap();
if let Err(e) = book::parse_summary(&content) {
eprintln!("Error parsing {}", filename.display());
eprintln!();
eprintln!("{:?}", e);
panic!();
}
}
};
}
summary_md_test!(rust_by_example, "rust_by_example.md");
summary_md_test!(rust_ffi_guide, "rust_ffi_guide.md");
summary_md_test!(example_book, "example_book.md");
summary_md_test!(the_book_2nd_edition, "the_book-2nd_edition.md");

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,20 +0,0 @@
# Summary
- [mdBook](README.md)
- [Command Line Tool](cli/cli-tool.md)
- [init](cli/init.md)
- [build](cli/build.md)
- [watch](cli/watch.md)
- [serve](cli/serve.md)
- [test](cli/test.md)
- [Format](format/format.md)
- [SUMMARY.md](format/summary.md)
- [Configuration](format/config.md)
- [Theme](format/theme/theme.md)
- [index.hbs](format/theme/index-hbs.md)
- [Syntax highlighting](format/theme/syntax-highlighting.md)
- [MathJax Support](format/mathjax.md)
- [Rust code specific features](format/rust.md)
- [Rust Library](lib/lib.md)
-----------
[Contributors](misc/contributors.md)

View file

@ -1,191 +0,0 @@
# Summary
[Introduction](index.md)
- [Hello World](hello.md)
- [Comments](hello/comment.md)
- [Formatted print](hello/print.md)
- [Debug](hello/print/print_debug.md)
- [Display](hello/print/print_display.md)
- [Testcase: List](hello/print/print_display/testcase_list.md)
- [Formatting](hello/print/fmt.md)
- [Primitives](primitives.md)
- [Literals and operators](primitives/literals.md)
- [Tuples](primitives/tuples.md)
- [Arrays and Slices](primitives/array.md)
- [Custom Types](custom_types.md)
- [Structures](custom_types/structs.md)
- [Enums](custom_types/enum.md)
- [use](custom_types/enum/enum_use.md)
- [C-like](custom_types/enum/c_like.md)
- [Testcase: linked-list](custom_types/enum/testcase_linked_list.md)
- [constants](custom_types/constants.md)
- [Variable Bindings](variable_bindings.md)
- [Mutability](variable_bindings/mut.md)
- [Scope and Shadowing](variable_bindings/scope.md)
- [Declare first](variable_bindings/declare.md)
- [Types](types.md)
- [Casting](types/cast.md)
- [Literals](types/literals.md)
- [Inference](types/inference.md)
- [Aliasing](types/alias.md)
- [Conversion](conversion.md)
- [From and Into](conversion/from_into.md)
- [To and From String](conversion/string.md)
- [Expressions](expression.md)
- [Flow Control](flow_control.md)
- [if/else](flow_control/if_else.md)
- [loop](flow_control/loop.md)
- [Nesting and labels](flow_control/loop/nested.md)
- [Returning from loops](flow_control/loop/return.md)
- [while](flow_control/while.md)
- [for and range](flow_control/for.md)
- [match](flow_control/match.md)
- [Destructuring](flow_control/match/destructuring.md)
- [tuples](flow_control/match/destructuring/destructure_tuple.md)
- [enums](flow_control/match/destructuring/destructure_enum.md)
- [pointers/ref](flow_control/match/destructuring/destructure_pointers.md)
- [structs](flow_control/match/destructuring/destructure_structures.md)
- [Guards](flow_control/match/guard.md)
- [Binding](flow_control/match/binding.md)
- [if let](flow_control/if_let.md)
- [while let](flow_control/while_let.md)
- [Functions](fn.md)
- [Methods](fn/methods.md)
- [Closures](fn/closures.md)
- [Capturing](fn/closures/capture.md)
- [As input parameters](fn/closures/input_parameters.md)
- [Type anonymity](fn/closures/anonymity.md)
- [Input functions](fn/closures/input_functions.md)
- [As output parameters](fn/closures/output_parameters.md)
- [Examples in `std`](fn/closures/closure_examples.md)
- [Iterator::any](fn/closures/closure_examples/iter_any.md)
- [Iterator::find](fn/closures/closure_examples/iter_find.md)
- [Higher Order Functions](fn/hof.md)
- [Modules](mod.md)
- [Visibility](mod/visibility.md)
- [Struct visibility](mod/struct_visibility.md)
- [The `use` declaration](mod/use.md)
- [`super` and `self`](mod/super.md)
- [File hierarchy](mod/split.md)
- [Crates](crates.md)
- [Library](crates/lib.md)
- [`extern crate`](crates/link.md)
- [Attributes](attribute.md)
- [`dead_code`](attribute/unused.md)
- [Crates](attribute/crate.md)
- [`cfg`](attribute/cfg.md)
- [Custom](attribute/cfg/custom.md)
- [Generics](generics.md)
- [Functions](generics/gen_fn.md)
- [Implementation](generics/impl.md)
- [Traits](generics/gen_trait.md)
- [Bounds](generics/bounds.md)
- [Testcase: empty bounds](generics/bounds/testcase_empty.md)
- [Multiple bounds](generics/multi_bounds.md)
- [Where clauses](generics/where.md)
- [New Type Idiom](generics/new_types.md)
- [Associated items](generics/assoc_items.md)
- [The Problem](generics/assoc_items/the_problem.md)
- [Associated types](generics/assoc_items/types.md)
- [Phantom type parameters](generics/phantom.md)
- [Testcase: unit clarification](generics/phantom/testcase_units.md)
- [Scoping rules](scope.md)
- [RAII](scope/raii.md)
- [Ownership and moves](scope/move.md)
- [Mutability](scope/move/mut.md)
- [Borrowing](scope/borrow.md)
- [Mutability](scope/borrow/mut.md)
- [Freezing](scope/borrow/freeze.md)
- [Aliasing](scope/borrow/alias.md)
- [The ref pattern](scope/borrow/ref.md)
- [Lifetimes](scope/lifetime.md)
- [Explicit annotation](scope/lifetime/explicit.md)
- [Functions](scope/lifetime/fn.md)
- [Methods](scope/lifetime/methods.md)
- [Structs](scope/lifetime/struct.md)
- [Bounds](scope/lifetime/lifetime_bounds.md)
- [Coercion](scope/lifetime/lifetime_coercion.md)
- [static](scope/lifetime/static_lifetime.md)
- [elision](scope/lifetime/elision.md)
- [Traits](trait.md)
- [Derive](trait/derive.md)
- [Operator Overloading](trait/ops.md)
- [Drop](trait/drop.md)
- [Iterators](trait/iter.md)
- [Clone](trait/clone.md)
- [macro_rules!](macros.md)
- [Syntax](macro/syntax.md)
- [Designators](macros/designators.md)
- [Overload](macros/overload.md)
- [Repeat](macros/repeat.md)
- [DRY (Don't Repeat Yourself)](macros/dry.md)
- [DSL (Domain Specific Languages)](macros/dsl.md)
- [Variadics](macros/variadics.md)
- [Error handling](error.md)
- [`panic`](error/panic.md)
- [`Option` & `unwrap`](error/option_unwrap.md)
- [Combinators: `map`](error/option_unwrap/map.md)
- [Combinators: `and_then`](error/option_unwrap/and_then.md)
- [`Result`](error/result.md)
- [`map` for `Result`](error/result/result_map.md)
- [aliases for `Result`](error/result/result_alias.md)
- [Early returns](error/result/early_returns.md)
- [Introducing `?`](error/result/enter_question_mark.md)
- [Multiple error types](error/multiple_error_types.md)
- [Pulling `Result`s out of `Option`s](error/multiple_error_types/option_result.md)
- [Defining an error type](error/multiple_error_types/define_error_type.md)
- [`Box`ing errors](error/multiple_error_types/boxing_errors.md)
- [Other uses of `?`](error/multiple_error_types/reenter_question_mark.md)
- [Wrapping errors](error/multiple_error_types/wrap_error.md)
- [Iterating over `Result`s](error/iter_result.md)
- [Std library types](std.md)
- [Box, stack and heap](std/box.md)
- [Vectors](std/vec.md)
- [Strings](std/str.md)
- [`Option`](std/option.md)
- [`Result`](std/result.md)
- [`?`](std/result/question_mark.md)
- [`panic!`](std/panic.md)
- [HashMap](std/hash.md)
- [Alternate/custom key types](std/hash/alt_key_types.md)
- [HashSet](std/hash/hashset.md)
- [Std misc](std_misc.md)
- [Threads](std_misc/threads.md)
- [Testcase: map-reduce](std_misc/threads/testcase_mapreduce.md)
- [Channels](std_misc/channels.md)
- [Path](std_misc/path.md)
- [File I/O](std_misc/file.md)
- [`open`](std_misc/file/open.md)
- [`create`](std_misc/file/create.md)
- [Child processes](std_misc/process.md)
- [Pipes](std_misc/process/pipe.md)
- [Wait](std_misc/process/wait.md)
- [Filesystem Operations](std_misc/fs.md)
- [Program arguments](std_misc/arg.md)
- [Argument parsing](std_misc/arg/matching.md)
- [Foreign Function Interface](std_misc/ffi.md)
- [Meta](meta.md)
- [Documentation](meta/doc.md)
- [Testing](meta/test.md)
- [Unsafe Operations](unsafe.md)

View file

@ -1,19 +0,0 @@
# Summary
- [Overview](./overview.md)
- [Setting Up](./setting_up.md)
- [Core Client Library](./client.md)
- [Constructing a Basic Request](./basic_request.md)
- [Sending the Request](./send_basic.md)
- [Generating a Header File](./cbindgen.md)
- [Better Error Handling](./error_handling.md)
- [Asynchronous Operations](./async.md)
- [More Complex Requests](./complex_request.md)
- [Testing](./testing.md)
- [Dynamic Loading & Plugins](./dynamic_loading.md)
---
- [Break All The Things!!1!](./fun/index.md)
- [Problems](./fun/problems.md)
- [Solutions](./fun/solutions.md)

View file

@ -1,130 +0,0 @@
# The Rust Programming Language
## Getting started
- [Introduction](ch01-00-introduction.md)
- [Installation](ch01-01-installation.md)
- [Hello, World!](ch01-02-hello-world.md)
- [Guessing Game Tutorial](ch02-00-guessing-game-tutorial.md)
- [Common Programming Concepts](ch03-00-common-programming-concepts.md)
- [Variables and Mutability](ch03-01-variables-and-mutability.md)
- [Data Types](ch03-02-data-types.md)
- [How Functions Work](ch03-03-how-functions-work.md)
- [Comments](ch03-04-comments.md)
- [Control Flow](ch03-05-control-flow.md)
- [Understanding Ownership](ch04-00-understanding-ownership.md)
- [What is Ownership?](ch04-01-what-is-ownership.md)
- [References & Borrowing](ch04-02-references-and-borrowing.md)
- [Slices](ch04-03-slices.md)
- [Using Structs to Structure Related Data](ch05-00-structs.md)
- [Defining and Instantiating Structs](ch05-01-defining-structs.md)
- [An Example Program Using Structs](ch05-02-example-structs.md)
- [Method Syntax](ch05-03-method-syntax.md)
- [Enums and Pattern Matching](ch06-00-enums.md)
- [Defining an Enum](ch06-01-defining-an-enum.md)
- [The `match` Control Flow Operator](ch06-02-match.md)
- [Concise Control Flow with `if let`](ch06-03-if-let.md)
## Basic Rust Literacy
- [Modules](ch07-00-modules.md)
- [`mod` and the Filesystem](ch07-01-mod-and-the-filesystem.md)
- [Controlling Visibility with `pub`](ch07-02-controlling-visibility-with-pub.md)
- [Referring to Names in Different Modules](ch07-03-importing-names-with-use.md)
- [Common Collections](ch08-00-common-collections.md)
- [Vectors](ch08-01-vectors.md)
- [Strings](ch08-02-strings.md)
- [Hash Maps](ch08-03-hash-maps.md)
- [Error Handling](ch09-00-error-handling.md)
- [Unrecoverable Errors with `panic!`](ch09-01-unrecoverable-errors-with-panic.md)
- [Recoverable Errors with `Result`](ch09-02-recoverable-errors-with-result.md)
- [To `panic!` or Not To `panic!`](ch09-03-to-panic-or-not-to-panic.md)
- [Generic Types, Traits, and Lifetimes](ch10-00-generics.md)
- [Generic Data Types](ch10-01-syntax.md)
- [Traits: Defining Shared Behavior](ch10-02-traits.md)
- [Validating References with Lifetimes](ch10-03-lifetime-syntax.md)
- [Testing](ch11-00-testing.md)
- [Writing tests](ch11-01-writing-tests.md)
- [Running tests](ch11-02-running-tests.md)
- [Test Organization](ch11-03-test-organization.md)
- [An I/O Project: Building a Command Line Program](ch12-00-an-io-project.md)
- [Accepting Command Line Arguments](ch12-01-accepting-command-line-arguments.md)
- [Reading a File](ch12-02-reading-a-file.md)
- [Refactoring to Improve Modularity and Error Handling](ch12-03-improving-error-handling-and-modularity.md)
- [Developing the Librarys Functionality with Test Driven Development](ch12-04-testing-the-librarys-functionality.md)
- [Working with Environment Variables](ch12-05-working-with-environment-variables.md)
- [Writing Error Messages to Standard Error Instead of Standard Output](ch12-06-writing-to-stderr-instead-of-stdout.md)
## Thinking in Rust
- [Functional Language Features: Iterators and Closures](ch13-00-functional-features.md)
- [Closures: Anonymous Functions that Can Capture Their Environment](ch13-01-closures.md)
- [Processing a Series of Items with Iterators](ch13-02-iterators.md)
- [Improving Our I/O Project](ch13-03-improving-our-io-project.md)
- [Comparing Performance: Loops vs. Iterators](ch13-04-performance.md)
- [More about Cargo and Crates.io](ch14-00-more-about-cargo.md)
- [Customizing Builds with Release Profiles](ch14-01-release-profiles.md)
- [Publishing a Crate to Crates.io](ch14-02-publishing-to-crates-io.md)
- [Cargo Workspaces](ch14-03-cargo-workspaces.md)
- [Installing Binaries from Crates.io with `cargo install`](ch14-04-installing-binaries.md)
- [Extending Cargo with Custom Commands](ch14-05-extending-cargo.md)
- [Smart Pointers](ch15-00-smart-pointers.md)
- [`Box<T>` Points to Data on the Heap and Has a Known Size](ch15-01-box.md)
- [The `Deref` Trait Allows Access to the Data Through a Reference](ch15-02-deref.md)
- [The `Drop` Trait Runs Code on Cleanup](ch15-03-drop.md)
- [`Rc<T>`, the Reference Counted Smart Pointer](ch15-04-rc.md)
- [`RefCell<T>` and the Interior Mutability Pattern](ch15-05-interior-mutability.md)
- [Creating Reference Cycles and Leaking Memory is Safe](ch15-06-reference-cycles.md)
- [Fearless Concurrency](ch16-00-concurrency.md)
- [Threads](ch16-01-threads.md)
- [Message Passing](ch16-02-message-passing.md)
- [Shared State](ch16-03-shared-state.md)
- [Extensible Concurrency: `Sync` and `Send`](ch16-04-extensible-concurrency-sync-and-send.md)
- [Is Rust an Object-Oriented Programming Language?](ch17-00-oop.md)
- [What Does Object-Oriented Mean?](ch17-01-what-is-oo.md)
- [Trait Objects for Using Values of Different Types](ch17-02-trait-objects.md)
- [Object-Oriented Design Pattern Implementations](ch17-03-oo-design-patterns.md)
## Advanced Topics
- [Patterns Match the Structure of Values](ch18-00-patterns.md)
- [All the Places Patterns May be Used](ch18-01-all-the-places-for-patterns.md)
- [Refutability: Whether a Pattern Might Fail to Match](ch18-02-refutability.md)
- [All the Pattern Syntax](ch18-03-pattern-syntax.md)
- [Advanced Features](ch19-00-advanced-features.md)
- [Unsafe Rust](ch19-01-unsafe-rust.md)
- [Advanced Lifetimes](ch19-02-advanced-lifetimes.md)
- [Advanced Traits](ch19-03-advanced-traits.md)
- [Advanced Types](ch19-04-advanced-types.md)
- [Advanced Functions & Closures](ch19-05-advanced-functions-and-closures.md)
- [Final Project: Building a Multithreaded Web Server](ch20-00-final-project-a-web-server.md)
- [A Single Threaded Web Server](ch20-01-single-threaded.md)
- [How Slow Requests Affect Throughput](ch20-02-slow-requests.md)
- [Designing the Thread Pool Interface](ch20-03-designing-the-interface.md)
- [Creating the Thread Pool and Storing Threads](ch20-04-storing-threads.md)
- [Sending Requests to Threads Via Channels](ch20-05-sending-requests-via-channels.md)
- [Graceful Shutdown and Cleanup](ch20-06-graceful-shutdown-and-cleanup.md)
- [Appendix](appendix-00.md)
- [A - Keywords](appendix-01-keywords.md)
- [B - Operators and Symbols](appendix-02-operators.md)
- [C - Derivable Traits](appendix-03-derivable-traits.md)
- [D - Macros](appendix-04-macros.md)
- [E - Translations](appendix-05-translation.md)
- [F - Newest Features](appendix-06-newest-features.md)

View file

@ -1,47 +0,0 @@
mod dummy_book;
use crate::dummy_book::DummyBook;
use mdbook::MDBook;
#[test]
fn mdbook_can_correctly_test_a_passing_book() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
let result = md.test(vec![]);
assert!(
result.is_ok(),
"Tests failed with {}",
result.err().unwrap()
);
}
#[test]
fn mdbook_detects_book_with_failing_tests() {
let temp = DummyBook::new().with_passing_test(false).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
assert!(md.test(vec![]).is_err());
}
#[test]
fn mdbook_test_chapter() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
let result = md.test_chapter(vec![], Some("Introduction"));
assert!(
result.is_ok(),
"test_chapter failed with {}",
result.err().unwrap()
);
}
#[test]
fn mdbook_test_chapter_not_found() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
assert!(md.test_chapter(vec![], Some("Bogus Chapter Name")).is_err());
}

43
tests/testsuite/README.md Normal file
View file

@ -0,0 +1,43 @@
# Testsuite
## Introduction
This is the main testsuite for exercising all functionality of mdBook.
Tests should be organized into modules based around major features. Tests should use `BookTest` to drive the test. `BookTest` will set up a temp directory, and provides a variety of methods to help create a build books.
## Basic structure of a test
Using `BookTest`, you typically use it to copy a directory into a temp directory, and then run mdbook commands in that temp directory. You can run the `mdbook` executable, or use the mdbook API to perform whatever tasks you need. Running the executable has the benefit of being able to validate the console output.
See `build::basic_build` for a simple test example. I recommend reviewing the methods on `BookTest` to learn more, and reviewing some of the existing tests to get a feel for how they are structured.
For example, let's say you are creating a new theme test. In the `testsuite/theme` directory, create a new directory with the book source that you want to exercise. At a minimum, this needs a `src/SUMMARY.md`, but often you'll also want `book.toml`. Then, in `testsuite/theme.rs`, add a test with `BookTest::from_dir("theme/mytest")`, and then use the methods to perform whatever actions you want.
`BookTest` is designed to be able to chain a series of actions. For example, you can do something like:
```rust
BookTest::from_dir("theme/mytest")
.build()
.check_main_file("book/index.html", str![["file contents"]])
.change_file("src/index.md", "new contents")
.build()
.check_main_file("book/index.html", str![["new contents"]]);
```
## Snapbox
The testsuite uses [`snapbox`] to drive most of the tests. This library provides the ability to compare strings using a variety of methods. These strings are written in the source code using either the [`str!`] or [`file!`] macros.
The magic is that you can set the `SNAPSHOTS=overwrite` environment variable, and snapbox will automatically update the strings contents of `str!`, or the file contents of `file!`. This makes it easier to update tests. Snapbox provides nice diffing output, and quite a few other features.
Expected contents can have wildcards like `...` (matches any lines) or `[..]` (matches any characters on a line). See [snapbox filters] for more info and other filters.
Typically when writing a test, I'll just start with an empty `str!` or `file!`, and let snapbox fill it in. Then I review the contents to make sure they are what I expect.
Note that there is some normalization applied to the strings. See `book_test::assert` for how some of these normalizations happen.
[`snapbox`]: https://docs.rs/snapbox/latest/snapbox/
[`str!`]: https://docs.rs/snapbox/latest/snapbox/macro.str.html
[`file!`]: https://docs.rs/snapbox/latest/snapbox/macro.file.html
[snapbox filters]: https://docs.rs/snapbox/latest/snapbox/assert/struct.Assert.html#method.eq

View file

@ -0,0 +1,442 @@
//! Utility for building and running tests against mdbook.
use mdbook::book::BookBuilder;
use mdbook::MDBook;
use snapbox::IntoData;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
/// Test number used for generating unique temp directory names.
static NEXT_TEST_ID: AtomicU32 = AtomicU32::new(0);
#[derive(Clone, Copy, Eq, PartialEq)]
enum StatusCode {
Success,
Failure,
Code(i32),
}
/// Main helper for driving mdbook tests.
pub struct BookTest {
/// The temp directory where the test should perform its work.
pub dir: PathBuf,
assert: snapbox::Assert,
/// This indicates whether or not the book has been built.
built: bool,
}
impl BookTest {
/// Creates a new test, copying the contents from the given directory into
/// a temp directory.
pub fn from_dir(dir: &str) -> BookTest {
// Copy this test book to a temp directory.
let dir = Path::new("tests/testsuite").join(dir);
assert!(dir.exists(), "{dir:?} should exist");
let tmp = Self::new_tmp();
mdbook::utils::fs::copy_files_except_ext(
&dir,
&tmp,
true,
Some(&PathBuf::from("book")),
&[],
)
.unwrap_or_else(|e| panic!("failed to copy test book {dir:?} to {tmp:?}: {e:?}"));
Self::new(tmp)
}
/// Creates a new test with an empty temp directory.
pub fn empty() -> BookTest {
Self::new(Self::new_tmp())
}
/// Creates a new test with the given function to initialize a new book.
///
/// The book itself is not built.
pub fn init(f: impl Fn(&mut BookBuilder)) -> BookTest {
let tmp = Self::new_tmp();
let mut bb = MDBook::init(&tmp);
f(&mut bb);
bb.build()
.unwrap_or_else(|e| panic!("failed to initialize book at {tmp:?}: {e:?}"));
Self::new(tmp)
}
fn new_tmp() -> PathBuf {
let id = NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst);
let tmp = Path::new(env!("CARGO_TARGET_TMPDIR"))
.join("ts")
.join(format!("t{id}"));
if tmp.exists() {
std::fs::remove_dir_all(&tmp)
.unwrap_or_else(|e| panic!("failed to remove {tmp:?}: {e:?}"));
}
std::fs::create_dir_all(&tmp).unwrap_or_else(|e| panic!("failed to create {tmp:?}: {e:?}"));
tmp
}
fn new(dir: PathBuf) -> BookTest {
let assert = assert(&dir);
BookTest {
dir,
assert,
built: false,
}
}
/// Checks the contents of an HTML file that it has the given contents
/// between the `<main>` tag.
///
/// Normally the contents outside of the `<main>` tag aren't interesting,
/// and they add a significant amount of noise.
pub fn check_main_file(&mut self, path: &str, expected: impl IntoData) -> &mut Self {
if !self.built {
self.build();
}
let full_path = self.dir.join(path);
let actual = read_to_string(&full_path);
let start = actual
.find("<main>")
.unwrap_or_else(|| panic!("didn't find <main> in:\n{actual}"));
let end = actual.find("</main>").unwrap();
let contents = actual[start + 6..end - 7].trim();
self.assert.eq(contents, expected);
self
}
/// Checks the summary contents of `toc.js` against the expected value.
pub fn check_toc_js(&mut self, expected: impl IntoData) -> &mut Self {
if !self.built {
self.build();
}
let inner = self.toc_js_html();
// Would be nice if this were prettified, but a primitive wrapping will do for now.
let inner = inner.replace("><", ">\n<");
self.assert.eq(inner, expected);
self
}
/// Returns the summary contents from `toc.js`.
pub fn toc_js_html(&self) -> String {
let full_path = self.dir.join("book/toc.js");
let actual = read_to_string(&full_path);
let inner = actual
.lines()
.filter_map(|line| {
let line = line.trim().strip_prefix("this.innerHTML = '")?;
let line = line.strip_suffix("';")?;
Some(line)
})
.next()
.expect("should have innerHTML");
inner.to_string()
}
/// Checks that the contents of the given file matches the expected value.
pub fn check_file(&mut self, path: &str, expected: impl IntoData) -> &mut Self {
if !self.built {
self.build();
}
let path = self.dir.join(path);
let actual = read_to_string(&path);
self.assert.eq(actual, expected);
self
}
/// Checks that the given file contains the given string somewhere.
pub fn check_file_contains(&mut self, path: &str, expected: &str) -> &mut Self {
if !self.built {
self.build();
}
let path = self.dir.join(path);
let actual = read_to_string(&path);
assert!(
actual.contains(expected),
"Did not find {expected:?} in {path:?}\n\n{actual}",
);
self
}
/// Checks that the given file does not contain the given string anywhere.
///
/// Beware that using this is fragile, as it may be unable to catch
/// regressions (it can't tell the difference between success, or the
/// string being looked for changed).
pub fn check_file_doesnt_contain(&mut self, path: &str, string: &str) -> &mut Self {
if !self.built {
self.build();
}
let path = self.dir.join(path);
let actual = read_to_string(&path);
assert!(
!actual.contains(string),
"Unexpectedly found {string:?} in {path:?}\n\n{actual}",
);
self
}
/// Checks that the list of files at the given path matches the given value.
pub fn check_file_list(&mut self, path: &str, expected: impl IntoData) -> &mut Self {
let mut all_paths: Vec<_> = walkdir::WalkDir::new(&self.dir.join(path))
.into_iter()
// Skip the outer directory.
.skip(1)
.map(|e| {
e.unwrap()
.into_path()
.strip_prefix(&self.dir)
.unwrap()
.to_str()
.unwrap()
.replace('\\', "/")
})
.collect();
all_paths.sort();
let actual = all_paths.join("\n");
self.assert.eq(actual, expected);
self
}
/// Loads an [`MDBook`] from the temp directory.
pub fn load_book(&self) -> MDBook {
MDBook::load(&self.dir).unwrap_or_else(|e| panic!("book failed to load: {e:?}"))
}
/// Builds the book in the temp directory.
pub fn build(&mut self) -> &mut Self {
let book = self.load_book();
book.build()
.unwrap_or_else(|e| panic!("book failed to build: {e:?}"));
self.built = true;
self
}
/// Runs the `mdbook` binary in the temp directory.
///
/// This runs `mdbook` with the given args. The args are split on spaces
/// (if you need args with spaces, use the `args` method). The given
/// callback receives a [`BookCommand`] for you to customize how the
/// executable is run.
pub fn run(&mut self, args: &str, f: impl Fn(&mut BookCommand)) -> &mut Self {
let mut cmd = BookCommand {
assert: self.assert.clone(),
dir: self.dir.clone(),
args: split_args(args),
env: BTreeMap::new(),
expect_status: StatusCode::Success,
expect_stderr_data: None,
expect_stdout_data: None,
};
f(&mut cmd);
cmd.run();
self
}
/// Change a file's contents in the given path.
pub fn change_file(&mut self, path: impl AsRef<Path>, body: &str) -> &mut Self {
let path = self.dir.join(path);
std::fs::write(&path, body).unwrap_or_else(|e| panic!("failed to write {path:?}: {e:?}"));
self
}
/// Builds a Rust program with the given src.
///
/// The given path should be the path where to output the executable in
/// the temp directory.
pub fn rust_program(&mut self, path: &str, src: &str) -> &mut Self {
let rs = self.dir.join(path).with_extension("rs");
let parent = rs.parent().unwrap();
if !parent.exists() {
std::fs::create_dir_all(&parent).unwrap();
}
std::fs::write(&rs, src).unwrap_or_else(|e| panic!("failed to write {rs:?}: {e:?}"));
let status = std::process::Command::new("rustc")
.arg(&rs)
.current_dir(&parent)
.status()
.expect("rustc should run");
assert!(status.success());
self
}
}
/// A builder for preparing to run the `mdbook` executable.
///
/// By default, it expects the process to succeed.
pub struct BookCommand {
pub dir: PathBuf,
assert: snapbox::Assert,
args: Vec<String>,
env: BTreeMap<String, Option<String>>,
expect_status: StatusCode,
expect_stderr_data: Option<snapbox::Data>,
expect_stdout_data: Option<snapbox::Data>,
}
impl BookCommand {
/// Indicates that the process should fail.
pub fn expect_failure(&mut self) -> &mut Self {
self.expect_status = StatusCode::Failure;
self
}
/// Indicates the process should fail with the given exit code.
pub fn expect_code(&mut self, code: i32) -> &mut Self {
self.expect_status = StatusCode::Code(code);
self
}
/// Verifies that stderr matches the given value.
pub fn expect_stderr(&mut self, expected: impl snapbox::IntoData) -> &mut Self {
self.expect_stderr_data = Some(expected.into_data());
self
}
/// Verifies that stdout matches the given value.
pub fn expect_stdout(&mut self, expected: impl snapbox::IntoData) -> &mut Self {
self.expect_stdout_data = Some(expected.into_data());
self
}
/// Adds arguments to the command to run.
pub fn args(&mut self, args: &[&str]) -> &mut Self {
self.args.extend(args.into_iter().map(|t| t.to_string()));
self
}
/// Specifies an environment variable to set on the executable.
pub fn env<T: Into<String>>(&mut self, key: &str, value: T) -> &mut Self {
self.env.insert(key.to_string(), Some(value.into()));
self
}
/// Runs the command, and verifies the output.
fn run(&mut self) {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mdbook"));
cmd.current_dir(&self.dir)
.args(&self.args)
.env_remove("RUST_LOG")
// Don't read the system git config which is out of our control.
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", &self.dir)
.env("GIT_CONFIG_SYSTEM", &self.dir)
.env_remove("GIT_AUTHOR_EMAIL")
.env_remove("GIT_AUTHOR_NAME")
.env_remove("GIT_COMMITTER_EMAIL")
.env_remove("GIT_COMMITTER_NAME");
for (k, v) in &self.env {
match v {
Some(v) => cmd.env(k, v),
None => cmd.env_remove(k),
};
}
let output = cmd.output().expect("mdbook should be runnable");
let stdout = std::str::from_utf8(&output.stdout).expect("stdout is not utf8");
let stderr = std::str::from_utf8(&output.stderr).expect("stderr is not utf8");
let render_output = || format!("\n--- stdout\n{stdout}\n--- stderr\n{stderr}");
match (self.expect_status, output.status.success()) {
(StatusCode::Success, false) => {
panic!("mdbook failed, but expected success{}", render_output())
}
(StatusCode::Failure, true) => {
panic!("mdbook succeeded, but expected failure{}", render_output())
}
(StatusCode::Code(expected), _) => match output.status.code() {
Some(actual) => assert_eq!(
actual, expected,
"process exit code did not match as expected"
),
None => panic!("process exited via signal {:?}", output.status),
},
_ => {}
}
self.expect_status = StatusCode::Success; // Reset to default.
if let Some(expect_stderr_data) = &self.expect_stderr_data {
if let Err(e) = self.assert.try_eq(
Some(&"stderr"),
stderr.into_data(),
expect_stderr_data.clone(),
) {
panic!("{e}");
}
}
if let Some(expect_stdout_data) = &self.expect_stdout_data {
if let Err(e) = self.assert.try_eq(
Some(&"stdout"),
stdout.into_data(),
expect_stdout_data.clone(),
) {
panic!("{e}");
}
}
}
}
fn split_args(s: &str) -> Vec<String> {
s.split_whitespace()
.map(|arg| {
if arg.contains(&['"', '\''][..]) {
panic!("shell-style argument parsing is not supported");
}
String::from(arg)
})
.collect()
}
static LITERAL_REDACTIONS: &[(&str, &str)] = &[
// Unix message for an entity was not found
("[NOT_FOUND]", "No such file or directory (os error 2)"),
// Windows message for an entity was not found
(
"[NOT_FOUND]",
"The system cannot find the file specified. (os error 2)",
),
(
"[NOT_FOUND]",
"The system cannot find the path specified. (os error 3)",
),
("[NOT_FOUND]", "program not found"),
// Unix message for exit status
("[EXIT_STATUS]", "exit status"),
// Windows message for exit status
("[EXIT_STATUS]", "exit code"),
("[TAB]", "\t"),
("[EXE]", std::env::consts::EXE_SUFFIX),
];
/// This makes it easier to write regex replacements that are guaranteed to only
/// get compiled once
macro_rules! regex {
($re:literal $(,)?) => {{
static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
RE.get_or_init(|| regex::Regex::new($re).unwrap())
}};
}
fn assert(root: &Path) -> snapbox::Assert {
let mut subs = snapbox::Redactions::new();
subs.insert("[ROOT]", root.to_path_buf()).unwrap();
subs.insert(
"[TIMESTAMP]",
regex!(r"(?m)(?<redacted>20\d\d-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"),
)
.unwrap();
subs.insert("[VERSION]", mdbook::MDBOOK_VERSION).unwrap();
subs.extend(LITERAL_REDACTIONS.into_iter().cloned())
.unwrap();
snapbox::Assert::new()
.action_env(snapbox::assert::DEFAULT_ACTION_ENV)
.redact_with(subs)
}
/// Helper to read a string from the filesystem.
#[track_caller]
pub fn read_to_string<P: AsRef<Path>>(path: P) -> String {
let path = path.as_ref();
std::fs::read_to_string(path).unwrap_or_else(|e| panic!("could not read file {path:?}: {e:?}"))
}

67
tests/testsuite/build.rs Normal file
View file

@ -0,0 +1,67 @@
//! General build tests.
//!
//! More specific tests should usually go into a module based on the feature.
//! This module should just have general build tests, or misc small things.
use crate::prelude::*;
// Simple smoke test that building works.
#[test]
fn basic_build() {
BookTest::from_dir("build/basic_build").run("build", |cmd| {
cmd.expect_stderr(str![[r#"
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
"#]]);
});
}
// Ensure building fails if `create-missing` is false and one of the files does
// not exist.
#[test]
fn failure_on_missing_file() {
BookTest::from_dir("build/missing_file").run("build", |cmd| {
cmd.expect_failure().expect_stderr(str![[r#"
[TIMESTAMP] [ERROR] (mdbook::utils): Error: Chapter file not found, ./chapter_1.md
[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: [NOT_FOUND]
"#]]);
});
}
// Ensure a missing file is created if `create-missing` is true.
#[test]
fn create_missing() {
let test = BookTest::from_dir("build/create_missing");
assert!(test.dir.join("src/SUMMARY.md").exists());
assert!(!test.dir.join("src/chapter_1.md").exists());
test.load_book();
assert!(test.dir.join("src/chapter_1.md").exists());
}
// Checks that it fails if the summary has a reserved filename.
#[test]
fn no_reserved_filename() {
BookTest::from_dir("build/no_reserved_filename").run("build", |cmd| {
cmd.expect_failure().expect_stderr(str![[r#"
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed
[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: print.md is reserved for internal use
"#]]);
});
}
// Build without book.toml should be OK.
#[test]
fn book_toml_isnt_required() {
let mut test = BookTest::init(|_| {});
std::fs::remove_file(test.dir.join("book.toml")).unwrap();
test.build();
test.check_main_file(
"book/chapter_1.html",
str![[r##"<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>"##]],
);
}

View file

@ -0,0 +1,2 @@
[book]
title = "basic_build"

View file

@ -0,0 +1,3 @@
# Summary
- [Chapter 1](./chapter_1.md)

View file

@ -0,0 +1,2 @@
[book]
title = "create_missing"

View file

@ -0,0 +1,3 @@
# Summary
- [Chapter 1](./chapter_1.md)

View file

@ -0,0 +1,5 @@
[book]
title = "missing_file"
[build]
create-missing = false

View file

@ -0,0 +1,3 @@
# Summary
- [Chapter 1](./chapter_1.md)

View file

@ -0,0 +1,2 @@
[book]
title = "no_reserved_filename"

View file

@ -0,0 +1,3 @@
# Summary
- [Print](print.md)

View file

@ -0,0 +1 @@
# Print

36
tests/testsuite/cli.rs Normal file
View file

@ -0,0 +1,36 @@
//! Basic tests for mdbook's CLI.
use crate::prelude::*;
use snapbox::file;
// Test with no args.
#[test]
#[cfg_attr(
not(all(feature = "watch", feature = "serve")),
ignore = "needs all features"
)]
fn no_args() {
BookTest::empty().run("", |cmd| {
cmd.expect_code(2)
.expect_stdout(str![[""]])
.expect_stderr(file!["cli/no_args.term.svg"]);
});
}
// Help command.
#[test]
#[cfg_attr(
not(all(feature = "watch", feature = "serve")),
ignore = "needs all features"
)]
fn help() {
BookTest::empty()
.run("help", |cmd| {
cmd.expect_stdout(file!["cli/help.term.svg"])
.expect_stderr(str![[""]]);
})
.run("--help", |cmd| {
cmd.expect_stdout(file!["cli/help.term.svg"])
.expect_stderr(str![[""]]);
});
}

View file

@ -0,0 +1,63 @@
<svg width="740px" height="398px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.container {
padding: 0 10px;
line-height: 18px;
}
tspan {
font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
white-space: pre;
line-height: 18px;
}
</style>
<rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
<text xml:space="preserve" class="container fg">
<tspan x="10px" y="28px"><tspan>Creates a book from markdown files</tspan>
</tspan>
<tspan x="10px" y="46px">
</tspan>
<tspan x="10px" y="64px"><tspan>Usage: mdbook[EXE] [COMMAND]</tspan>
</tspan>
<tspan x="10px" y="82px">
</tspan>
<tspan x="10px" y="100px"><tspan>Commands:</tspan>
</tspan>
<tspan x="10px" y="118px"><tspan> init Creates the boilerplate structure and files for a new book</tspan>
</tspan>
<tspan x="10px" y="136px"><tspan> build Builds a book from its markdown files</tspan>
</tspan>
<tspan x="10px" y="154px"><tspan> test Tests that a book's Rust code samples compile</tspan>
</tspan>
<tspan x="10px" y="172px"><tspan> clean Deletes a built book</tspan>
</tspan>
<tspan x="10px" y="190px"><tspan> completions Generate shell completions for your shell to stdout</tspan>
</tspan>
<tspan x="10px" y="208px"><tspan> watch Watches a book's files and rebuilds it on changes</tspan>
</tspan>
<tspan x="10px" y="226px"><tspan> serve Serves a book at http://localhost:3000, and rebuilds it on changes</tspan>
</tspan>
<tspan x="10px" y="244px"><tspan> help Print this message or the help of the given subcommand(s)</tspan>
</tspan>
<tspan x="10px" y="262px">
</tspan>
<tspan x="10px" y="280px"><tspan>Options:</tspan>
</tspan>
<tspan x="10px" y="298px"><tspan> -h, --help Print help</tspan>
</tspan>
<tspan x="10px" y="316px"><tspan> -V, --version Print version</tspan>
</tspan>
<tspan x="10px" y="334px">
</tspan>
<tspan x="10px" y="352px"><tspan>For more information about a specific command, try `mdbook &lt;command&gt; --help`</tspan>
</tspan>
<tspan x="10px" y="370px"><tspan>The source code for mdBook is available at: https://github.com/rust-lang/mdBook</tspan>
</tspan>
<tspan x="10px" y="388px">
</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,63 @@
<svg width="740px" height="398px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.container {
padding: 0 10px;
line-height: 18px;
}
tspan {
font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
white-space: pre;
line-height: 18px;
}
</style>
<rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
<text xml:space="preserve" class="container fg">
<tspan x="10px" y="28px"><tspan>Creates a book from markdown files</tspan>
</tspan>
<tspan x="10px" y="46px">
</tspan>
<tspan x="10px" y="64px"><tspan>Usage: mdbook[EXE] [COMMAND]</tspan>
</tspan>
<tspan x="10px" y="82px">
</tspan>
<tspan x="10px" y="100px"><tspan>Commands:</tspan>
</tspan>
<tspan x="10px" y="118px"><tspan> init Creates the boilerplate structure and files for a new book</tspan>
</tspan>
<tspan x="10px" y="136px"><tspan> build Builds a book from its markdown files</tspan>
</tspan>
<tspan x="10px" y="154px"><tspan> test Tests that a book's Rust code samples compile</tspan>
</tspan>
<tspan x="10px" y="172px"><tspan> clean Deletes a built book</tspan>
</tspan>
<tspan x="10px" y="190px"><tspan> completions Generate shell completions for your shell to stdout</tspan>
</tspan>
<tspan x="10px" y="208px"><tspan> watch Watches a book's files and rebuilds it on changes</tspan>
</tspan>
<tspan x="10px" y="226px"><tspan> serve Serves a book at http://localhost:3000, and rebuilds it on changes</tspan>
</tspan>
<tspan x="10px" y="244px"><tspan> help Print this message or the help of the given subcommand(s)</tspan>
</tspan>
<tspan x="10px" y="262px">
</tspan>
<tspan x="10px" y="280px"><tspan>Options:</tspan>
</tspan>
<tspan x="10px" y="298px"><tspan> -h, --help Print help</tspan>
</tspan>
<tspan x="10px" y="316px"><tspan> -V, --version Print version</tspan>
</tspan>
<tspan x="10px" y="334px">
</tspan>
<tspan x="10px" y="352px"><tspan>For more information about a specific command, try `mdbook &lt;command&gt; --help`</tspan>
</tspan>
<tspan x="10px" y="370px"><tspan>The source code for mdBook is available at: https://github.com/rust-lang/mdBook</tspan>
</tspan>
<tspan x="10px" y="388px">
</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

112
tests/testsuite/includes.rs Normal file
View file

@ -0,0 +1,112 @@
//! Tests for include preprocessor.
use crate::prelude::*;
// Basic test for #include.
#[test]
fn include() {
BookTest::from_dir("includes/all_includes")
.check_main_file(
"book/includes.html",
str![[r##"
<h1 id="basic-includes"><a class="header" href="#basic-includes">Basic Includes</a></h1>
<h2 id="sample"><a class="header" href="#sample">Sample</a></h2>
<p>This is a sample include.</p>
"##]],
)
.check_main_file(
"book/relative/includes.html",
str![[r##"
<h1 id="relative-includes"><a class="header" href="#relative-includes">Relative Includes</a></h1>
<h2 id="sample"><a class="header" href="#sample">Sample</a></h2>
<p>This is a sample include.</p>
"##]],
);
}
// Checks for anchored includes.
#[test]
fn anchored_include() {
BookTest::from_dir("includes/all_includes").check_main_file(
"book/anchors.html",
str![[r##"
<h1 id="include-anchors"><a class="header" href="#include-anchors">Include Anchors</a></h1>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let x = 1;
<span class="boring">}</span></code></pre></pre>
"##]],
);
}
// Checks behavior of recursive include.
#[test]
fn recursive_include() {
BookTest::from_dir("includes/all_includes")
.run("build", |cmd| {
cmd.expect_stderr(str![[r#"
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
[TIMESTAMP] [ERROR] (mdbook::preprocess::links): Stack depth exceeded in recursive.md. Check for cyclic includes
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
"#]]);
})
.check_main_file(
"book/recursive.html",
str![[r#"
<p>Around the world, around the world
Around the world, around the world
Around the world, around the world
Around the world, around the world
Around the world, around the world
Around the world, around the world
Around the world, around the world
Around the world, around the world
Around the world, around the world
Around the world, around the world
Around the world, around the world</p>
"#]],
);
}
// Checks the behavior of `{{#playground}}` include.
#[test]
fn playground_include() {
BookTest::from_dir("includes/all_includes")
.check_main_file("book/playground.html",
str![[r##"
<h1 id="playground-includes"><a class="header" href="#playground-includes">Playground Includes</a></h1>
<pre><pre class="playground"><code class="language-rust">fn main() {
println!("Hello World!");
<span class="boring">
</span><span class="boring"> // You can even hide lines! :D
</span><span class="boring"> println!("I am hidden! Expand the code snippet to see me");
</span>}</code></pre></pre>
"##]]);
}
// Checks the behavior of `{{#rustdoc_include}}`.
#[test]
fn rustdoc_include() {
BookTest::from_dir("includes/all_includes")
.check_main_file("book/rustdoc.html",
str![[r##"
<h1 id="rustdoc-includes"><a class="header" href="#rustdoc-includes">Rustdoc Includes</a></h1>
<h2 id="rustdoc-include-adds-the-rest-of-the-file-as-hidden"><a class="header" href="#rustdoc-include-adds-the-rest-of-the-file-as-hidden">Rustdoc include adds the rest of the file as hidden</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">fn some_function() {
</span><span class="boring"> println!("some function");
</span><span class="boring">}
</span><span class="boring">
</span>fn main() {
some_function();
}</code></pre></pre>
<h2 id="rustdoc-include-works-with-anchors-too"><a class="header" href="#rustdoc-include-works-with-anchors-too">Rustdoc include works with anchors too</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">fn some_other_function() {
</span><span class="boring"> println!("unused anchor");
</span><span class="boring">}
</span><span class="boring">
</span>fn main() {
some_other_function();
}</code></pre></pre>
"##]]);
}

View file

@ -0,0 +1,6 @@
[book]
authors = ["Eric Huss"]
language = "en"
multilingual = false
src = "src"
title = "all_includes"

View file

@ -0,0 +1,8 @@
# Summary
- [Basic Includes](./includes.md)
- [Relative Includes](./relative/includes.md)
- [Recursive Includes](./recursive.md)
- [Include Anchors](./anchors.md)
- [Rustdoc Includes](./rustdoc.md)
- [Playground Includes](./playground.md)

View file

@ -0,0 +1,5 @@
# Include Anchors
```rust
{{#include nested-test-with-anchors.rs:myanchor}}
```

View file

@ -0,0 +1,4 @@
# Basic Includes
{{#include sample.md}}

View file

@ -0,0 +1,6 @@
// This is a test of includes with anchors.
// ANCHOR: myanchor
// ANCHOR: unendinganchor
let x = 1;
// ANCHOR_END: myanchor

View file

@ -1,6 +1,6 @@
fn some_other_function() {
// ANCHOR: unused-anchor-that-should-be-stripped
assert!($TEST_STATUS);
println!("unused anchor");
// ANCHOR_END: unused-anchor-that-should-be-stripped
}

View file

@ -1,5 +1,5 @@
fn some_function() {
assert!($TEST_STATUS);
println!("some function");
}
fn main() {

View file

@ -0,0 +1,3 @@
# Playground Includes
{{#playground example.rs}}

View file

@ -0,0 +1,3 @@
# Relative Includes
{{#include ../sample.md}}

View file

@ -0,0 +1,13 @@
# Rustdoc Includes
## Rustdoc include adds the rest of the file as hidden
```rust
{{#rustdoc_include partially-included-test.rs:5:7}}
```
## Rustdoc include works with anchors too
```rust
{{#rustdoc_include partially-included-test-with-anchors.rs:rustdoc-include-anchor}}
```

View file

@ -0,0 +1,3 @@
## Sample
This is a sample include.

38
tests/testsuite/index.rs Normal file
View file

@ -0,0 +1,38 @@
//! Tests for the index preprocessor.
use crate::prelude::*;
// Checks basic README to index.html conversion.
#[test]
fn readme_to_index() {
let mut test = BookTest::from_dir("index/basic_readme");
test.check_main_file(
"book/index.html",
str![[r##"<h1 id="intro"><a class="header" href="#intro">Intro</a></h1>"##]],
)
.check_main_file(
"book/first/index.html",
str![[r##"<h1 id="first"><a class="header" href="#first">First</a></h1>"##]],
)
.check_main_file(
"book/second/index.html",
str![[r##"<h1 id="second"><a class="header" href="#second">Second</a></h1>"##]],
)
.check_toc_js(str![[r#"
<ol class="chapter">
<li class="chapter-item expanded affix ">
<a href="index.html">Intro</a>
</li>
<li class="chapter-item expanded ">
<a href="first/index.html">
<strong aria-hidden="true">1.</strong> First</a>
</li>
<li class="chapter-item expanded ">
<a href="second/index.html">
<strong aria-hidden="true">2.</strong> Second</a>
</li>
</ol>
"#]]);
assert!(test.dir.join("book/index.html").exists());
assert!(!test.dir.join("book/README.html").exists());
}

View file

@ -0,0 +1,2 @@
[book]
title = "basic_readme"

View file

@ -0,0 +1 @@
# Intro

View file

@ -0,0 +1,6 @@
# Summary
[Intro](./README.md)
- [First](first/README)
- [Second](second/Readme.md)

View file

@ -0,0 +1 @@
# First

View file

@ -0,0 +1 @@
# Second

253
tests/testsuite/init.rs Normal file
View file

@ -0,0 +1,253 @@
//! Tests for `mdbook init`.
use crate::prelude::*;
use mdbook::{Config, MDBook};
use std::path::PathBuf;
// Tests "init" with no args.
#[test]
fn basic_init() {
let mut test = BookTest::empty();
test.run("init", |cmd| {
cmd.expect_stdout(str![[r#"
Do you want a .gitignore to be created? (y/n)
What title would you like to give the book?
All done, no errors...
"#]])
.expect_stderr(str![[r#"
[TIMESTAMP] [INFO] (mdbook::book::init): Creating a new book with stub content
"#]]);
})
.check_file(
"book.toml",
str![[r#"
[book]
authors = []
language = "en"
src = "src"
"#]],
)
.check_file(
"src/SUMMARY.md",
str![[r#"
# Summary
- [Chapter 1](./chapter_1.md)
"#]],
)
.check_file(
"src/chapter_1.md",
str![[r#"
# Chapter 1
"#]],
)
.check_main_file(
"book/chapter_1.html",
str![[r##"<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>"##]],
);
assert!(!test.dir.join(".gitignore").exists());
assert!(test.dir.join("book").exists());
}
// Test init via API. This does a little less than the CLI does.
#[test]
fn init_api() {
let mut test = BookTest::empty();
MDBook::init(&test.dir).build().unwrap();
test.check_file_list(
".",
str![[r#"
book
book.toml
src
src/SUMMARY.md
src/chapter_1.md
"#]],
);
}
// Run `mdbook init` with `--force` to skip the confirmation prompts
#[test]
fn init_force() {
let mut test = BookTest::empty();
test.run("init --force", |cmd| {
cmd.expect_stdout(str![[r#"
All done, no errors...
"#]])
.expect_stderr(str![[r#"
[TIMESTAMP] [INFO] (mdbook::book::init): Creating a new book with stub content
"#]]);
})
.check_file(
"book.toml",
str![[r#"
[book]
authors = []
language = "en"
src = "src"
"#]],
);
assert!(!test.dir.join(".gitignore").exists());
}
// Run `mdbook init` with `--title` without git config.
//
// Regression test for https://github.com/rust-lang/mdBook/issues/2485
#[test]
fn no_git_config_with_title() {
let mut test = BookTest::empty();
test.run("init", |cmd| {
cmd.expect_stdout(str![[r#"
Do you want a .gitignore to be created? (y/n)
All done, no errors...
"#]])
.expect_stderr(str![[r#"
[TIMESTAMP] [INFO] (mdbook::book::init): Creating a new book with stub content
"#]])
.args(&["--title", "Example title"]);
})
.check_file(
"book.toml",
str![[r#"
[book]
authors = []
language = "en"
src = "src"
title = "Example title"
"#]],
);
assert!(!test.dir.join(".gitignore").exists());
}
// Run `mdbook init` in a directory containing a SUMMARY.md should create the
// files listed in the summary.
#[test]
fn init_from_summary() {
BookTest::from_dir("init/init_from_summary")
.run("init", |_| {})
.check_file(
"src/intro.md",
str![[r#"
# intro
"#]],
)
.check_file(
"src/first.md",
str![[r#"
# First chapter
"#]],
)
.check_file(
"src/outro.md",
str![[r#"
# outro
"#]],
);
}
// Set some custom arguments for where to place the source and destination
// files, then call `mdbook init`.
#[test]
fn init_with_custom_book_and_src_locations() {
let mut test = BookTest::empty();
let mut cfg = Config::default();
cfg.book.src = PathBuf::from("in");
cfg.build.build_dir = PathBuf::from("out");
MDBook::init(&test.dir).with_config(cfg).build().unwrap();
test.check_file(
"book.toml",
str![[r#"
[book]
authors = []
language = "en"
src = "in"
[build]
build-dir = "out"
create-missing = true
extra-watch-dirs = []
use-default-preprocessors = true
"#]],
)
.check_file(
"in/SUMMARY.md",
str![[r#"
# Summary
- [Chapter 1](./chapter_1.md)
"#]],
)
.check_file(
"in/chapter_1.md",
str![[r#"
# Chapter 1
"#]],
);
assert!(test.dir.join("out").exists());
}
// Copies the theme into the initialized directory.
#[test]
fn copy_theme() {
BookTest::empty()
.run("init --theme", |_| {})
.check_file_list(
".",
str![[r#"
book
book.toml
src
src/SUMMARY.md
src/chapter_1.md
theme
theme/book.js
theme/css
theme/css/chrome.css
theme/css/general.css
theme/css/print.css
theme/css/variables.css
theme/favicon.png
theme/favicon.svg
theme/fonts
theme/fonts/OPEN-SANS-LICENSE.txt
theme/fonts/SOURCE-CODE-PRO-LICENSE.txt
theme/fonts/fonts.css
theme/fonts/open-sans-v17-all-charsets-300.woff2
theme/fonts/open-sans-v17-all-charsets-300italic.woff2
theme/fonts/open-sans-v17-all-charsets-600.woff2
theme/fonts/open-sans-v17-all-charsets-600italic.woff2
theme/fonts/open-sans-v17-all-charsets-700.woff2
theme/fonts/open-sans-v17-all-charsets-700italic.woff2
theme/fonts/open-sans-v17-all-charsets-800.woff2
theme/fonts/open-sans-v17-all-charsets-800italic.woff2
theme/fonts/open-sans-v17-all-charsets-italic.woff2
theme/fonts/open-sans-v17-all-charsets-regular.woff2
theme/fonts/source-code-pro-v11-all-charsets-500.woff2
theme/highlight.css
theme/highlight.js
theme/index.hbs
"#]],
);
}

View file

@ -0,0 +1,8 @@
# Summary
[intro](intro.md)
- [First chapter](first.md)
[outro](outro.md)

27
tests/testsuite/main.rs Normal file
View file

@ -0,0 +1,27 @@
//! Main testsuite for exercising all functionality of mdBook.
//!
//! See README.md for documentation.
mod book_test;
mod build;
mod cli;
mod includes;
mod index;
mod init;
mod markdown;
mod playground;
mod preprocessor;
mod print;
mod redirects;
mod renderer;
mod rendering;
#[cfg(feature = "search")]
mod search;
mod test;
mod theme;
mod toc;
mod prelude {
pub use crate::book_test::BookTest;
pub use snapbox::str;
}

146
tests/testsuite/markdown.rs Normal file
View file

@ -0,0 +1,146 @@
//! Tests for special markdown rendering.
use crate::prelude::*;
// Checks custom header id and classes.
#[test]
fn custom_header_attributes() {
BookTest::from_dir("markdown/custom_header_attributes")
.check_main_file("book/custom_header_attributes.html", str![[r##"
<h1 id="attrs"><a class="header" href="#attrs">Heading Attributes</a></h1>
<h2 id="heading-with-classes" class="class1 class2"><a class="header" href="#heading-with-classes">Heading with classes</a></h2>
<h2 id="both" class="class1 class2"><a class="header" href="#both">Heading with id and classes</a></h2>
"##]]);
}
// Test for a variety of footnote renderings.
#[test]
fn footnotes() {
BookTest::from_dir("markdown/footnotes")
.check_main_file("book/footnotes.html", str![[r##"
<h1 id="footnote-tests"><a class="header" href="#footnote-tests">Footnote tests</a></h1>
<p>Footnote example<sup class="footnote-reference" id="fr-1-1"><a href="#footnote-1">1</a></sup>, or with a word<sup class="footnote-reference" id="fr-word-1"><a href="#footnote-word">2</a></sup>.</p>
<p>There are multiple references to word<sup class="footnote-reference" id="fr-word-2"><a href="#footnote-word">2</a></sup>.</p>
<p>Footnote without a paragraph<sup class="footnote-reference" id="fr-para-1"><a href="#footnote-para">3</a></sup></p>
<p>Footnote with multiple paragraphs<sup class="footnote-reference" id="fr-multiple-1"><a href="#footnote-multiple">4</a></sup></p>
<p>Footnote name with wacky characters<sup class="footnote-reference" id="fr-&quot;wacky&quot;-1"><a href="#footnote-&quot;wacky&quot;">5</a></sup></p>
<p>Testing when referring to something earlier.<sup class="footnote-reference" id="fr-define-before-use-1"><a href="#footnote-define-before-use">6</a></sup></p>
<hr>
<ol class="footnote-definition"><li id="footnote-1">
<p>This is a footnote. <a href="#fr-1-1"></a> <a href="#fr-1-2">2</a></p>
</li>
<li id="footnote-word">
<p>A longer footnote.
With multiple lines. <a href="other.html">Link to other</a>.
With a reference inside.<sup class="footnote-reference" id="fr-1-2"><a href="#footnote-1">1</a></sup> <a href="#fr-word-1"></a> <a href="#fr-word-2">2</a></p>
</li>
<li id="footnote-para">
<ol>
<li>Item one
<ol>
<li>Sub-item</li>
</ol>
</li>
<li>Item two</li>
</ol>
<a href="#fr-para-1"></a></li>
<li id="footnote-multiple">
<p>One</p>
<p>Two</p>
<p>Three <a href="#fr-multiple-1"></a></p>
</li>
<li id="footnote-&quot;wacky&quot;">
<p>Testing footnote id with special characters. <a href="#fr-&quot;wacky&quot;-1"></a></p>
</li>
<li id="footnote-define-before-use">
<p>This is defined before it is referred to. <a href="#fr-define-before-use-1"></a></p>
</li>
</ol>
"##]]);
}
// Basic table test.
#[test]
fn tables() {
BookTest::from_dir("markdown/tables").check_main_file(
"book/tables.html",
str![[r##"
<h1 id="tables"><a class="header" href="#tables">Tables</a></h1>
<div class="table-wrapper"><table><thead><tr><th>foo</th><th>bar</th></tr></thead><tbody>
<tr><td>baz</td><td>bim</td></tr>
<tr><td>Backslash in code</td><td><code>/</code></td></tr>
<tr><td>Double back in code</td><td><code>//</code></td></tr>
<tr><td>Pipe in code</td><td><code>|</code></td></tr>
<tr><td>Pipe in code2</td><td><code>test | inside</code></td></tr>
</tbody></table>
</div>
"##]],
);
}
// Strikethrough test.
#[test]
fn strikethrough() {
BookTest::from_dir("markdown/strikethrough").check_main_file(
"book/strikethrough.html",
str![[r##"
<h1 id="strikethrough"><a class="header" href="#strikethrough">Strikethrough</a></h1>
<p><del>strikethrough example</del></p>
"##]],
);
}
// Tasklist test.
#[test]
fn tasklists() {
BookTest::from_dir("markdown/tasklists").check_main_file(
"book/tasklists.html",
str![[r##"
<h2 id="tasklisks"><a class="header" href="#tasklisks">Tasklisks</a></h2>
<ul>
<li><input disabled="" type="checkbox" checked=""/>
Apples</li>
<li><input disabled="" type="checkbox" checked=""/>
Broccoli</li>
<li><input disabled="" type="checkbox"/>
Carrots</li>
</ul>
"##]],
);
}
// Smart punctuation test.
#[test]
fn smart_punctuation() {
BookTest::from_dir("markdown/smart_punctuation")
// Default is off.
.check_main_file(
"book/smart_punctuation.html",
str![[r##"
<h1 id="smart-punctuation"><a class="header" href="#smart-punctuation">Smart Punctuation</a></h1>
<ul>
<li>En dash: --</li>
<li>Em dash: ---</li>
<li>Ellipsis: ...</li>
<li>Double quote: "quote"</li>
<li>Single quote: 'quote'</li>
</ul>
"##]],
)
.run("build", |cmd| {
cmd.env("MDBOOK_OUTPUT__HTML__SMART_PUNCTUATION", "true");
})
.check_main_file(
"book/smart_punctuation.html",
str![[r##"
<h1 id="smart-punctuation"><a class="header" href="#smart-punctuation">Smart Punctuation</a></h1>
<ul>
<li>En dash: </li>
<li>Em dash: </li>
<li>Ellipsis: </li>
<li>Double quote: quote</li>
<li>Single quote: quote</li>
</ul>
"##]],
);
}

View file

@ -0,0 +1,2 @@
[book]
title = "custom_header_attributes"

View file

@ -0,0 +1,3 @@
# Summary
- [Heading Attributes](./custom_header_attributes.md)

View file

@ -0,0 +1 @@
- [Footnotes](footnotes.md)

View file

@ -1,21 +1,11 @@
# Markdown tests
Tests for some markdown output.
## Tables
| foo | bar |
| --- | --- |
| baz | bim |
## Footnotes
# Footnote tests
Footnote example[^1], or with a word[^word].
[^1]: This is a footnote.
[^word]: A longer footnote.
With multiple lines. [Link to unicode](unicode.md).
With multiple lines. [Link to other](other.md).
With a reference inside.[^1]
There are multiple references to word[^word].
@ -31,10 +21,12 @@ Footnote with multiple paragraphs[^multiple]
[^define-before-use]: This is defined before it is referred to.
<!-- Using <p> tags to work around rustdoc issue, this should move to a separate book.
https://github.com/rust-lang/rust/issues/139064
-->
[^multiple]: <p>One</p><p>Two</p><p>Three</p>
[^multiple]:
One
Two
Three
[^unused]: This footnote is defined by not used.
@ -43,13 +35,3 @@ Footnote name with wacky characters[^"wacky"]
[^"wacky"]: Testing footnote id with special characters.
Testing when referring to something earlier.[^define-before-use]
## Strikethrough
~~strikethrough example~~
## Tasklisks
- [X] Apples
- [X] Broccoli
- [ ] Carrots

View file

@ -0,0 +1 @@
- [Smart Punctuation](smart_punctuation.md)

View file

@ -0,0 +1,7 @@
# Smart Punctuation
- En dash: --
- Em dash: ---
- Ellipsis: ...
- Double quote: "quote"
- Single quote: 'quote'

View file

@ -0,0 +1 @@
- [Strikethrough](strikethrough.md)

View file

@ -0,0 +1,4 @@
# Strikethrough
~~strikethrough example~~

View file

@ -0,0 +1 @@
- [Tables](tables.md)

View file

@ -0,0 +1,9 @@
# Tables
| foo | bar |
| --- | --- |
| baz | bim |
| Backslash in code | `\` |
| Double back in code | `\\` |
| Pipe in code | `\|` |
| Pipe in code2 | `test \| inside` |

View file

@ -0,0 +1 @@
- [Tasklists](tasklists.md)

View file

@ -0,0 +1,5 @@
## Tasklisks
- [X] Apples
- [X] Broccoli
- [ ] Carrots

View file

@ -0,0 +1,30 @@
//! Tests for Rust playground support.
use crate::prelude::*;
// Verifies that a rust codeblock gets the playground class.
#[test]
fn playground_on_rust_code() {
BookTest::from_dir("playground/playground_on_rust_code").check_main_file(
"book/index.html",
str![[r##"
<h1 id="rust-sample"><a class="header" href="#rust-sample">Rust Sample</a></h1>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let x = 1;
<span class="boring">}</span></code></pre></pre>
"##]],
);
}
// When the playground is disabled, there should be no playground class.
#[test]
fn disabled_playground() {
BookTest::from_dir("playground/disabled_playground").check_main_file(
"book/index.html",
str![[r##"
<h1 id="rust-sample"><a class="header" href="#rust-sample">Rust Sample</a></h1>
<pre><code class="language-rust">let x = 1;</code></pre>
"##]],
);
}

View file

@ -0,0 +1,5 @@
[book]
title = "playground_on_rust_code"
[output.html.playground]
runnable = false

View file

@ -0,0 +1,3 @@
# Summary
- [Rust Playground](./index.md)

View file

@ -0,0 +1,5 @@
# Rust Sample
```rust
let x = 1;
```

View file

@ -0,0 +1,2 @@
[book]
title = "playground_on_rust_code"

View file

@ -0,0 +1,3 @@
# Summary
- [Rust Playground](./index.md)

View file

@ -0,0 +1,5 @@
# Rust Sample
```rust
let x = 1;
```

View file

@ -0,0 +1,95 @@
//! Tests for custom preprocessors.
use crate::prelude::*;
use mdbook::book::Book;
use mdbook::errors::Result;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
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"]);
}
// 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#"
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
"#]]);
});
}
// 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#"
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
Boom!!1!
[TIMESTAMP] [ERROR] (mdbook::utils): 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(),
)
}
#[test]
fn example_supports_whatever() {
let cmd = example();
let got = cmd.supports_renderer("whatever");
assert_eq!(got, true);
}
#[test]
fn example_doesnt_support_not_supported() {
let cmd = example();
let got = cmd.supports_renderer("not-supported");
assert_eq!(got, false);
}

View file

@ -0,0 +1,4 @@
[preprocessor.nop-preprocessor]
command = "cargo run --quiet --example nop-preprocessor --"
blow-up = true

View file

@ -0,0 +1,2 @@
[preprocessor.nop-preprocessor]
command = "cargo run --quiet --example nop-preprocessor --"

Some files were not shown because too many files have changed in this diff Show more