文档测试

rustdoc 支持将文档示例作为测试执行。这确保了您的文档中的示例是最新的并且可以正常工作。

基本思想如下

#![allow(unused)]
fn main() {
/// # Examples
///
/// ```
/// let x = 5;
/// ```
fn f() {}
}

三个反引号开始和结束代码块。如果这在一个名为 foo.rs 的文件中,运行 rustdoc --test foo.rs 将提取此示例,然后将其作为测试运行。

请注意,默认情况下,如果没有为代码块设置语言,rustdoc 假定它是 Rust 代码。所以以下内容

```rust
let x = 5;
```

与以下内容严格等效

```
let x = 5;
```

不过,这里有一些微妙之处!请继续阅读以了解更多详情。

文档测试的通过或失败

与常规单元测试一样,如果常规文档测试编译并通过运行而没有 panic,则认为它们“通过”。因此,如果您想演示某些计算给出了特定的结果,assert! 系列宏的工作方式与其他 Rust 代码相同

#![allow(unused)]
fn main() {
let foo = "foo";
assert_eq!(foo, "foo");
}

这样,如果计算结果与预期不同,代码就会 panic,文档测试就会失败。

预处理示例

在上面的示例中,您会注意到一些奇怪的地方:没有 main 函数!强制您为每个示例编写 main,无论示例多么小,都会增加摩擦并使输出变得混乱。因此,rustdoc 在运行示例之前会对其进行轻微处理。以下是 rustdoc 用于预处理示例的完整算法

  1. 插入了一些常见的 allow 属性,包括 unused_variablesunused_assignmentsunused_mutunused_attributesdead_code。小型示例通常会触发这些 lint。
  2. 添加使用 #![doc(test(attr(...)))] 指定的任何属性。
  3. 任何前导的 #![foo] 属性都保持不变,作为 crate 属性。
  4. 如果示例不包含 extern crate,并且未指定 #![doc(test(no_crate_inject))],则插入 extern crate <mycrate>;(请注意缺少 #[macro_use])。
  5. 最后,如果示例不包含 fn main,则文本的其余部分将包装在 fn main() { your_code } 中。

有关规则 4 中那个警告的更多信息,请参阅下面的“文档宏”部分。

隐藏示例的部分内容

有时,您需要一些设置代码或其他会分散示例注意力,但对于使测试工作很重要的内容。考虑一个看起来像这样的示例代码块

#![allow(unused)]
fn main() {
/// ```
/// /// Some documentation.
/// # fn foo() {} // this function will be hidden
/// println!("Hello, World!");
/// ```
fn f() {}
}

它将像这样呈现

#![allow(unused)]
fn main() {
/// Some documentation.
fn foo() {}
println!("Hello, World!");
}

是的,没错:您可以添加以 # 开头的行,它们将从输出中隐藏,但在编译代码时会使用。您可以利用这一点。在这种情况下,文档注释需要应用于某种函数,因此如果我想向您展示一个文档注释,我需要在其下方添加一个小函数定义。与此同时,它只是为了满足编译器,所以隐藏它使示例更清晰。您可以使用此技术来详细解释更长的示例,同时仍然保持文档的可测试性。

例如,假设我们想要文档化这段代码

#![allow(unused)]
fn main() {
let x = 5;
let y = 6;
println!("{}", x + y);
}

我们可能希望文档最终看起来像这样

首先,我们将 x 设置为 5

#![allow(unused)]
fn main() {
let x = 5;
let y = 6;
println!("{}", x + y);
}

接下来,我们将 y 设置为 6

#![allow(unused)]
fn main() {
let x = 5;
let y = 6;
println!("{}", x + y);
}

最后,我们打印 xy 的和

#![allow(unused)]
fn main() {
let x = 5;
let y = 6;
println!("{}", x + y);
}

为了保持每个代码块的可测试性,我们希望每个块中都有完整的程序,但我们不希望读者每次都看到每一行。这是我们放入源代码中的内容

