跳转至

快速入门

克隆仓库

开始进行大作业编程的第一步是从 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(回车换行)引入到标准输入。这可能会影响代码的行为,例如表现为死循环或者等待标准输入。

为了解决这个问题,有如下四种解决方案:

  1. 在 WSL 内克隆仓库,保证换行符是 LF
  2. 使用 dos2unix 工具,把所有 CRLF 换行符转换回 LF
  3. 修改代码,对参数和输入进行处理,保证无论是否有额外的 CR(回车),都可以正常工作。
  4. (需要比较熟悉 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 库为例,做法是:

  1. 访问 crates.io
  2. 在搜索框中输入要寻找的库的名字,例如输入 console 然后回车
  3. 看到第一个名为 console 的库的结果,可以看到下面有 Documentation 的链接
  4. 点击会跳转到 https://docs.rs/console/latest/console/,也就是 console 这个库的最新文档
  5. 文档往下翻到 Colors and Styles,就可以看到如何用 console 库设置输出文本格式的例子:

    use console::style;
    
    println!("This is {} neat", style("quite").cyan());
    
  6. 继续往下翻,可以看到这个库提供的 Struct 和函数。点击 Style,就可以看到 Struct Style 的文档。往下翻,就可以看到它提供了各种函数,其中就包括了上面用到的函数:

    1. pub fn red(self) -> Style
    2. pub fn cyan(self) -> Style
    3. pub fn blink(self) -> Style
    4. pub fn bold(self) -> Style
  7. 可以继续探索文档,看看第三方库都提供了哪些函数,如果对实现感兴趣,也可以点击旁边的 Source 链接,可以直接定位到源代码,学习别人的写法

除了 src/main.rs 以外,还提供了 src/builtin_words.rs 文件,里面存储了“可用词”和“候选词”两个词库,你不需要修改里面的内容。

小结:阅读了模板代码,学习了交互模式和测试模式,接触了两个第三方库的用法,学会了怎么阅读第三方库的文档。

编译运行

可以用 cargo run 程序来运行 Wordle。实际上,这个命令做了两件事情:

  1. 运行 cargo build 来编译 wordle
  2. 运行 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()
    );
}

就可以看到下面的输出:

其他库也是类似的。总结一下使用新的第三方库的流程:

  1. 查看第三方库的文档:https://docs.rs/第三方库
  2. 根据文档的样例代码来理解这个第三方库的使用方式
  3. 使用 cargo add 第三方库 命令把它引入到项目中
  4. 在代码中使用第三方库

小结:学会如何把新的第三方库引入到项目中,并基于第三方库文档提供的代码,实现自己想要的功能。

自动测试

大作业的代码框架提供了自动测试,可以在本地测试基础要求部分的功能,帮助你检查代码是否出错。

自动测试的运行方式是:

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 目录下,可以看到这个测例的三个文件:

  1. 02_01_specify_answer.in:程序接收的标准输入
  2. 02_01_specify_answer.args:程序接收的命令行参数
  3. 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 build02_01_specify_answer.args 中指定的命令行参数。由于现在还没看到作业要求,所以你可能不知道这些参数是什么意思,等看完作业要求以后,可以再进行回顾。

然后比对 02_01_specify_answer.out02_01_specify_answer.ans 的内容,如果一致,并且程序退出返回值符合预期,则评测通过。

对于需要读取和保存状态的测例,自动测试程序还会做如下操作,以 07_01_save_state 为例:

  1. 复制 tests/cases/07_01_save_state.before.jsontests/cases/07_01_save_state.run.json
  2. 运行 wordle 程序,额外添加命令行参数 --state tests/cases/07_01_save_state.run.json
  3. 判断 tests/cases/07_01_save_state.run.jsontests/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 上。

简单来说,每当你:

  1. 开发一个小功能
  2. 实现一个小的要求
  3. 完成了一个大要求的一部分
  4. 修复了一个 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 上看到一个红色的叉。你可以点进去,直到看到完整的自动测试历史。当所有的自动测试通过的时候,就会看到一个绿色的勾。

持续集成带来了很多好处:

  1. 每次提交代码都会自动评测,例如在实现提高要求的时候,不小心把基础要求改坏了,那么自动评测就会帮你发现问题,并通过邮件提醒你
  2. 在 Linux 环境下测试一遍,减少因为环境不同而导致的问题的发生
  3. 可以看到自己的努力的完整历史,看到自己一步一步完成了基础要求,给自己更多成就感

小结:Tsinghua Git 配置了持续集成,每次提交都会进行自动测试,可以在 Tsinghua Git 网站上查看自动测试结果。

开始编程

你已经完成了快速入门,接下来就是按照大作业的要求,开始编程!

请浏览下一个文档以继续。