文档测试
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
用于预处理示例的完整算法
- 插入了一些常见的
allow
属性,包括unused_variables
、unused_assignments
、unused_mut
、unused_attributes
和dead_code
。小型示例通常会触发这些 lint。 - 使用
#![doc(test(attr(...)))]
指定的任何属性都会被添加。 - 任何以
#![foo]
开头的属性都将保留为 crate 属性。 - 如果示例不包含
extern crate
,并且没有指定#![doc(test(no_crate_inject))]
,则会插入extern crate <mycrate>;
(注意缺少#[macro_use]
)。 - 最后,如果示例不包含
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); }
最后,我们打印
x
和y
的总和#![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() {} }
edition2015
、edition2018
和 edition2021
告诉 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 代码的更惯用方式,而且无法使用 ignore
或 should_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 目录运行。