PingCap的Rust训练课程1:熟悉Rust工具链
前言
任务:在内存中创建一个能够接受命令行参数的键/值存储程序,且程序能够通过一些简单的测试。
目标:
- 安装Rust编译器和工具
- 了解本课程中使用的项目结构
- 使用
cargo init
/run
/test
/clippy
/fmt
- 了解如何从crates.io查找、导入crates
- 为键值存储程序定义恰当的数据类型
关键词:测试、clap
crate、了解CARGO_VERSION
等值、熟悉clippy
和rustfmt
工具。
扩展练习:尝试使用structopt
crate。
介绍
在这个项目中,您将在内存中创建一个简单的键/值存储,以将字符串映射到字符串,程序能够通过一些简单测试并能够响应命令行参数。项目的重点是熟悉典型Rust 项目的工具链和设置。
即使这些听起来很基础,也请亲手尝试这个项目,因为它将介绍一些会用在整个课程中的通用模式。
项目需求规格
cargo项目kvs
构建了一个名为kvs
的命令行键值存储客户端,该客户端又调用了一个名为kvs
的库。
kvs
可执行文件支持以下命令行参数:
kvs set <KEY> <VALUE>
将字符串键设置为字符串值kvs get <KEY>
获取给定字符串键的字符串值kvs rm <KEY>
删除给定的键kvs -V
打印版本
kvs
库包含一个类型,KvStore
,它支持以下方法:
KvStore::set(&mut self, key: String, value: String)
将字符串键设置为字符串值KvStore::get(&self, key: String) -> Option<String>
获取字符串键的字符串值。如果键不存在,返回None
。KvStore::remove(&mut self, key: String)
删除给定的键。
KvStore
类型将值存储在内存中,因此命令行客户端除了打印版本外,能做的并不多。从命令行运行get
/set
/rm
命令时返回”unimplemented”错误。未来项目会将值存储在磁盘上,并实现相应的的命令。
安装
以您此时的Rust基础应当知道如何使用rustup安装Rust。
如果不知道,运行下面的命令即可:
1 | curl https://sh.rustup.rs -sSf | sh |
(如果您使用的是Windows,请按照rustup.rs上的说明操作。请注意,您在本课程中将面临比其他人更多的挑战,因为它是在Unix上开发的。通常,在Windows上的Rust开发体验不如在Unix上那么顺滑)。
通过键入rustc -V
来验证工具链是否正确安装。如果这不起作用,请注销当前shell的会话并重新登录,以使得安装时的shell profile配置变动可以生效。
项目设置
您将使用Cargo在git仓库完成本项目。您将从本课程的源仓库中导入项目的测试用例。
请注意,在源仓库中,与本课程相关的所有内容都在rust
子目录中,您可以忽略其他目录。
本课程中的所有项目均包含库和可执行文件。使用可执行文件的形式,是因为我们正在开发一个可以运行的应用程序。使用库的形式,是因为提供的测试用例必须链接到这些库。
在本课程中,我们将为每个项目使用相同的设置。
我们将使用的目录结构为:
1 | ├── Cargo.toml |
其中Cargo.toml
、lib.rs
和kvs.rs
文件内容如下:
Cargo.toml
:
1 | [package] |
lib.rs
:
1 | // just leave it empty for now |
kvs.rs
:
1 | fn main() { |
其中author应该是您的名字,而name必须是kvs
,这是项目名称同时也是库的名称,以使测试用例起作用。同样,二进制文件(命令行应用程序)的名称必须是kvs
。在上面的设置中,项目会因文件名隐式地叫做kvs
,当然您也可以随意命名文件,只要将适当的信息写入配置文件(Cargo.toml
)中即可。
您可以使用cargo new --lib
直接新建项目;或使用cargo init --lib
在一个空目录中初始化项目;或手动设置这个项目:您可能还需要在同一目录中初始化一个git仓库。
最后,从课程资料中复制tests
目录:即将文件rust/projects/project-1/tests
从课程仓库复制到您自己的仓库中,也叫做tests
。
此时您应该可以使用cargo run
运行程序。
现在就试试。
您已经为这个项目做好了准备,开始编写业务代码吧。
第1部分:编译测试代码
tests/tests.rs
中为您提供了一套单元测试。 打开它看看。
尝试使用cargo test
运行测试。发生了什么?为什么?
本项目的第一个任务是让测试模块能够通过编译。
如果你的项目和我的一样,你应该会看到大量编译错误,我们来看看前几条。通常当你看到一堆错误时,第一条是最重要的 — rustc
会在遇到错误后尝试继续编译,因此后面的可能是级联错误,其意义远低于第一条。您的前几条错误可能如下所示:
1 | error[E0433]: failed to resolve: use of undeclared type or module `assert_cmd` |
(如果您看到其他错误,请反馈issue)。
对于一个新的Rust程序员来说,这两个错误很难诊断出来,所以我只会告诉你这里发生了什么:项目配置文件中缺少开发依赖项的crates。
对于这个项目,你的Cargo.toml
文件需要包含这些行:
1 | [dev-dependencies] |
了解这些依赖项的详情,对您完成本项目并不重要,您可以自己去查找此类信息。我们之前没有告诉您需要开发依赖项,是为了让您自己会遇到这些错误。在未来的项目中,会在项目配置部分告诉您后面所需的开发依赖。
简要说明:您如何确定这些错误是由于项目配置中缺少依赖,项而不是由于源码中的错误造成的?以前面显示的错误为例,一个重要线索:
1 | 1 | use assert_cmd::prelude::*; |
在use
语句中,路径中的第一个元素始终是crate的名称。不过,当第一个路径元素引用的,是之前由别的use
语句引入上下文的名称时,会出现例外情况。换句话说,如果这个文件中有另一个use
语句,例如use foo::assert_cmd
,那么use assert_cmd::prelude::*
中引用的就是之前那个assert_cmd
。关于这一点还有很多内容,我们就不在这里探讨这些技术细节了。只需要记住,通常在use
语句中,如果找不到路径中的第一个元素(即无法解析),问题可能是配置文件中没有声明那个crate。
这是我们在第一个项目中遇到的第一个问题。希望能有所启发。
继续,并将适当的开发依赖项添加到您的配置文件中。
再次尝试使用cargo test
运行测试。发生什么了?为什么?
此时前面的那些错误应该已经消失了。现在的错误应该都是关于测试用例无法在您自己的代码中找到它期望的代码。
所以现在您的任务是:声明测试模块编译所需的类型、方法等。
在本课程中,您将大量阅读测试用例。测试用例将准确地告诉您对代码的期望。如果书本和实验不一致,则实验为真(纠正错误!)。在编程中也是如此。测试用例展示了软件应有的行为为。测试是现实,应习惯阅读测试用例。
此外,测试用例通常是任何项目中编写质量最差的代码,原始且没有文档注释。
再次尝试使用cargo test
运行测试。发生什么了?为什么?
在src/lib.rs
中编写能够让cargo test --no-run
通过的类和方法签名。暂时不要编写任何函数体—使用panic!()
宏。这是在不知道或不关心实现的情况下描述API的方法(此外还有宏unimplemented!
,但由于这个单词有点长,因此通常简单地使用panic!
即可。不过,如果您正在发布的软件包含未实现的方法的程序时,则应当使用unimplemented!
宏)。
完成上述要求。
我的作业:
1 | pub struct KvStore { |
之后,如果您运行cargo test
(不带--no-run
),您应该会看到一些测试失败输出,例如:
1 | Finished dev [unoptimized + debuginfo] target(s) in 2.32s |
…后还有很多行。太棒了!这结果正是我们现在所需要的。您会在本项目后面的内容中编写让这些测试通过的代码。
测试技巧
如果你再次查看cargo test
的输出,就会看到一些有趣的东西:
1 | Running target/debug/deps/kvs-b03a01e7008067f6 |
cargo输出了三遍”Running …”。实际上前两次并没有运行任何测试。那么如果这三批测试都没有失败,cargo将运行另一组测试。
为什么会这样?
这是因为在Rust中有很多可以用来编写测试的地方:
- 在库的源码中
- 在每个二进制的源码中
- 在每个测试的源码中
- 在库源码的注释文档中
其实cargo并不知道上面这四条里哪些确实包含了测试,cargo只是构建并运行这些测试。
所以这里有两组空测试:
1 | Running target/debug/deps/kvs-b03a01e7008067f6 |
不过,这里可能不容易理解:这其中一个是您的库,编译后测试,另一个是您的二进制,编译后测试,目前两者都不包含任何测试。名称中都有”kvs”的原因是因为您的库和二进制文件都称为”kvs”。
这些测试的输出可能有点烦人,有两种方法可以让cargo安静下来:使用命令行参数,或更改项目配置:
以下是相关的命令行标识:
cargo test --lib
— 只测试库中的测试cargo test --doc
— 测试库中的文档测试cargo test --bins
— 测试项目中的所有二进制cargo test --bin foo
— 只测试foo
的二进制cargo test --test foo
— 测试 测试文件foo
中的测试
以上命令可以方便快速地隐藏测试输出,但如果项目不包含某种类型的测试,就最好不去处理测试。回想一下The Cargo Book中关于manifest description的描述,可知有两个配置选项可用:test = false
和doctest = false
。它们位于[lib]和[[bin]]部分,因此可以考虑更新您的项目配置。
如果以前没用过可以尝试下面这个命令:
1 | cargo test -- --help |
试试吧,输出很有趣。您在此处看到的是包含已编译测试的可执行文件的帮助信息(由空格包围的--
将告诉cargo将之后的所有参数传递给测试二进制文件)。这与运行cargo test --help
时显示的信息完全不同。它们是两个不同的命令:cargo在运行您的二进制测试时会将这些参数传递给测试程序。
您也可以通过命令执行同样的操作。让我们再回到的cargo test
示例。我们看到了这一行:
1 | Running target/debug/deps/kvs-b03a01e7008067f6 |
这就是cargo告诉您测试二进制的文件名。您可以自己运行它,比如target/debug/deps/kvs-b03a01e7008067f6 --help
。
target
目录里有很多有趣的东西。深入研究可以教会您很多关于Rust工具链工作细节的知识。
在实践中,特别是对于大型项目,您不会在开发单个功能时运行整个测试套件。要将测试集缩小到我们关心的某个测试,请运行以下命令:
1 | cargo test cli_no_args |
这将运行名为cli_no_args
的测试。事实上,它会运行名称中包含cli_no_args
的任何测试,因此,例如,如果你想运行所有CLI测试,你可以运行cargo test cli
。以上可能就是您在完成项目时手动运行测试的各种方式,否则您将被许多尚未修复的失败测试分心。不幸的是,该模式只是一个简单的字符串匹配,而不是像正则表达式那样功能强大的东西。
请注意,在撰写本文时,本课程中项目测试用例,并未按照能够明确指出某测试专为项目的某部分而编写的方式进行组织 - 只是到最后整个测试套件应该通过即可。您需要自己阅读测试的名称和实现,来确定您认为在哪些阶段应该通过哪些测试。
第2部分:接受命令行参数
本课程中的键/值存取都是通过命令行客户端执行的。在这个项目中,命令行客户端非常简单,因为键值存储的状态只存储在内存中,而不是持久化到磁盘。
通过这一部分,您应使名为cli_*
的测试用例通过。
回忆上一部分中介绍的关于如何运行单个测试用例的内容。
重申一下CLI的接口是:
kvs set <KEY> <VALUE>
将字符串键设置为字符串值kvs get <KEY>
获取给定字符串键的字符串值kvs rm <KEY>
删除给定的键kvs -V
打印版本
但在本次迭代中,get
和set
命令将从stderr输出字符串”unimplemented”,并以非零退出码退出,表示错误。
您可能需要使用clap
crate来处理命令行参数。
找到最新版本的clap
crate 并将其添加到Cargo.toml
的依赖项中。有多种方法可以搜索或导入crate,我们推荐:查看内置的cargo search
和插件cargo edit
。
接下来使用crates.io、lib.rs或docs.rs查找clap
crate 的文档,并实现命令行界面以使名为cli_*
的测试用例通过。
测试时,使用cargo run
;不要直接从target/
目录运行可执行文件。需要将参数传递给程序时,用两个破折号将它们与cargo run
命令分开,--
,如cargo run -- get key1
。
我的作业
1 | use clap::{Parser, Subcommand}; |
第3部分:cargo环境变量
当您设置clap
来解析命令行参数时,您可能会设置名称、版本、作者和描述(如果没有,请这样做)。这些是Cargo.toml
提供的冗余信息,而这些Cargo在构建时设置的环境变量可以被Rust源码访问。
修改您的clap
设置以从cargo标准环境变量中设置这些值。
第4部分:将值存储在内存中
您的命令行程序已经有了雏形,现在让我们实现KvStore
,以使得其余的测试用例通过。
通过阅读测试用例即可了解KvStore
方法的行为 — 您不需要任何进一步的描述来完成这个项目的代码。
通过在KvStore
上实现各方法使其余的测试用例通过。
我的作业:
1 | use std::collections::HashMap; |
第5部分:文档
您已经实现了项目的功能,但是在它成为一个优秀的Rust软件之前我们还需要做一些事情,以准备好贡献或发布。
首先,公共项目通常应该有文档注释。
文档注释显示在crate的API文档中。使用命令cargo doc
生成API文档,这可以将文档注释渲染为HTML并放在target/doc
文件夹中。请注意,尽管target/doc
文件夹不包含index.html
。在本项目中,您的crate文档将位于target/doc/kvs/index.html
。您可以使用cargo doc --open
在该位置启动Web浏览器。cargo doc --open
并不总是有效,比如,如果您通过ssh
连接到云实例就无法打开。不过就算打不开浏览器,该命令仍将打印它无法打开的html的文件名 — 这会方便我们查找API文档的位置。
好的文档注释并不是是重复函数名,也不是重复从类型签名中收集到的信息。好的注释应该解释为什么以及如何使用函数,成功和失败时的返回值是什么,什么条件会触发error和panic。您编写的库非常简单,因此文档也会很简单。如果您真的想不出通过文档注释添加任何有用的内容,那么可以不添加文档注释(这是一个偏好问题)。在没有文档注释的情况下,仅从名称和类型签名就应该清楚如何使用类型或函数。
文档注释可以包含示例,而这些示例可以使用cargo test --doc
进行测试。
将#![deny(missing_docs)]
添加到src/lib.rs
的顶部以强制所有公共项都具有文档注释。然后向您已经实现的类型和方法中添加文档注释,编写注释时请遵循文档指南。给每个函数一个例子,并确保他们通过cargo test --doc
。
我的作业:
1 |
|
第6部分:使用clippy
和rustfmt
规范代码
clippy
和rustfmt
是规范Rust代码的工具。clippy
有助于确保代码风格符合规范,也能够检查出一些可能产生错误的编程模式。rustfmt
会强制代码格式保持一致。在您有空的时候可以单击这些链接并阅读它们的文档。这些都是较为复杂的工具,功能远不止文中所述。
这两个工具都包含在Rust工具链中,但默认情况下不安装。可以使用以下rustup
命令进行安装:
1 | rustup component add clippy |
现在就试试吧。
这两个工具均以cargo子命令的形式调用,clippy
使用cargo clippy
、rustfmt
使用cargo fmt
调用。请注意,cargo fmt
会修改您的源代码,因此请在运行之前提交您的工作,以避免产生意料之外的非必要修改,之后您可以再使用git commit --amend
将更改作为先前提交的一部分包含在内。或者只是将它们作为自己的格式化提交 — 对于Rust来说,在一系列提交之后进行clippy
和rustfmt
是很常见的,比如在提交一个pull request前。
在您的项目上执行cargo clippy
并按照建议修改代码。在您的项目上执行cargo fmt
并提交所有更改。
建议阅读rustup
、clippy
和rustfmt
文档,因为这些是您经常会用到的工具。
恭喜,您完成了你的第一个项目!如果您愿意,还可以试着完成下面的扩展作业,扩展作业是可选的。
此外,还可以使用下面的命令探索Rust工具链:
1 | rustup component list |
干得漂亮,同学,休息一下吧。
扩展1:structopt
在这个项目中,我们使用clap
来解析命令行参数。通常将程序已解析的命令行参数表示为一个结构体,比如叫做Config
或Options
。这就需要调用clap
的 ArgMatches
类型上的相应方法。对于较大的程序,这两个步骤都需要大量模板化的代码。structopt
crate能够让您定义一个可以自动生成并注释clap
命令行解释器的Config
结构体,来大幅减少这些模板化代码。一些人发现使用这种方式,比显式编写clap
代码更方便。
修改您的程序以使用structopt
来解析命令行参数,而不是直接使用clap
。
我的作业:
由于我在2022年3月初完成的作业,所以在我的Cargo.toml
中声明的clap
依赖已经具备了derive
这一新特性。
1 | clap = { version = "3.1.5", features = ["derive"] } |
而derive
特性就是装饰在某结构体上,通过宏给该结构体隐式实现各种功能。因此不再需要编写ArgMatches
的相应方法,也不再需要使用structopt
简化实现了:
1 | use clap::{Parser, Subcommand}; |
通过测试:
1 | running 0 tests |