First, we set `x` to five:

```
let x = 5;
# let y = 6;
# println!("{}", x + y);
```

Next, we set `y` to six:

```
# let x = 5;
let y = 6;
# println!("{}", x + y);
```

Finally, we print the sum of `x` and `y`:

```
# let x = 5;
# let y = 6;
println!("{}", x + y);
```

通过重复示例的所有部分,您可以确保您的示例仍然可以编译,同时仅显示与该部分解释相关的内容。

可以通过使用两个连续的井号 ## 来防止 #-隐藏行。这只需要对第一个 # 执行,否则会导致隐藏。如果我们有一个像下面这样的字符串字面量,其中有一行以 # 开头

#![allow(unused)]
fn main() {
let s = "foo
# bar # baz";
}

我们可以通过转义初始 # 来文档化它

/// let s = "foo
/// ## bar # baz";

在文档测试中使用 ?

在编写示例时,包含完整的错误处理很少有用,因为它会增加大量的样板代码。相反,您可能想要以下内容

#![allow(unused)]
fn main() {
/// ```
/// use std::io;
/// let mut input = String::new();
/// io::stdin().read_line(&mut input)?;
/// ```
fn f() {}
}

问题是 ? 返回 Result<T, E>,而测试函数不返回任何内容,因此这会给出类型不匹配错误。

您可以通过手动添加一个返回 Result<T, E>main 来解决此限制,因为 Result<T, E> 实现了 Termination trait

/// A doc test using ?
///
/// ```
/// use std::io;
///
/// fn main() -> io::Result<()> {
///     let mut input = String::new();
///     io::stdin().read_line(&mut input)?;
///     Ok(())
/// }
/// ```
fn f() {}

结合上面部分中的 # ,您得到一个解决方案,该解决方案对读者来说看起来像是最初的想法,但适用于文档测试

/// ```
/// use std::io;
/// # fn main() -> io::Result<()> {
/// let mut input = String::new();
/// io::stdin().read_line(&mut input)?;
/// # Ok(())
/// # }
/// ```
fn f() {}

从 1.34.0 版本开始,您也可以省略 fn main(),但您必须消除错误类型的歧义

#![allow(unused)]
fn main() {
/// ```
/// use std::io;
/// let mut input = String::new();
/// io::stdin().read_line(&mut input)?;
/// # Ok::<(), io::Error>(())
/// ```
fn f() {}
}

这是 ? 运算符添加隐式转换的一个不幸的后果,因此类型推断失败,因为类型不是唯一的。请注意,您必须在一个序列中编写 (()),中间没有空格,以便 rustdoc 理解您想要一个隐式的返回 Result 的函数。

在文档测试中显示警告

您可以通过运行 rustdoc --test --test-args=--show-output(或者,如果您使用 cargo,则为 cargo test --doc -- --show-output)在文档测试中显示警告。默认情况下,这仍然会隐藏 unused 警告,因为很多示例都使用私有函数;如果您想查看未使用的变量或死代码警告,可以在示例的顶部添加 #![warn(unused)]。您也可以在 crate 根目录中使用 #![doc(test(attr(warn(unused))))] 来全局启用警告。

文档宏

这是一个文档宏的示例

/// Panic with a given message unless an expression evaluates to true.
///
/// # Examples
///
/// ```
/// # #[macro_use] extern crate foo;
/// # fn main() {
/// panic_unless!(1 + 1 == 2, “Math is broken.”);
/// # }
/// ```
///
/// ```should_panic
/// # #[macro_use] extern crate foo;
/// # fn main() {
/// panic_unless!(true == false, “I’m broken.”);
/// # }
/// ```
#[macro_export]
macro_rules! panic_unless {
    ($condition:expr, $($rest:expr),+) => ({ if ! $condition { panic!($($rest),+); } });
}
fn main() {}

