文档测试
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
用于预处理示例的完整算法
- 插入了一些常见的
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";
在文档测试中使用 ?
在编写示例时,包含完整的错误处理很少有用,因为它会增加大量的样板代码。相反,您可能想要以下内容
#![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() {} }
edition2015
、edition2018
、edition2021
和 edition2024
告诉 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开始,兼容的文档测试在运行之前会合并为一个。我们合并文档测试是出于性能原因:文档测试最慢的部分是编译它们。将所有文档测试合并到一个文件中并编译这个新文件,然后运行文档测试要快得多。无论文档测试是否合并,它们都在自己的进程中运行。
运行文档测试时花费的时间示例
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
这是基于整个 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 代码的习惯用法,而且无法将诸如 ignore
或 should_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 目录运行。