Skip to content
111 changes: 111 additions & 0 deletions tests/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! Helpers for tests which exercise the overall application, in particular
//! the `MDBook` initialization and build/rendering process.
//!
//! This will create an entire book in a temporary directory using some
//! dummy content.

#![allow(dead_code, unused_variables, unused_imports)]
extern crate tempdir;

use std::path::Path;
use std::fs::{self, File};
use std::io::{Read, Write};

use tempdir::TempDir;


const SUMMARY_MD: &'static str = "# Summary

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an idea for maintainability:

We could make a simple directory with a dummy book like:

tests
├── config.rs
├── helpers.rs
├── init.rs
├── jsonconfig.rs
├── rendered_output.rs
├── test-book
│   ├── SUMMARY.md
│   ├── conclusion.md
│   ├── first
│   │   ├── index.md
│   │   └── nested.md
│   └── second.md
├── testing.rs
└── tomlconfig.rs

and then include the files like this

static SUMMARY: &str = include_str!("test-book/SUMMARY.md");

It makes it easier to edit and expand the book when we add more tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, since you convert to bytes later you can directly use ìnclude_bytes!()


[Introduction](intro.md)

- [First Chapter](./first/index.md)
- [Nested Chapter](./first/nested.md)
- [Second Chapter](./second.md)

[Conclusion](./conclusion.md)
";

const INTRO: &'static str = "# Introduction

Here's some interesting text...";

const FIRST: &'static str = "# First Chapter

more text.";

const NESTED: &'static str = r#"# Nested Chapter

This file has some testable code.

```rust
assert!($TEST_STATUS);
```"#;

const SECOND: &'static str = "# Second Chapter";

const CONCLUSION: &'static str = "# Conclusion";


/// 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.
pub fn create_book(passing_test: bool) -> TempDir {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little worried that we will end up with a huge number of arguments for this function in the future.. Currently there is only one argument passing_test, but as more tests are added I am afraid that more arguments will appear too.

Maybe we should make this a builder struct with a good set of default options? In the simplest case we will have to call BookBuilder::create() but in more complex scenarios, when we want some variation, we can just pass the arguments that need changing. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the BookBuilder idea! It'll make things a lot easier in the long run, and also integrates quite nicely with your earlier dummy book directory comment.

let temp = TempDir::new("dummy_book").unwrap();

let src = temp.path().join("src");
fs::create_dir_all(&src).unwrap();

File::create(src.join("SUMMARY.md"))
.unwrap()
.write_all(SUMMARY_MD.as_bytes())
.unwrap();
File::create(src.join("intro.md"))
.unwrap()
.write_all(INTRO.as_bytes())
.unwrap();

let first = src.join("first");
fs::create_dir_all(&first).unwrap();
File::create(first.join("index.md"))
.unwrap()
.write_all(FIRST.as_bytes())
.unwrap();

let to_substitute = if passing_test { "true" } else { "false" };
let nested_text = NESTED.replace("$TEST_STATUS", to_substitute);
File::create(first.join("nested.md"))
.unwrap()
.write_all(nested_text.as_bytes())
.unwrap();

File::create(src.join("second.md"))
.unwrap()
.write_all(SECOND.as_bytes())
.unwrap();
File::create(src.join("conclusion.md"))
.unwrap()
.write_all(CONCLUSION.as_bytes())
.unwrap();

temp
}

/// 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 mut content = String::new();
File::open(&filename)
.expect("Couldn't open the provided file")
.read_to_string(&mut content)
.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);
}
}
43 changes: 43 additions & 0 deletions tests/init.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
extern crate mdbook;
extern crate tempdir;

use tempdir::TempDir;
use mdbook::MDBook;


#[test]
fn run_mdbook_init() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could add one or two lines of comments to explain what each test does?
It's easier if people don't have to read the code to understand what exactly the test is asserting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. run_mdbook_init is a pretty useless name. I'll rename it to something longer like base_mdbook_init_should_create_default_content.

let created_files = vec!["book", "src", "src/SUMMARY.md", "src/chapter_1.md"];

