Skip to content
Merged
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ ws = { version = "0.7", optional = true}
[build-dependencies]
error-chain = "0.11"

[dev-dependencies]
select = "0.4"
pretty_assertions = "0.4"
walkdir = "1.0"

[features]
default = ["output", "watch", "serve"]
debug = []
Expand Down
87 changes: 0 additions & 87 deletions tests/dummy/mod.rs

This file was deleted.

130 changes: 130 additions & 0 deletions tests/dummy_book/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! 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)]
extern crate mdbook;
extern crate tempdir;
extern crate walkdir;

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

// The funny `self::` here is because we've got an `extern crate ...` and are
// in a submodule
use self::tempdir::TempDir;
use self::mdbook::MDBook;
use self::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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

why not implement it directly? It's a single bool.

DummyBook::default()
}

/// 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 Self {
self.passing_test = test_passes;
self
}

/// Write a book to a temporary directory using the provided settings.
///
/// # Note
///
/// If this fails for any reason it will `panic!()`. If we can't write to a
/// temporary directory then chances are you've got bigger problems...
pub fn build(&self) -> TempDir {
let temp = TempDir::new("dummy_book").unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

unwraps trigger my OCD ;) (and I tend to grep for them as TODO's) we already have error_chain so using -> Result<()> is trivial or maybe we could at least use expects.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I think I was just being lazy and not wanting to deal with errors properly. It does just end up pushing the unwrap()'s up one level to the tests though...

let dummy_book_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/dummy_book");
recursive_copy(&dummy_book_root, temp.path()).expect("Couldn't copy files into a \
temporary directory");

let sub_pattern = if self.passing_test { "true" } else { "false" };
let file_containing_test = temp.path().join("src/first/nested.md");
poor_mans_sed(&file_containing_test, "$TEST_STATUS", sub_pattern);

temp
}
}

fn poor_mans_sed(filename: &Path, from: &str, to: &str) {
Copy link
Contributor

Choose a reason for hiding this comment

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

the name is not super descriptive. I'd rename it to replace_file_contents

let contents = read_file(filename).unwrap();
File::create(filename).unwrap()
.write_all(contents.replace(from, to).as_bytes())
.unwrap();
}

impl Default for DummyBook {
Copy link
Contributor

Choose a reason for hiding this comment

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

do we need a separate Default impl? Why not just implement new directly?

fn default() -> DummyBook {
DummyBook { passing_test: true }
}
}

/// 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")
Copy link
Contributor

Choose a reason for hiding this comment

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

we can use the file_to_string fn from utils

.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);
}
}



/// 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<(), Box<Error>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

we already use error_chain how about we use it also in this context instead of boxing the errors?
also
copy_files_except_ext already exists

let from = from.as_ref();
let to = to.as_ref();

for entry in WalkDir::new(&from) {
let entry = entry?;

let original_location = entry.path();
let relative = original_location.strip_prefix(&from)?;
let new_location = to.join(relative);

if original_location.is_file() {
if let Some(parent) = new_location.parent() {
fs::create_dir_all(parent)?;
}

fs::copy(&original_location, &new_location)?;
}
}

Ok(())
}

pub fn read_file<P: AsRef<Path>>(filename: P) -> Result<String, Box<Error>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

there is already a file_to_string

let mut contents = String::new();
File::open(filename)?.read_to_string(&mut contents)?;

Ok(contents)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

[Conclusion](./conclusion.md)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
27 changes: 0 additions & 27 deletions tests/helpers/mod.rs

This file was deleted.

120 changes: 115 additions & 5 deletions tests/rendered_output.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
extern crate mdbook;
extern crate tempdir;
#[macro_use]
extern crate pretty_assertions;
extern crate select;
extern crate walkdir;

mod dummy;
mod helpers;
mod dummy_book;

use dummy::DummyBook;
use helpers::assert_contains_strings;
use dummy_book::{assert_contains_strings, DummyBook};
use mdbook::MDBook;

use std::path::Path;
use walkdir::{DirEntry, WalkDir, WalkDirIterator};
Copy link
Contributor

