快速入门¶
克隆仓库¶
开始进行大作业编程的第一步是从 Tsinghua Git 上克隆仓库。首先打开 VSCode(如果在 Windows 上,需要用 VSCode 的 Remote 功能连接到 WSL),在 Terminal 中,用下面的命令克隆仓库并用 VSCode 打开项目:
git clone git@git.tsinghua.edu.cn:rust-course/2023/wordle/wordle-你的清华用户名.git
cd wordle-你的清华用户名
code .
- 第一句命令的含义是从 Tsinghua Git 上克隆仓库,仓库的 URL 可以在 Tsinghua Git 上点击 Clone,然后在 Clone with SSH 下找到。
- 第二句命令的含义是进入到刚刚克隆的仓库,因为 Git 克隆的时候,会在当前目录下创建子目录,默认目录名称就是仓库的名称,也就是
wordle-你的清华用户名
;cd
是 Change Directory 的缩写,意思是把当前目录修改到子目录中。 - 第三句命令的含义是用 VSCode 打开当前目录,如果用 VSCode 打开项目所在的目录,Rust 插件就可以正常找到 Cargo 项目并且工作,否则就无法提供代码补全等高级功能了。
运行命令以后,应该会得到一个新的 VSCode 窗口,之后大作业的所有开发,都会在这个 VSCode 窗口中完成。
需要注意的是,如果你在 Windows 环境下(而不是 WSL)克隆,Git 会把代码中的 LF(换行)替换成 CRLF(回车换行),这会导致自动评测的时候将 CR(回车)引入到命令行参数,将 CRLF(回车换行)引入到标准输入。这可能会影响代码的行为,例如表现为死循环或者等待标准输入。
为了解决这个问题,有如下四种解决方案:
- 在 WSL 内克隆仓库,保证换行符是 LF
- 使用 dos2unix 工具,把所有 CRLF 换行符转换回 LF
- 修改代码,对参数和输入进行处理,保证无论是否有额外的 CR(回车),都可以正常工作。
- (需要比较熟悉 Git 操作)参考 https://stackoverflow.com/questions/1967370/git-replacing-lf-with-crlf,将 core.crlf 设置为 input 模式(如果已经克隆则需要
git rm --cached -r
然后git reset --hard
,注意 reset 之前保存好正在进行的工作)。
小结:克隆了大作业代码仓库,并且用 VSCode 打开。
模板代码¶
仓库中提供了初始的模板代码,首先是 src/main.rs
,定义了 main
函数:
use console;
use std::io::{self, Write};
/// The main function for the Wordle game, implement your own logic here
fn main() -> Result<(), Box<dyn std::error::Error>> {
let is_tty = atty::is(atty::Stream::Stdout);
if is_tty {
println!(
"I am in a tty. Please print {}!",
console::style("colorful characters").bold().blink().blue()
);
} else {
println!("I am not in a tty. Please print according to test requirements!");
}
if is_tty {
print!("{}", console::style("Your name: ").bold().red());
io::stdout().flush().unwrap();
}
let mut line = String::new();
io::stdin().read_line(&mut line)?;
println!("Welcome to wordle, {}!", line.trim());
// example: print arguments
print!("Command line arguments: ");
for arg in std::env::args() {
print!("{} ", arg);
}
println!("");
Ok(())
}
这段代码的所有部分都可以删掉重新写,放在模板中只是为了演示一些第三方库的用法。例如如何使用 let is_tty = atty::is(atty::Stream::Stdout);
语句来判断目前处于交互模式还是测试模式:
- 如果
is_tty == true
,则使用较为友好和直观的输出,此时程序处于交互模式。此时要从优化用户体验的角度来设计,例如更多颜色,更接近网页上的显示的排布方式等等。 - 如果
is_tty == false
,则严格遵守后面会定义的输出格式,此时程序处于测试模式。此时输出的内容都会直接发送给测试程序,因此不要输出颜色或者额外的内容。如果有任何不可恢复的错误(如参数格式错误、文件不存在等),程序必须以非正常返回值退出。
这么做是为了方便自动测试。和 Online Judge 一样,自动测试程序会通过标准输入输出与 Wordle 程序进行交互,因此不希望出现额外的输出来妨碍自动测试程序。在运行程序的时候,如果把标准输出重定向到了文件或者管道,那么 let is_tty = atty::is(atty::Stream::Stdout)
就会得到 is_tty == false
,进入测试模式。例子如下:
$ cargo run
# is_tty = true
$ cargo run < input
# is_tty = true, stdin redirected
$ cargo run > output
# is_tty = false, stdout redirected
$ cargo run < input > output
# is_tty = false, stdin/stdout redirected
$ cargo run | tee
# is_tty = false, stdout piped
接着,代码中出现了 console
库的使用,来打印出带颜色的文字:
console::style("colorful characters").bold().blink().blue()
:对文本colorful characters
设置加粗(.bold()
)、闪烁(.blink()
,需要终端支持,不一定会有效)和蓝色(.blue()
)。函数会返回一个String
,不会直接输出,因此还需要传给println!
进行实际的输出。console::style("Your name: ").bold().red()
:对文本Your name:
设置加粗和红色
在 Rust 语言中,第三方库(即除语言的标准库之外的库)对应的是外部的 crate,所有公开发布的 crate 都在 crates.io 上可以找到,它相当于 Python 语言的 PyPI,Java 语言的 Maven。如果想要查询第三方库的文档,以 console
库为例,做法是:
- 访问 crates.io
- 在搜索框中输入要寻找的库的名字,例如输入
console
然后回车 - 看到第一个名为
console
的库的结果,可以看到下面有 Documentation 的链接 - 点击会跳转到 https://docs.rs/console/latest/console/,也就是 console 这个库的最新文档
-
文档往下翻到
Colors and Styles
,就可以看到如何用console
库设置输出文本格式的例子:use console::style; println!("This is {} neat", style("quite").cyan());
-
继续往下翻,可以看到这个库提供的 Struct 和函数。点击 Style,就可以看到 Struct Style 的文档。往下翻,就可以看到它提供了各种函数,其中就包括了上面用到的函数:
pub fn red(self) -> Style
pub fn cyan(self) -> Style
pub fn blink(self) -> Style
pub fn bold(self) -> Style
-
可以继续探索文档,看看第三方库都提供了哪些函数,如果对实现感兴趣,也可以点击旁边的 Source 链接,可以直接定位到源代码,学习别人的写法
除了 src/main.rs
以外,还提供了 src/builtin_words.rs
文件,里面存储了“可用词”和“候选词”两个词库,你不需要修改里面的内容。
小结:阅读了模板代码,学习了交互模式和测试模式,接触了两个第三方库的用法,学会了怎么阅读第三方库的文档。
编译运行¶
可以用 cargo run
程序来运行 Wordle。实际上,这个命令做了两件事情:
- 运行
cargo build
来编译 wordle - 运行 wordle,默认情况下是运行
target/debug/wordle
你也可以从运行输出中看出它做的事情:
$ cargo run
Compiling wordle v0.1.0 (/Volumes/Data/rust-course/tpl_wordle)
Finished dev [unoptimized + debuginfo] target(s) in 0.39s
Running `target/debug/wordle`
I am in a tty. Please print colorful characters!
Your name:
在作业要求中,经常需要向程序传递命令行参数。例如在作业要求中,需要通过命令行传递 -w CARGO
参数,此时可以使用 cargo run -- -w CARGO
命令:
$ cargo run -- -w CARGO
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/wordle -w CARGO`
在 --
之后的命令行参数都原样地传递给了 Wordle 程序。类似地,你也可以传递其他参数。使用 --
分隔的目的是让 cargo run
识别出哪些参数是要传给 cargo run
自己的,哪些参数是要传给 Wordle 程序的。
小结:用 cargo run
命令运行 Wordle 程序,可以用 cargo run -- 命令行参数
命令来把命令行参数传递给 Wordle 程序。
第三方库¶
我们再尝试使用另一个第三方库 colored
,来实现和 console
类似的功能。
访问的文档:https://docs.rs/colored,可以看到它已经提供了一个代码例子:
use colored::Colorize;
"this is blue".blue();
"this is red".red();
"this is red on blue".red().on_blue();
"this is also red on blue".on_blue().red();
"you can use truecolor values too!".truecolor(0, 255, 136);
"background truecolor also works :)".on_truecolor(135, 28, 167);
"you can also make bold comments".bold();
println!("{} {} {}", "or use".cyan(), "any".italic().yellow(), "string type".cyan());
"or change advice. This is red".yellow().blue().red();
"or clear things up. This is default color and style".red().bold().clear();
"purple and magenta are the same".purple().magenta();
"bright colors are also allowed".bright_blue().on_bright_white();
"you can specify color by string".color("blue").on_color("red");
"and so are normal and clear".normal().clear();
String::from("this also works!").green().bold();
format!("{:30}", "format works as expected. This will be padded".blue());
format!("{:.3}", "and this will be green but truncated to 3 chars".green());
阅读上面的代码,已经大概可以看出这个库要如何使用了。接下来,需要使用命令 cargo add colored
把这个库引入到项目的依赖中:
# Add latest version
$ cargo add colored
Updating `tuna` index
Adding colored v2.0.0 to dependencies.
Features:
- no-color
# If you want a specific version instead of latest
$ cargo add colored@1.0.0
Updating `tuna` index
Adding colored v1.0.0 to dependencies.
在 Cargo.toml
文件中也可以看到它的依赖信息已经出现了:
[dependencies]
colored = "2.0.0"
接下来,修改 src/main.rs
,根据刚刚从它的文档中学习到的使用方法来尝试一下:
use colored::Colorize;
fn main() {
println!(
"{} {} {} {} {} {}",
"blue".blue(),
"yellow".yellow(),
"red".red(),
"bold".bold(),
"italic".italic(),
"bold red".bold().red()
);
}
就可以看到下面的输出:
其他库也是类似的。总结一下使用新的第三方库的流程:
- 查看第三方库的文档:
https://docs.rs/第三方库
- 根据文档的样例代码来理解这个第三方库的使用方式
- 使用
cargo add 第三方库
命令把它引入到项目中 - 在代码中使用第三方库
小结:学会如何把新的第三方库引入到项目中,并基于第三方库文档提供的代码,实现自己想要的功能。
自动测试¶
大作业的代码框架提供了自动测试,可以在本地测试基础要求部分的功能,帮助你检查代码是否出错。
自动测试的运行方式是:
cargo test -- --test-threads=1
这条命令的意思是,串行(--test-threads=1
)地运行所有的测试。这里指定了串行,是因为 Wordle 会涉及到文件的存取。初始代码下运行自动测试,可以看到所有的测试点都失败:
failures:
test_01_20_pts_basic_one_game
test_02_5_pts_specify_answer
test_03_10_pts_difficult_mode
test_04_5_pts_continue_game_and_statistics
test_05_5_pts_specify_offset_and_seed
test_06_5_pts_specify_word_list
test_07_5_pts_save_game_state
test_08_5_pts_config_file
test result: FAILED. 0 passed; 8 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.60s
你在接下来的时间内,就要一步一步地完成各项要求,把测试结果变成 PASSED
。
从测试结果的底部往上翻,就可以看到每个测试点的具体情况。具体地,它会告诉你正在运行哪个测试点,你的程序输出的是什么,正确输出是什么,如
---- test_01_20_pts_basic_one_game stdout ----
thread '<unnamed>' panicked at 'assertion failed: `(left == right)`: case 01_01_single_game incorrect
Diff < left / right > :
<I am not in a tty. Please print according to test requirements!
<Welcome to wordle, cargo!
<Command line arguments: target/debug/wordle
>RRYRY XXXXXRXXXXXRXXYXXYXXXXXXXX
>YRRRG YXXRXRXXRXXRXXGXXYXXRXXXXX
>GYYRR YXGRRRXXRXXRXRGXXYXXRXXXXX
>GGGGG GXGRRRGXRXXRXRGXXGXXRXXXXX
>CORRECT 4
', tests/common.rs:140:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'test_01_20_pts_basic_one_game' panicked at 'timeout: the function call took 573 ms. Max time 3000 ms', tests/basic_requirements.rs:7:1
自动测试程序会自动比对 Wordle 程序的输出和正确的输出。红色代表 Wordle 程序的错误输出,绿色代表正确的输出。你的目标就是消除掉所有的错误。具体怎么做,会在未来的作业要求文档中细化。
如果想要测试单个测例,可以在命令行中指定测例的名字:
cargo test -- test_01_20_pts_basic_one_game
此时就不再需要 --test-threads=1
参数了,因为只运行一个测试,不指定这个参数也会是串行的。
需要注意的是,自动测试并不能覆盖所有情况,意味着自动测试通过了 不代表可以得到对应要求的满分 。此外,提高要求没有对应的自动测试,需要自己手动测试。
小结:学习了自动测试的运行方式,学会了如何阅读自动测试的输出结果。
手动测试¶
自动测试虽然方便,但是在开发的时候,可能会希望手动运行测试的程序,这样可以直接看到输出的内容,减少调试的时间。
以测例 02_01_specify_answer
为例子,首先进入仓库的 tests/cases
目录下,可以看到这个测例的三个文件:
02_01_specify_answer.in
:程序接收的标准输入02_01_specify_answer.args
:程序接收的命令行参数02_01_specify_answer.ans
:程序在测试模式下,应该向标准输出打印的内容
那么,使用 cargo test -- test_02_5_pts_specify_answer
评测的时候,实际上运行了下面的命令:
cargo build
./target/debug/wordle -w build < tests/cases/02_01_specify_answer.in > tests/cases/02_01_specify_answer.out
这里的 -w build
是 02_01_specify_answer.args
中指定的命令行参数。由于现在还没看到作业要求,所以你可能不知道这些参数是什么意思,等看完作业要求以后,可以再进行回顾。
然后比对 02_01_specify_answer.out
和 02_01_specify_answer.ans
的内容,如果一致,并且程序退出返回值符合预期,则评测通过。
对于需要读取和保存状态的测例,自动测试程序还会做如下操作,以 07_01_save_state
为例:
- 复制
tests/cases/07_01_save_state.before.json
到tests/cases/07_01_save_state.run.json
- 运行 wordle 程序,额外添加命令行参数
--state tests/cases/07_01_save_state.run.json
- 判断
tests/cases/07_01_save_state.run.json
与tests/cases/07_01_save_state.after.json
是否表示同样的 JSON 值
所以如果自动测试报错了,可以按照上面的命令进行手工测试,此时比较方便使用各种调试手段,例如使用下一节的调试功能。
小结:当自动测试不通过的时候,可以尝试手动测试出问题的测例,此时可以更方便地调试程序。
调试功能¶
推荐使用 VSCode 的一大原因,就是 VSCode 对 Rust 调试功能的支持比较完善。合理利用调试器来调试,可以极大地加快开发的效率,帮助你解决各种看起来很玄学的问题。
在之前的课程中,你应该已经学过如何用调试器对 C/C++ 语言编写的程序进行调试。如果没有,至少也学过如何用 printf
来打印中间变量的方法。在实践中,基于调试器和基于打印的调试方法各有优劣,但二者都需要掌握,才能应付更多的场景。
在 VSCode 中使用调试器的方法特别简单。当你打开 src/main.rs
,你会发现在 main
函数上面出现了两个“按钮”:
▶ Run | Debug
fn main() -> Result<(), Box<dyn std::error::Error>> {
两个按钮分别对应 Run(执行)和 Debug(调试)。按下 Debug,就启动了调试器,就这么简单!接下来,介绍 VSCode 调试的任务就交给 官方文档,这里就不再重复了。
除了在 UI 上进行单步执行、打断点或者继续执行,你还可以修改 launch.json
中的内容来自定义传入程序的命令行参数和进行输入输出重定向等。结合上一小节的内容,可以修改 launch.json
的配置,按照某一个测例设置命令行参数,进行输入重定向,那么就可以用调试器测试程序在该测例上的行为。
如果安装了 CodeLLDB 扩展,还可以让它帮你自动创建调试配置:打开 Rust 项目后,点击左侧 Run and Debug
按钮,点击 Show all automatic debug configurations
,选择 Add configuration
,选择 LLDB
,此时会弹出窗口询问是否要自动从 Cargo 项目中生成 launch.json
配置,选择 Yes
。此时就可以在 launch.json
里看到多个调试配置,对应可执行文件、测试等等。接着,参考 CodeLLDB Manual 修改配置文件,可以实现添加命令行参数,进行输入输出重定向等等。
小结:VSCode Rust 插件提供了非常方便的调试器支持,不要忘记使用。结合手动测试,可以提高调试效率。
提交代码¶
代码需要提交到 Tsinghua Git 上,因此需要用 Git 来把代码以 commit 的方式,push 到 Tsinghua Git 上。
简单来说,每当你:
- 开发一个小功能
- 实现一个小的要求
- 完成了一个大要求的一部分
- 修复了一个 Bug
等等,当你觉得目前的代码值得提交的时候,使用 Git 命令来生成一个 commit:
git add .
git commit -m "在这里写提交说明"
然后提交到 Tsinghua Git 上:
git push
这里只考虑了最基本的用法,更详细的用法还需要去学习 Git。
在代码规范文档中,有关于对 Git 提交历史和提交说明的要求,请按照代码规范进行。
小结:了解如何用 Git 提交代码,学习关于 Git 的代码规范。
持续集成¶
代码仓库还配置了持续集成(Continuous Integration,缩写 CI),持续集成的意思就是,每当你用 Git Push 新的 commit 到 Tsinghua Git 的时候,Tsinghua Git 会自动运行自动测试程序,也就是在课程组提供的服务器上运行下面的命令:
cargo build
cargo test -- --test-threads=1
并且会记录命令的输出和结果,如果命令以非 0 码退出,那么你就会在 Tsinghua Git 上看到一个红色的叉。你可以点进去,直到看到完整的自动测试历史。当所有的自动测试通过的时候,就会看到一个绿色的勾。
持续集成带来了很多好处:
- 每次提交代码都会自动评测,例如在实现提高要求的时候,不小心把基础要求改坏了,那么自动评测就会帮你发现问题,并通过邮件提醒你
- 在 Linux 环境下测试一遍,减少因为环境不同而导致的问题的发生
- 可以看到自己的努力的完整历史,看到自己一步一步完成了基础要求,给自己更多成就感
小结:Tsinghua Git 配置了持续集成,每次提交都会进行自动测试,可以在 Tsinghua Git 网站上查看自动测试结果。
开始编程¶
你已经完成了快速入门,接下来就是按照大作业的要求,开始编程!
请浏览下一个文档以继续。