您会注意到三件事:我们需要添加我们自己的 extern crate 行,以便我们可以添加 #[macro_use] 属性。其次,我们也需要添加我们自己的 main()(原因如上所述)。最后,明智地使用 # 注释掉这两件事,这样它们就不会显示在输出中。

属性

可以使用属性注释代码块,这些属性可以帮助 rustdoc 在测试您的代码时做正确的事情

ignore 属性告诉 Rust 忽略您的代码。这几乎不是您想要的,因为它最通用。相反,如果它不是代码,请考虑使用 text 对其进行注释,或使用 # 获取仅显示您关心的部分的工作示例。

#![allow(unused)]
fn main() {
/// ```ignore
/// fn foo() {
/// ```
fn foo() {}
}

should_panic 告诉 rustdoc 代码应该正确编译但在执行期间 panic。如果代码没有 panic,则测试将失败。

#![allow(unused)]
fn main() {
/// ```should_panic
/// assert!(false);
/// ```
fn foo() {}
}

no_run 属性将编译您的代码但不运行它。这对于诸如“这是如何检索网页”之类的示例非常重要,您希望确保它可以编译,但可能在没有网络访问的测试环境中运行。此属性也可用于演示可能导致未定义行为的代码片段。

#![allow(unused)]
fn main() {
/// ```no_run
/// loop {
///     println!("Hello, world");
/// }
/// ```
fn foo() {}
}

compile_fail 告诉 rustdoc 编译应该失败。如果编译成功,则测试将失败。但是,请注意,在当前 Rust 版本中失败的代码可能在未来的版本中可以工作,因为添加了新功能。

#![allow(unused)]
fn main() {
/// ```compile_fail
/// let x = 5;
/// x += 2; // shouldn't compile!
/// ```
fn foo() {}
}

edition2015edition2018edition2021edition2024 告诉 rustdoc 代码示例应使用 Rust 的相应版本进行编译。

#![allow(unused)]
fn main() {
/// Only runs on the 2018 edition.
///
/// ```edition2018
/// let result: Result<i32, ParseIntError> = try {
///     "1".parse::<i32>()?
///         + "2".parse::<i32>()?
///         + "3".parse::<i32>()?
/// };
/// ```
fn foo() {}
}

从 2024 版本1开始,兼容的文档测试在运行之前会合并为一个。我们合并文档测试是出于性能原因:文档测试最慢的部分是编译它们。将所有文档测试合并到一个文件中并编译这个新文件,然后运行文档测试要快得多。无论文档测试是否合并,它们都在自己的进程中运行。

运行文档测试时花费的时间示例

sysinfo crate:

wall-time duration: 4.59s
total compile time: 27.067s
total runtime: 3.969s

Rust core library

wall-time duration: 102s
total compile time: 775.204s
total runtime: 15.487s
1

这是基于整个 crate 的版本,而不是可能在其代码属性中指定的单个测试用例的版本。

在某些情况下,文档测试无法合并。例如,如果您有

#![allow(unused)]
fn main() {
//! ```
//! let location = std::panic::Location::caller();
//! assert_eq!(location.line(), 4);
//! ```
}

此代码的问题在于,如果您更改任何其他文档测试,则在运行 rustdoc --test 时很可能会中断,从而使其难以维护。

这就是 standalone_crate 属性的用武之地:它告诉 rustdoc 文档测试不应与其他文档测试合并。所以之前的代码应该使用它

#![allow(unused)]
fn main() {
//! ```standalone_crate
//! let location = std::panic::Location::caller();
//! assert_eq!(location.line(), 4);
//! ```
}

在这种情况下,这意味着如果您添加/删除其他文档测试,行信息将不会更改。

代码块的自定义 CSS 类

#![allow(unused)]
fn main() {
/// ```custom,{class=language-c}
/// int main(void) { return 0; }
/// ```
pub struct Bar;
}

