Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extern crate #2503

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions guide/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ language = "en"

[rust]
edition = "2018"
package-dir = "../"

[output.html]
smart-punctuation = true
Expand Down
1 change: 1 addition & 0 deletions guide/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [Installation](guide/installation.md)
- [Reading Books](guide/reading.md)
- [Creating a Book](guide/creating.md)
- [Writing Code Samples](guide/writing.md)

# Reference Guide

Expand Down
34 changes: 11 additions & 23 deletions guide/src/cli/test.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
# The test command

When writing a book, you sometimes need to automate some tests. For example,
When writing a book, you may want to provide some code samples,
and it's important that these be accurate.
For example,
[The Rust Programming Book](https://doc.rust-lang.org/stable/book/) uses a lot
of code examples that could get outdated. Therefore it is very important for
of code samples that could get outdated as the language evolves. Therefore it is very important for
them to be able to automatically test these code examples.

mdBook supports a `test` command that will run all available tests in a book. At
the moment, only Rust tests are supported.
mdBook supports a `test` command that will run code samples as doc tests for your book. At
the moment, only Rust doc tests are supported.

#### Disable tests on a code block

rustdoc doesn't test code blocks which contain the `ignore` attribute:

```rust,ignore
fn main() {}
```

rustdoc also doesn't test code blocks which specify a language other than Rust:

```markdown
**Foo**: _bar_
```

rustdoc *does* test code blocks which have no language specified:

```
This is going to cause an error!
```
For details on writing code samples and runnable code samples in your book, see [Writing](../guide/writing.md).

#### Specify a directory

Expand All @@ -39,6 +23,10 @@ mdbook test path/to/book

#### `--library-path`

> Note: This argument doesn't provide sufficient information for current Rust compilers.
Instead, add `package-dir` to your ***book.toml***, as described in [configuration](/format/configuration/general.md#rust-options).


The `--library-path` (`-L`) option allows you to add directories to the library
search path used by `rustdoc` when it builds and tests the examples. Multiple
directories can be specified with multiple options (`-L foo -L bar`) or with a
Expand Down
8 changes: 8 additions & 0 deletions guide/src/format/configuration/general.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,16 @@ integration.

```toml
[rust]
package-dir = "folder/for/Cargo.toml"
edition = "2015" # the default edition for code blocks
```

- **package-dir**: Folder containing a Cargo package whose targets and dependencies
you want to use in your book's code samples.
It must be specified if you want to test code samples with `use` statements, even if
there is a `Cargo.toml` in the folder containing the `book.toml`.
This can be a relative path, relative to the folder containing `book.toml`.

- **edition**: Rust edition to use by default for the code snippets. Default
is `"2015"`. Individual code blocks can be controlled with the `edition2015`,
`edition2018` or `edition2021` annotations, such as:
Expand All @@ -82,6 +89,7 @@ edition = "2015" # the default edition for code blocks
```
~~~


### Build options

This controls the build process of your book.
Expand Down
1 change: 1 addition & 0 deletions guide/src/guide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ This user guide provides an introduction to basic concepts of using mdBook.
- [Installation](installation.md)
- [Reading Books](reading.md)
- [Creating a Book](creating.md)
- [Writing Code Samples](writing.md)
132 changes: 132 additions & 0 deletions guide/src/guide/writing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Writing code samples and documentation tests

If your book is about software, a short code sample may communicate the point better than many words of explanation.
This section describes how to format samples and, perhaps more importantly, how to verify they compile and run
to ensue they stay aligned with the software APIs they describe.

Code blocks in your book are passed through mdBook and processed by rustdoc. For more details on structuring codeblocks and running doc tests,
refer to the [rustdoc book](https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html)

### Code blocks for sample code

You include a code sample in your book as a markdown fenced code block specifying `rust`, like so:

`````markdown
```rust
let four = 2 + 2;
assert_eq!(four, 4);
```
`````

This displays as:

```rust
let four = 2 + 2;
assert_eq!(four, 4);
```

Rustdoc will wrap this sample in a `fn main() {}` so that it can be compiled and even run by `mdbook test`.

#### Disable tests on a code block

rustdoc does not test code blocks which contain the `ignore` attribute:

`````markdown
```rust,ignore
fn main() {}
This would not compile anyway.
```
`````

rustdoc also doesn't test code blocks which specify a language other than Rust:

`````markdown
```markdown
**Foo**: _bar_
```
`````

rustdoc *does* test code blocks which have no language specified:

`````markdown
```
let four = 2 + 2;
assert_eq!(four, 4);
```
`````

### Hiding source lines within a sample

A longer sample may contain sections of boilerplate code that are not relevant to the current section of your book.
You can hide source lines within the code block prefixing them with `#_`
(that is a line starting with `#` followed by a single space), like so:

`````markdown
```rust
# use std::fs::File;
# use std::io::{Write,Result};
# fn main() -> Result<()> {
let mut file = File::create("foo.txt")?;
file.write_all(b"Hello, world!")?;
# Ok(())
# }
```
`````

This displays as:

```rust
# use std::fs::File;
# use std::io::{Write,Result};
# fn main() -> Result<()> {
let mut file = File::create("foo.txt")?;
file.write_all(b"Hello, world!")?;
# Ok(())
# }
```

Note that the code block displays an "show hidden lines" button in the upper right of the code block (when hovered over).

Note, too, that the sample provided its own `fn main(){}`, so the `use` statements could be positioned outside it.
When rustdoc sees the sample already provides `fn main`, it does *not* do its own wrapping.


### Tests using external crates

The previous example shows that you can `use` a crate within your sample.
But if the crate is an *external* crate, that is, one declared as a dependency in your
package `Cargo.toml`, rustc (the compiler invoked by rustdoc) needs
`-L` and `--extern` switches in order to compile it.
Cargo does this automatically for `cargo build` and `cargo rustdoc` and mdBook can as well.

To allow mdBook to determine the correct external crate information,
add `package-dir` to your ***book.toml**, as described in [configuration](/format/configuration/general.md#rust-options).
Note that mdBook runs a `cargo build` for the package to determine correct dependencies.

This example (borrowed from the `serde` crate documentation) compiles and runs in a properly configured book:

```rust
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Point {
x: i32,
y: i32,
}

fn main() {
let point = Point { x: 1, y: 2 };

// Convert the Point to a JSON string.
let serialized = serde_json::to_string(&point).unwrap();

// Prints serialized = {"x":1,"y":2}
println!("serialized = {}", serialized);

// Convert the JSON string back to a Point.
let deserialized: Point = serde_json::from_str(&serialized).unwrap();

// Prints deserialized = Point { x: 1, y: 2 }
println!("deserialized = {:?}", deserialized);
}
```
12 changes: 11 additions & 1 deletion src/book/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderConte
use crate::utils;

use crate::config::{Config, RustEdition};
use crate::utils::extern_args::ExternArgs;

/// The object used to manage and build a book.
pub struct MDBook {
Expand Down Expand Up @@ -304,6 +305,14 @@ impl MDBook {
let (book, _) = self.preprocess_book(&TestRenderer)?;

let color_output = std::io::stderr().is_terminal();

// get extra args we'll need for rustdoc, if config points to a cargo project.

let mut extern_args = ExternArgs::new();
if let Some(package_dir) = &self.config.rust.package_dir {
extern_args.load(&package_dir)?;
}

let mut failed = false;
for item in book.iter() {
if let BookItem::Chapter(ref ch) = *item {
Expand Down Expand Up @@ -332,7 +341,8 @@ impl MDBook {
cmd.current_dir(temp_dir.path())
.arg(chapter_path)
.arg("--test")
.args(&library_args);
.args(&library_args) // also need --extern for doctest to actually work
.args(extern_args.get_args());

if let Some(edition) = self.config.rust.edition {
match edition {
Expand Down
27 changes: 26 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ impl Default for BuildConfig {
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct RustConfig {
/// Path to a Cargo package
pub package_dir: Option<PathBuf>,
/// Rust edition used in playground
pub edition: Option<RustEdition>,
}
Expand Down Expand Up @@ -798,6 +800,9 @@ mod tests {
create-missing = false
use-default-preprocessors = true

[rust]
package-dir = "."

[output.html]
theme = "./themedir"
default-theme = "rust"
Expand Down Expand Up @@ -839,7 +844,10 @@ mod tests {
use_default_preprocessors: true,
extra_watch_dirs: Vec::new(),
};
let rust_should_be = RustConfig { edition: None };
let rust_should_be = RustConfig {
package_dir: Some(PathBuf::from(".")),
edition: None,
};
let playground_should_be = Playground {
editable: true,
copyable: true,
Expand Down Expand Up @@ -918,6 +926,7 @@ mod tests {
assert_eq!(got.book, book_should_be);

let rust_should_be = RustConfig {
package_dir: None,
edition: Some(RustEdition::E2015),
};
let got = Config::from_str(src).unwrap();
Expand All @@ -937,6 +946,7 @@ mod tests {
"#;

let rust_should_be = RustConfig {
package_dir: None,
edition: Some(RustEdition::E2018),
};

Expand All @@ -957,6 +967,7 @@ mod tests {
"#;

let rust_should_be = RustConfig {
package_dir: None,
edition: Some(RustEdition::E2021),
};

Expand Down Expand Up @@ -1356,4 +1367,18 @@ mod tests {
false
);
}

/* todo -- make this test fail, as it should
#[test]
#[should_panic(expected = "Invalid configuration file")]
// invalid key in config file should really generate an error...
fn invalid_rust_setting() {
let src = r#"
[rust]
foo = "bar"
"#;

Config::from_str(src).unwrap();
}
*/
}
Loading
Loading