let temp = TempDir::new("mdbook").unwrap();
for file in &created_files {
assert!(!temp.path().join(file).exists());
}

let mut md = MDBook::new(temp.path());
md.init().unwrap();

for file in &created_files {
assert!(temp.path().join(file).exists(), "{} doesn't exist", file);
}
}

#[test]
fn run_mdbook_init_with_custom_args() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe the name can reflect that it specifically tests the source and output destination?

let created_files = vec!["out", "in", "in/SUMMARY.md", "in/chapter_1.md"];

let temp = TempDir::new("mdbook").unwrap();
for file in &created_files {
assert!(!temp.path().join(file).exists());
}

let mut md = MDBook::new(temp.path())
.with_source("in")
.with_destination("out");

md.init().unwrap();

for file in &created_files {
assert!(temp.path().join(file).exists(), "{} doesn't exist", file);
}
}
108 changes: 108 additions & 0 deletions tests/rendered_output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
extern crate mdbook;
extern crate tempdir;

mod helpers;
use mdbook::MDBook;


#[test]
fn build_the_dummy_book() {
let temp = helpers::create_book(true);
let mut md = MDBook::new(temp.path());

md.build().unwrap();
}

#[test]
fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
let temp = helpers::create_book(false);
let mut md = MDBook::new(temp.path());

assert!(!temp.path().join("book").exists());
md.build().unwrap();

assert!(temp.path().join("book").exists());
assert!(temp.path().join("book").join("index.html").exists());
}

#[test]
fn make_sure_bottom_level_files_contain_links_to_chapters() {
let temp = helpers::create_book(false);
let mut md = MDBook::new(temp.path());
md.build().unwrap();

let dest = temp.path().join("book");
let links = vec![
"intro.html",
"first/index.html",
"first/nested.html",
"second.html",
"conclusion.html",
];

let files_in_bottom_dir = vec!["index.html", "intro.html", "second.html", "conclusion.html"];

for filename in files_in_bottom_dir {
helpers::assert_contains_strings(dest.join(filename), &links);
}
}

#[test]
fn check_correct_cross_links_in_nested_dir() {
let temp = helpers::create_book(false);
let mut md = MDBook::new(temp.path());
md.build().unwrap();

let first = temp.path().join("book").join("first");
let links = vec![
r#"<base href="../">"#,
"intro.html",
"first/index.html",
"first/nested.html",
"second.html",
"conclusion.html",
];

let files_in_nested_dir = vec!["index.html", "nested.html"];

for filename in files_in_nested_dir {
helpers::assert_contains_strings(first.join(filename), &links);
}
}

#[test]
fn rendered_code_has_playpen_stuff() {
let temp = helpers::create_book(true);
let mut md = MDBook::new(temp.path());
md.build().unwrap();

let nested = temp.path().join("book/first/nested.html");
let playpen_class = vec![r#"class="playpen""#];

helpers::assert_contains_strings(nested, &playpen_class);

let book_js = temp.path().join("book/book.js");
helpers::assert_contains_strings(book_js, &[".playpen"]);
}

#[test]
fn chapter_content_appears_in_rendered_document() {
let content = vec![
("index.html", "Here's some interesting text"),
("second.html", "Second Chapter"),
("first/nested.html", "testable code"),
("first/index.html", "more text"),
("conclusion.html", "Conclusion"),
];

let temp = helpers::create_book(true);
let mut md = MDBook::new(temp.path());
md.build().unwrap();

let destination = temp.path().join("book");

for (filename, text) in content {
let path = destination.join(filename);
helpers::assert_contains_strings(path, &[text]);
}
}
22 changes: 22 additions & 0 deletions tests/testing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
extern crate tempdir;
extern crate mdbook;

mod helpers;
use mdbook::MDBook;


#[test]
fn mdbook_can_correctly_test_a_passing_book() {
let temp = helpers::create_book(true);
let mut md = MDBook::new(temp.path());

assert!(md.test(vec![]).is_ok());
}

#[test]
fn mdbook_detects_book_with_failing_tests() {
let temp = helpers::create_book(false);
let mut md: MDBook = MDBook::new(temp.path());

assert!(md.test(vec![]).is_err());
}