文本 int main(void) { return 0; } 在具有类 language-c 的代码块中呈现时没有突出显示。这可以用于通过 JavaScript 库突出显示其他语言,例如。

如果没有 custom 属性,它将被生成为 Rust 代码示例,并带有额外的 language-C CSS 类。因此,如果您明确不希望它成为 Rust 代码示例,请不要忘记添加 custom 属性。

需要注意的是,您可以用 . 替换 class= 以达到相同的效果

#![allow(unused)]
fn main() {
/// ```custom,{.language-c}
/// int main(void) { return 0; }
/// ```
pub struct Bar;
}

需要注意的是,rust.rust/class=rust 具有不同的效果:rust 表示这是一个 Rust 代码块,而其他两个在代码块上添加了“rust”CSS 类。

您也可以使用双引号

#![allow(unused)]
fn main() {
/// ```"not rust" {."hello everyone"}
/// int main(void) { return 0; }
/// ```
pub struct Bar;
}

语法参考

代码块的确切语法,包括边缘情况,可以在 CommonMark 规范的 Fenced Code Blocks 部分找到。

Rustdoc 也接受缩进代码块作为围栏代码块的替代方案:您可以将每行缩进四个或更多空格,而不是用三个反引号包围您的代码。

    let foo = "foo";
    assert_eq!(foo, "foo");

这些也在 CommonMark 规范的 Indented Code Blocks 部分中进行了文档化。

但是,最好使用围栏代码块而不是缩进代码块。围栏代码块不仅被认为更符合 Rust 代码的习惯用法,而且无法将诸如 ignoreshould_panic 之类的属性与缩进代码块一起使用。

仅在收集文档测试时包含项

Rustdoc 的文档测试可以做一些常规单元测试不能做的事情,因此有时扩展您的文档测试,使其包含一些原本不需要在文档中的示例会很有用。为此,Rustdoc 允许您使某些项仅在收集文档测试时出现,这样您就可以利用文档测试功能,而无需强制测试出现在文档中,或找到任意私有项将其包含在内。

当编译 crate 以用于文档测试(使用 --test 选项)时,rustdoc 将设置 #[cfg(doctest)]。请注意,它们仍然只会链接到您的 crate 的公共项;如果您需要测试私有项,则需要编写单元测试。

在此示例中,我们添加了我们知道无法编译的文档测试,以验证我们的 struct 只能接收有效数据

#![allow(unused)]
fn main() {
/// We have a struct here. Remember it doesn't accept negative numbers!
pub struct MyStruct(pub usize);

/// ```compile_fail
/// let x = my_crate::MyStruct(-5);
/// ```
#[cfg(doctest)]
pub struct MyStructOnlyTakesUsize;
}

请注意,这里的 struct MyStructOnlyTakesUsize 实际上不是您的公共 crate API 的一部分。#[cfg(doctest)] 的使用确保此 struct 仅在 rustdoc 收集文档测试时存在。这意味着当 --test 传递给 rustdoc 时,将执行其文档测试,但会从公共文档中隐藏。

#[cfg(doctest)] 的另一种可能的用途是测试包含在您的 README 文件中的文档测试,而无需将其包含在您的主要文档中。例如,您可以将此写入您的 lib.rs 以测试您的 README 作为文档测试的一部分

#![allow(unused)]
fn main() {
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;
}

这将把您的 README 作为隐藏 struct ReadmeDoctests 的文档包含在内,然后将与其余文档测试一起进行测试。

控制编译和运行目录

默认情况下,rustdoc --test 将从同一工作目录编译和运行文档测试示例。编译目录用于编译器诊断、file!() 宏和 rustdoc 测试运行器本身的输出,而运行目录对文档测试示例中的文件系统操作(例如 std::fs::read_to_string)产生影响。

--test-run-directory 标志允许将运行目录与编译目录分开控制。这在工作区中特别有用,在工作区中,编译器调用以及诊断应相对于工作区目录,但文档测试示例应相对于 crate 目录运行。