Choose a reason for hiding this comment

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

we probably should move to new walkdir which does not have WalkDirIterator (but It's best left for separate PR)

use select::document::Document;
use select::predicate::{Class, Name, Predicate};


const BOOK_ROOT: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/dummy_book");
const TOC_TOP_LEVEL: &[&'static str] = &["1. First Chapter",
"2. Second Chapter",
"Conclusion",
"Introduction"];
const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter"];

/// Make sure you can load the dummy book and build it without panicking.
#[test]
Expand Down Expand Up @@ -111,3 +124,100 @@ fn chapter_content_appears_in_rendered_document() {
assert_contains_strings(path, &[text]);
}
}


/// Apply a series of predicates to some root predicate, where each
/// successive predicate is the descendant of the last one.
macro_rules! descendants {
($root:expr, $($child:expr),*) => {
$root
$(
.descendant($child)
)*
};
}


/// Make sure that all `*.md` files (excluding `SUMMARY.md`) were rendered
/// and placed in the `book` directory with their extensions set to `*.html`.
#[test]
fn chapter_files_were_rendered_to_html() {
let temp = DummyBook::new().build();
let src = Path::new(BOOK_ROOT).join("src");

let chapter_files = WalkDir::new(&src).into_iter()
.filter_entry(|entry| entry_ends_with(entry, ".md"))
.filter_map(|entry| entry.ok())
.map(|entry| entry.path().to_path_buf())
.filter(|path| path.file_name().unwrap() != "SUMMARY");
Copy link
Contributor

Choose a reason for hiding this comment

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

how about .filter(|path| path.file_name().and_then(OsStr::to_str) != Some("SUMMARY"));
or at least expect?
Also shouldn't it be "SUMMARY.md" not "SUMMARY"?


for chapter in chapter_files {
let rendered_location = temp.path().join(chapter.strip_prefix(&src).unwrap())
.with_extension("html");
assert!(rendered_location.exists(),
"{} doesn't exits",
rendered_location.display());
}
}

fn entry_ends_with(entry: &DirEntry, ending: &str) -> bool {
entry.file_name().to_string_lossy().ends_with(ending)
}

/// Read the main page (`book/index.html`) and expose it as a DOM which we
/// can search with the `select` crate
fn root_index_html() -> Document {
let temp = DummyBook::new().build();
MDBook::new(temp.path()).build().unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

how about we return Result<Document>?


let index_page = temp.path().join("book").join("index.html");
println!("{}", index_page.display());
for thing in temp.path().read_dir().unwrap() {
println!("{:?}", thing);
}
let html = dummy_book::read_file(&index_page).unwrap();
Document::from(html.as_str())
}

#[test]
fn check_second_toc_level() {
let doc = root_index_html();
let mut should_be = Vec::from(TOC_SECOND_LEVEL);
should_be.sort();

let pred = descendants!(Class("chapter"), Name("li"), Name("li"), Name("a"));

let mut children_of_children: Vec<String> =
Copy link
Contributor

Choose a reason for hiding this comment

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

Vec<_> type ascription should be enough

doc.find(pred).map(|elem| elem.text().trim().to_string())
.collect();
children_of_children.sort();

assert_eq!(children_of_children, should_be);
}

#[test]
fn check_first_toc_level() {
let doc = root_index_html();
let mut should_be = Vec::from(TOC_TOP_LEVEL);

should_be.extend(TOC_SECOND_LEVEL);
should_be.sort();

let pred = descendants!(Class("chapter"), Name("li"), Name("a"));

let mut children: Vec<String> = doc.find(pred).map(|elem| elem.text().trim().to_string())
Copy link
Contributor

Choose a reason for hiding this comment

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

Vec<_> type ascription should be enough

.collect();
children.sort();

assert_eq!(children, should_be);
}

#[test]
fn check_spacers() {
let doc = root_index_html();
let should_be = 1;

let num_spacers =
doc.find(Class("chapter").descendant(Name("li").and(Class("spacer")))).count();
assert_eq!(num_spacers, should_be);
}
Loading