Skip to content

Commit c8d46c8

Browse files
authored
Rollup merge of #148183 - fmease:rustdoc-test-n-doc-doctest-test-harness, r=GuillaumeGomez
rustdoc: Test & document `test_harness` code block attribute They were fully untested and undocumented previously despite being stable. Context: [#t-rustdoc > `test_harness` langstr @ 💬](https://rust-lang.zulipchat.com/#narrow/channel/266220-t-rustdoc/topic/.60test_harness.60.20langstr/near/546748038). While at it, I've also improved the documentation for code block attributes in general. I was only inspired to do so because there was no good place for `test_harness` and because it got hard to skim due to a lack of subsubsections.
2 parents 20080dc + 41bda8c commit c8d46c8

8 files changed

Lines changed: 325 additions & 32 deletions

File tree

src/doc/rustdoc/src/write-documentation/documentation-tests.md

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Documentation tests
22

3-
`rustdoc` supports executing your documentation examples as tests. This makes sure
4-
that examples within your documentation are up to date and working.
3+
`rustdoc` supports executing your documentation examples as tests.
4+
This makes sure that examples within your documentation are up to date and working.
55

66
The basic idea is this:
77

@@ -14,11 +14,13 @@ The basic idea is this:
1414
# fn f() {}
1515
```
1616

17-
The triple backticks start and end code blocks. If this were in a file named `foo.rs`,
18-
running `rustdoc --test foo.rs` will extract this example, and then run it as a test.
17+
Here, the triple backticks start and end the code block.
18+
If this were in a file named `foo.rs`, running `rustdoc --test foo.rs` will extract this example,
19+
and then run it as a test.
1920

20-
Please note that by default, if no language is set for the block code, rustdoc
21-
assumes it is Rust code. So the following:
21+
Please note that by default,
22+
if no language is set for the block code, rustdoc assumes it is Rust code.
23+
So the following:
2224

2325
``````markdown
2426
```rust
@@ -321,21 +323,43 @@ we can add the `#[macro_use]` attribute. Second, we’ll need to add our own
321323

322324
## Attributes
323325

324-
Code blocks can be annotated with attributes that help `rustdoc` do the right
325-
thing when testing your code:
326+
Code blocks can be annotated with attributes that tell `rustdoc` how to build and interpret the test.
327+
They follow the [code fence] in the opening line.
328+
As such, they share the place with language strings like `rust` or `text`.
329+
Multiple attributes can be provided by separating them with commas, spaces or tabs.
330+
You can also write comments which are enclosed in parentheses `(…)`.
326331

327-
The `ignore` attribute tells Rust to ignore your code. This is almost never
328-
what you want as it's the most generic. Instead, consider annotating it
329-
with `text` if it's not code or using `#`s to get a working example that
330-
only shows the part you care about.
332+
As alluded to in the introduction at the very top,
333+
unless you specify `rust` or something that isn't an attribute (except for `custom`),
334+
the code block is assumed to be Rust source code (and is syntax highlighted as such).
335+
336+
You can of course add `rust` explicitly (like `rust,ignore`) if the Markdown is also consumed by
337+
other tools (e.g., if it's contained inside of a `README.md` that's included via `include_str`).
338+
339+
### `ignore`
340+
341+
The `ignore` attribute tells `rustdoc` to ignore your code. This is useful if you would like to
342+
have Rust syntax highlighting but the snippet is incomplete or pseudocode.
343+
It is customary to add the reason why it should be ignored in a `(…)` comment.
331344

332345
```rust
333346
/// ```ignore
334347
/// fn foo() {
335348
/// ```
349+
///
350+
/// ```ignore (needs extra dependency)
351+
/// use dependency::functionality;
352+
/// functionality();
353+
/// ```
336354
# fn foo() {}
337355
```
338356

357+
Do note that this is almost never what you want as it's the most generic.
358+
Instead, consider annotating it with `text` if it's not code or
359+
using `#`s to get a working example that only shows the part you care about.
360+
361+
### `should_panic`
362+
339363
`should_panic` tells `rustdoc` that the code should compile correctly but
340364
panic during execution. If the code doesn't panic, the test will fail.
341365

@@ -346,6 +370,8 @@ panic during execution. If the code doesn't panic, the test will fail.
346370
# fn foo() {}
347371
```
348372

373+
### `no_run`
374+
349375
The `no_run` attribute will compile your code but not run it. This is
350376
important for examples such as "Here's how to retrieve a web page,"
351377
which you would want to ensure compiles, but might be run in a test
@@ -361,10 +387,10 @@ used to demonstrate code snippets that can cause Undefined Behavior.
361387
# fn foo() {}
362388
```
363389

390+
### `compile_fail`
391+
364392
`compile_fail` tells `rustdoc` that the compilation should fail. If it
365-
compiles, then the test will fail. However, please note that code failing
366-
with the current Rust release may work in a future release, as new features
367-
are added.
393+
compiles, then the test will fail.
368394

369395
```rust
370396
/// ```compile_fail
@@ -374,6 +400,13 @@ are added.
374400
# fn foo() {}
375401
```
376402

403+
<div class="warning">
404+
However, please note that code failing with the current Rust release may work in a future release,
405+
as new features are added!
406+
</div>
407+
408+
### `edition…`
409+
377410
`edition2015`, `edition2018`, `edition2021`, and `edition2024` tell `rustdoc`
378411
that the code sample should be compiled using the respective edition of Rust.
379412

@@ -390,6 +423,8 @@ that the code sample should be compiled using the respective edition of Rust.
390423
# fn foo() {}
391424
```
392425

426+
### `standalone_crate`
427+
393428
Starting in the 2024 edition[^edition-note], compatible doctests are merged as one before being
394429
run. We combine doctests for performance reasons: the slowest part of doctests is to compile them.
395430
Merging all of them into one file and compiling this new file, then running the doctests is much
@@ -441,7 +476,7 @@ should not be merged with the others. So the previous code should use it:
441476
In this case, it means that the line information will not change if you add/remove other
442477
doctests.
443478

444-
### Ignoring targets
479+
### `ignore-…`: Ignoring targets
445480

446481
Attributes starting with `ignore-` can be used to ignore doctests for specific
447482
targets. For example, `ignore-x86_64` will avoid building doctests when the
@@ -478,7 +513,7 @@ struct Foo;
478513
In older versions, this will be ignored on all targets, but starting with
479514
version 1.88.0, `ignore-x86_64` will override `ignore`.
480515

481-
### Custom CSS classes for code blocks
516+
### `{…}` & `custom`: Custom CSS classes for code blocks
482517

483518
```rust
484519
/// ```custom,{class=language-c}
@@ -504,8 +539,9 @@ To be noted that you can replace `class=` with `.` to achieve the same result:
504539
pub struct Bar;
505540
```
506541

507-
To be noted, `rust` and `.rust`/`class=rust` have different effects: `rust` indicates that this is
508-
a Rust code block whereas the two others add a "rust" CSS class on the code block.
542+
To be noted, `rust` and `{.rust}` / `{class=rust}` have different effects:
543+
`rust` indicates that this is a Rust code block whereas
544+
the two others add a "rust" CSS class on the code block in the generated HTML.
509545

510546
You can also use double quotes:
511547

@@ -516,24 +552,42 @@ You can also use double quotes:
516552
pub struct Bar;
517553
```
518554

555+
### `test_harness`
556+
557+
With `test_harness` applied, `rustdoc` will run any contained *test functions*
558+
instead of the (potentially implicit) `main` function.
559+
560+
```rust
561+
//! ```test_harness
562+
//! #[test]
563+
//! #[should_panic]
564+
//! fn abc() { assert!(false); }
565+
//!
566+
//! #[test]
567+
//! fn xyz() { assert!(true); }
568+
//! ```
569+
```
570+
571+
You can read more about *test functions* in [the Book][testing-book] or in [the Rust Reference][testing-ref].
572+
573+
[testing-book]: ../../book/ch11-01-writing-tests.html
574+
[testing-ref]: ../../reference/attributes/testing.html
575+
519576
## Syntax reference
520577

521-
The *exact* syntax for code blocks, including the edge cases, can be found
522-
in the [Fenced Code Blocks](https://spec.commonmark.org/0.29/#fenced-code-blocks)
523-
section of the CommonMark specification.
578+
The *exact* syntax for code blocks, including the edge cases,
579+
can be found in the [Fenced Code Blocks] section of the CommonMark specification.
524580

525-
Rustdoc also accepts *indented* code blocks as an alternative to fenced
526-
code blocks: instead of surrounding your code with three backticks, you
527-
can indent each line by four or more spaces.
581+
Rustdoc also accepts *indented* code blocks as an alternative to fenced code blocks:
582+
Instead of surrounding your code with a [code fence] (e.g., three backticks),
583+
you can indent each line by four or more spaces.
528584

529585
``````markdown
530586
let foo = "foo";
531587
assert_eq!(foo, "foo");
532588
``````
533589

534-
These, too, are documented in the CommonMark specification, in the
535-
[Indented Code Blocks](https://spec.commonmark.org/0.29/#indented-code-blocks)
536-
section.
590+
These, too, are documented in the CommonMark specification, in the [Indented Code Blocks] section.
537591

538592
However, it's preferable to use fenced code blocks over indented code blocks.
539593
Not only are fenced code blocks considered more idiomatic for Rust code,
@@ -595,3 +649,8 @@ operations within documentation test examples, such as `std::fs::read_to_string`
595649
The `--test-run-directory` flag allows controlling the run directory separately from the compilation directory.
596650
This is particularly useful in workspaces, where compiler invocations and thus diagnostics should be
597651
relative to the workspace directory, but documentation test examples should run relative to the crate directory.
652+
653+
654+
[code fence]: https://spec.commonmark.org/0.29/#code-fence
655+
[Fenced Code Blocks]: https://spec.commonmark.org/0.29/#fenced-code-blocks
656+
[Indented Code Blocks]: https://spec.commonmark.org/0.29/#indented-code-blocks

src/librustdoc/html/markdown.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -846,11 +846,12 @@ pub(crate) enum Ignore {
846846
Some(Vec<String>),
847847
}
848848

849-
/// This is the parser for fenced codeblocks attributes. It implements the following eBNF:
849+
/// This is the parser for fenced codeblocks attributes.
850850
///
851-
/// ```eBNF
852-
/// lang-string = *(token-list / delimited-attribute-list / comment)
851+
/// It implements the following grammar as expressed in ABNF:
853852
///
853+
/// ```ABNF
854+
/// lang-string = *(token-list / delimited-attribute-list / comment)
854855
/// bareword = LEADINGCHAR *(CHAR)
855856
/// bareword-without-leading-char = CHAR *(CHAR)
856857
/// quoted-string = QUOTE *(NONQUOTE) QUOTE
@@ -861,7 +862,7 @@ pub(crate) enum Ignore {
861862
/// attribute-list = [sep] attribute *(sep attribute) [sep]
862863
/// delimited-attribute-list = OPEN-CURLY-BRACKET attribute-list CLOSE-CURLY-BRACKET
863864
/// token-list = [sep] token *(sep token) [sep]
864-
/// comment = OPEN_PAREN *(all characters) CLOSE_PAREN
865+
/// comment = OPEN_PAREN *<all characters except closing parentheses> CLOSE_PAREN
865866
///
866867
/// OPEN_PAREN = "("
867868
/// CLOSE_PARENT = ")"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Test that we can successfully run two separate test suites.
2+
// Check that we run all four tests even though `ill` and `bad` both fail.
3+
4+
//! ```test_harness
5+
//! #[test]
6+
//! fn well() {
7+
//! assert!(true);
8+
//! }
9+
//!
10+
//! #[test]
11+
//! fn ill() {
12+
//! assert!(false);
13+
//! }
14+
//! ```
15+
16+
//! ```test_harness
17+
//! #[test]
18+
//! fn bad() {
19+
//! assert!(false);
20+
//! }
21+
//!
22+
//! #[test]
23+
//! fn good() {
24+
//! assert!(true);
25+
//! }
26+
//! ```
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
2+
running 2 tests
3+
test doctests.rs - (line 15) ... FAILED
4+
test doctests.rs - (line 4) ... FAILED
5+
6+
failures:
7+
8+
---- doctests.rs - (line 15) stdout ----
9+
Test executable failed ($STATUS).
10+
11+
stdout:
12+
13+
running 2 tests
14+
test bad ... FAILED
15+
test good ... ok
16+
17+
failures:
18+
19+
---- bad stdout ----
20+
21+
thread 'bad' ($TID) panicked at doctests.rs:4:5:
22+
assertion failed: false
23+
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
24+
25+
26+
failures:
27+
bad
28+
29+
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
30+
31+
32+
33+
---- doctests.rs - (line 4) stdout ----
34+
Test executable failed ($STATUS).
35+
36+
stdout:
37+
38+
running 2 tests
39+
test ill ... FAILED
40+
test well ... ok
41+
42+
failures:
43+
44+
---- ill stdout ----
45+
46+
thread 'ill' ($TID) panicked at doctests.rs:9:6:
47+
assertion failed: false
48+
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
49+
50+
51+
failures:
52+
ill
53+
54+
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
55+
56+
57+
58+
59+
failures:
60+
doctests.rs - (line 15)
61+
doctests.rs - (line 4)
62+
63+
test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
64+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//@ ignore-cross-compile (needs to run host tool binary)
2+
3+
// Test the behavior of doctests that are marked `test_harness` and that contain multiple `#[test]`
4+
// functions. Sadly this needs to be a run-make test instead of rustdoc-UI one because at the time
5+
// of writing we can only pass `--test-threads=1` to the inner test suite using a runtool (needed to
6+
// guarantee deterministic test output) which we'd like to be written in Rust to be cross-platform.
7+
//
8+
// See also #157511 and `tests/rustdoc-ui/doctest/test_harness.rs`.
9+
10+
use std::path::Path;
11+
12+
use run_make_support::{diff, rustc, rustdoc};
13+
14+
fn main() {
15+
let doctests_path = Path::new("doctests.rs");
16+
let runtool_path = Path::new("runtool.rs");
17+
18+
rustc().input(doctests_path).crate_type("lib").run();
19+
rustc().input(runtool_path).run();
20+
21+
let output = rustdoc()
22+
.input(doctests_path)
23+
.arg("--test")
24+
// for the outer test suite
25+
.arg("--test-args=--test-threads=1")
26+
.arg("--test-runtool")
27+
.arg(Path::new(".").join(runtool_path).with_extension(std::env::consts::EXE_EXTENSION))
28+
.arg("-L.")
29+
.env("RUST_BACKTRACE", "0")
30+
.run_fail();
31+
output.assert_exit_code(101);
32+
output.assert_stderr_equals("");
33+
34+
diff()
35+
.expected_file(doctests_path.with_extension("stdout"))
36+
.actual_text("stdout", output.stdout_utf8())
37+
.normalize(r#"finished in \d+\.\d+s"#, "finished in $$TIME")
38+
.normalize(r"thread '(?P<name>.*?)' \(\d+\) panicked", "thread '$name' ($$TID) panicked")
39+
.normalize(r"Test executable failed \(.+?\)", "Test executable failed ($$STATUS)")
40+
.run();
41+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// The whole purpose of this runtool is to pass `--test-threads=1`
2+
// to the inner testsuite to guarantee deterministic output.
3+
// See also #157511.
4+
5+
fn main() {
6+
let status = std::process::Command::new(std::env::args().nth(1).unwrap())
7+
.arg("--test-threads=1")
8+
.status()
9+
.unwrap();
10+
std::process::exit(status.code().unwrap())
11+
}

0 commit comments

Comments
 (0)