文档测试

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;
```

不过有一些细微之处!请继续阅读以了解更多详细信息。

通过或失败 doctest

与常规单元测试一样,常规 doctest 如果编译并运行而没有恐慌,则被认为是“通过”。因此,如果您想证明某些计算会产生特定结果,assert! 宏系列的工作方式与其他 Rust 代码相同

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

这样,如果计算返回了不同的结果,代码就会恐慌,doctest 就会失败。

预处理示例

在上面的示例中,您会注意到一些奇怪的地方:没有 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";

在 doctest 中使用 ?

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

#![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 特性

/// 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() {}

结合上面部分的 # ,您将得到一个对读者来说看起来像最初的想法但可以与 doctest 一起使用的解决方案

/// ```
/// 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 返回函数。

在 doctest 中显示警告

您可以通过运行 rustdoc --test --test-args=--show-output(或者,如果您使用的是 cargo,则运行 cargo test --doc -- --show-output)来显示 doctest 中的警告。默认情况下,这仍然会隐藏 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 代码应该编译正确,但在执行期间恐慌。如果代码没有恐慌,测试将失败。

#![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() {}
}

edition2015edition2018edition2021 告诉 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() {}
}

语法参考

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

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

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

这些也在 CommonMark 规范中进行了记录,在 缩进代码块 部分。

但是,最好使用围栏代码块而不是缩进代码块。围栏代码块不仅被认为是 Rust 代码的更惯用方式,而且无法使用 ignoreshould_panic 等属性与缩进代码块一起使用。

仅在收集 doctest 时包含项目

Rustdoc 的文档测试可以做一些常规单元测试无法做到的事情,因此有时使用一些在文档中不需要的示例来扩展您的 doctests 可能会很有用。为此,Rustdoc 允许您只在收集 doctests 时显示某些项目,因此您可以利用 doctest 功能,而无需强制测试出现在文档中,或找到一个任意私有项目来包含它。

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

在这个例子中,我们添加了一些我们知道无法编译的 doctests,以验证我们的结构只能接受有效数据。

#![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;
}

请注意,这里的结构 MyStructOnlyTakesUsize 实际上并不属于您的公共 crate API。使用 #[cfg(doctest)] 确保该结构只在 rustdoc 收集 doctests 时存在。这意味着它的 doctest 在将 --test 传递给 rustdoc 时执行,但隐藏在公共文档中。

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

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

这将包含您的 README 作为隐藏结构 ReadmeDoctests 上的文档,然后将与您的其他 doctests 一起进行测试。

控制编译和运行目录

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

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