Help us understand the problem. What is going on with this article?

Rust勉強中 - その3 -> ユニットテストとコマンドライン引数

自己紹介

出田 守と申します。
しがないPythonプログラマです。
情報セキュリティに興味があり現在勉強中です。CTFやバグバウンティなどで腕を磨いています。主に低レイヤの技術が好きで、そっちばかり目が行きがちです。

Rustを勉強していくうえで、読んで学び、手を動かし、記録し、楽しく学んでいけたらと思います。

環境

新しい言語を学ぶということで、普段使わないWindowsとVimという新しい開発環境で行っています。
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
RAM: 8.00GB
Editor: Vim 8.1.1
Terminal: PowerShell

前回

前回はハロワと関数の定義やその中身について簡単に触れました。
Rust勉強中 - その2

ユニットテスト

Rustには、テストする機能が標準で組み込まれています。前回のsum関数を例にテストしてみます。テストを実行するコマンドはcargo testです。

$ cargo test --help
Execute all unit and integration tests and build examples of a local package

USAGE:
    cargo.exe test [OPTIONS] [TESTNAME] [-- <args>...]

OPTIONS:
    -q, --quiet                     Display one character per test instead of one line
        --lib                       Test only this package's library unit tests
        --bin <NAME>...             Test only the specified binary
        --bins                      Test all binaries
        --example <NAME>...         Test only the specified example
        --examples                  Test all examples
        --test <NAME>...            Test only the specified test target
        --tests                     Test all tests
        --bench <NAME>...           Test only the specified bench target
        --benches                   Test all benches
        --all-targets               Test all targets
        --doc                       Test only this library's documentation
        --no-run                    Compile, but don't run tests
        --no-fail-fast              Run all tests regardless of failure
    -p, --package <SPEC>...         Package to run tests for
        --all                       Test all packages in the workspace
        --exclude <SPEC>...         Exclude packages from the test
    -j, --jobs <N>                  Number of parallel jobs, defaults to # of CPUs
        --release                   Build artifacts in release mode, with optimizations
        --features <FEATURES>       Space-separated list of features to activate
        --all-features              Activate all available features
        --no-default-features       Do not activate the `default` feature
        --target <TRIPLE>           Build for the target triple
        --target-dir <DIRECTORY>    Directory for all generated artifacts
        --manifest-path <PATH>      Path to Cargo.toml
        --message-format <FMT>      Error format [default: human]  [possible values: human, json, short]
    -v, --verbose                   Use verbose output (-vv very verbose/build.rs output)
        --color <WHEN>              Coloring: auto, always, never
        --frozen                    Require Cargo.lock and cache are up to date
        --locked                    Require Cargo.lock is up to date
        --offline                   Run without accessing the network
    -Z <FLAG>...                    Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
    -h, --help                      Prints help information

ARGS:
    <TESTNAME>    If specified, only run tests containing this string in their names
    <args>...     Arguments for the test binary

The test filtering argument TESTNAME and all the arguments following the
two dashes (`--`) are passed to the test binaries and thus to libtest
(rustc's built in unit-test and micro-benchmarking framework). If you're
passing arguments to both Cargo and the binary, the ones after `--` go to the
binary, the ones before go to Cargo. For details about libtest's arguments see
the output of `cargo test -- --help`. As an example, this will run all
tests with `foo` in their name on 3 threads in parallel:

    cargo test foo -- --test-threads 3

If the `--package` argument is given, then SPEC is a package ID specification
which indicates which package should be tested. If it is not given, then the
current package is tested. For more information on SPEC and its format, see the
`cargo help pkgid` command.

All packages in the workspace are tested if the `--all` flag is supplied. The
`--all` flag is automatically assumed for a virtual manifest.
Note that `--exclude` has to be specified in conjunction with the `--all` flag.

The `--jobs` argument affects the building of the test executable but does
not affect how many jobs are used when running the tests. The default value
for the `--jobs` argument is the number of CPUs. If you want to control the
number of simultaneous running test cases, pass the `--test-threads` option
to the test binaries:

    cargo test -- --test-threads=1

Compilation can be configured via the `test` profile in the manifest.

By default the rust test harness hides output from test execution to
keep results readable. Test output can be recovered (e.g., for debugging)
by passing `--nocapture` to the test binaries:

    cargo test -- --nocapture

To get the list of all options available for the test binaries use this:

    cargo test -- --help
main.rs
fn sum(mut a: i32, b: i32) -> i32 {
    assert!(a != 0 && b != 0);
    a = a + b;
    a
}

fn main() {
    let a: i32 = 1;
    let b: i32 = 1;
    let result = sum(a, b);
    println!("sum({}, {}) = {}", a, b, result);
    let result2 = {
        println!("Executing in block.");
        sum(result, b)
    };
    println!("sum({}, {}) = {}", result, b, result2);
}

#[test]
fn test_sum() {
    assert_eq!(sum(1, 1), 2);
    assert_eq!(sum(-1, -1), -2);
}
$ cargo test
   Compiling sum v0.1.0 (C:\Users\deta\hack\rust\sum)
    Finished dev [unoptimized + debuginfo] target(s) in 1.96s
     Running target\debug\deps\sum-7071123ff24f1e76.exe

running 1 test
test test_sum ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

#[test]をテスト関数の前に付けることで関数をテスト関数と見なされるようです。#[test]は属性(Attribute)の一つで、他にもスタイルチェックや警告などがあるようです。
assert_eq!は、第一引数と第二引数が等しいかどうかをチェックします。(==)
他にassert_ne!もあるようでこちらは第一引数と第二引数が等しくないかチェックします。(!=)

※ 属性って何やねんって思ったので調べてみました。属性(Attribute)とは、コード中のあらゆる記述に対して何らかの情報を付加するための拡張機能メタデータということのようです。

テストはテスト関数ごとに集計されるようです。テスト関数を増やせば別々のテストと見なされます。以下はテスト関数を増やした例です。

main.rs
...
#[test]
fn test_sum1() {
    assert_eq!(sum(1, 1), 2);
}

#[test]
fn test_sum2() {
    assert_eq!(sum(-1, -1), -2);
}
$ cargo test
   Compiling sum v0.1.0 (C:\Users\deta\hack\rust\sum)
    Finished dev [unoptimized + debuginfo] target(s) in 0.71s
     Running target\debug\deps\sum-7071123ff24f1e76.exe
running 2 tests
test test_sum1 ... ok
test test_sum2 ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

テスト関数を増やした分、#[test]属性も増やします。

ついでにテスト失敗時にどのようになるか確認してみます。

main.rs
...
#[test]
fn test_sum() {
    assert_eq!(sum(1, 1), 1); // false test
    assert_eq!(sum(-1, -1), -2);
}
$ cargo test
   Compiling sum v0.1.0 (C:\Users\deta\hack\rust\sum)
    Finished dev [unoptimized + debuginfo] target(s) in 0.79s
     Running target\debug\deps\sum-7071123ff24f1e76.exe

running 1 test
test test_sum ... FAILED

failures:

---- test_sum stdout ----
thread 'test_sum' panicked at 'assertion failed: `(left == right)`
  left: `2`,
 right: `1`', src\main.rs:21:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.


failures:
    test_sum

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed, to rerun pass '--bin sum'

最初のassert_eq!の答えを1に設定してみました。すると、「スレッドtest_sumでパニクりましたで。左が2やのに右が1でっせ。」というようなメッセージが出力されました。

テストについては、他にも良い感じのやり方があるようなので、Rustに慣れてきたタイミングで再度取り上げると思います。

コマンドライン引数

実行時に引数を渡せるようにしたいということで、コマンドライン引数です。

main.rs
use std::io::Write;
use std::str::FromStr;

fn sum(mut a: i32, b: i32) -> i32 {
    assert!(a != 0 && b != 0);
    a = a + b;
    a
}

fn main() {
    let mut args = Vec::new();

    for arg in std::env::args().skip(1) {
        args.push(i32::from_str(&arg).expect("THIS IS PARSE ERROR MESSAGE!!!"));
    }

    if args.len() < 2 {
        writeln!(std::io::stderr(), "Usage: sum <arg1> <arg2> ...").unwrap();
        std::process::exit(1);
    }

    if args.len()==2 {
        let a = args[0];
        let b = args[1];
        println!("sum({}, {}) = {}", a, b, sum(a, b));
    } else {
        let mut a = args[0];
        for b in &args[1..] {
            a = sum(a, *b);
        }
        println!("sum({:?}) = {}", args, a);
    }
}
$ cargo run 1 1
   Compiling sum v0.1.0 (C:\Users\deta\hack\rust\sum)
    Finished dev [unoptimized + debuginfo] target(s) in 1.00s
     Running `target\debug\sum.exe 1 1`
sum(1, 1) = 2

$ cargo run 1 2 3
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target\debug\sum.exe 1 2 3`
sum([1, 2, 3]) = 6

いきなりゴツくなりました。流れとしては以下です。

  1. コマンドライン引数の展開とパース
  2. コマンドライン引数のサイズチェック
    1. サイズが2未満の場合はusageを表示して終了。
    2. サイズが2の場合は二つを足し合わせて表示。
    3. サイズが3以上の場合は中身をすべて足し合わせて表示。

順番に解読していきます。

use宣言

main.rs
use std::io::Write;
use std::str::FromStr;
...

useを使うことでWriteとFromStrの2つのトレイトをスコープに入れています。トレイトはもう少し先で詳しく学習するとして、今のところはトレイトとはその型におけるメソッド群という理解で大丈夫そうです。(Pythonとかでいうimportとかとちょっと似てる?違うかな?)
Writeトレイトにはwrite_fmtメソッドがあり、std::io::Stderr型がWriteトレイトを持っています。そして、コード中のwriteln!マクロでエラーメッセージを出力する際に展開されて、write_fmtメソッドが呼び出されるということのようです。
同様にFromStrトレイトにはfrom_strメソッドがあり、i32型はFromStrトレイトを持っています。そして、コード中ではi32::from_strメソッドの引数にコマンドライン引数を指定して呼び出しています。このときfrom_strメソッドにより、整数にparseされます。

繰り返しますが、トレイトはもう少し後のほうで再度詳しく学習していこうと思います。

コマンドライン引数の展開とパース

main.rs
...
fn main() {
    let mut args = Vec::new();

    for arg in std::env::args().skip(1) {
        args.push(i32::from_str(&arg).expect("THIS IS PARSE ERROR MESSAGE!!!"));
    }
    ...
}

let mut args = Vec::new();ではmutな変数argsに空のVectorを代入しています。Vecとはサイズが可変のVector型で、Pythonのリストのようなもののようです。ただし、Vectorは可変ですが、それでも更新する場合はmutを付けます。
for arg in std::env::args().skip(1) {は何となく想像が付きそうです。
Rustのfor文はPythonやBashのようにfor 変数 in collectionという文法のようです。std::env::argsはコマンドライン引数のイテレータ(iterator)を返す関数です。イテレータとは要素を一つずつ繰り返し生成し、要素がなくなった時点で知らせてくれるデータ構造です。.skip(1)はイテレータが持つメソッドの一つで、要素を一つ飛ばして1番目から生成するという意味です。理由は、0番目にはコマンド自身の名前が含まれているからです。
args.push(...);は、先ほど初期化した変数argsにpush関数の引数を追加するという意味のようです。
i32::from_strは上で説明した通り、コマンドライン引数を符号なし32ビット整数としてパースします。ここでfrom_strメソッドはi32整数値を直接返しません。代わりにResult値というのを返すみたいです。試しに以下のように変更してパース結果を出力してみました。

main.rs
...
fn main() {
    let mut args = Vec::new();

    for arg in std::env::args().skip(1) {
        println!("arg = {:?}", i32::from_str(&arg));
        args.push(i32::from_str(&arg).expect("THIS IS PARSE ERROR MESSAGE!!!"));
    }
    ...
}

$ cargo run 1 a
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target\debug\sum.exe 1 a`
arg = Ok(1)
arg = Err(ParseIntError { kind: InvalidDigit })

Result値は成功ならばOk(v)を返し、失敗ならばErr(e)を返します。vは結果の値、eはエラーメッセージを示します。例では最初のコマンドライン引数で1を指定したので成功し、2つめで文字aを指定したので失敗しているのがわかりますね。そして.except("THIS IS PARSE ERROR MESSAGE!!!")は、Result値が持つメソッドで、成功ならば結果の値を返し、失敗ならばexceptに指定した引数のエラーメッセージが表示されpanicとして終了します。以下は、panicとなる例です。

$ cargo run 1 a
   Compiling sum v0.1.0 (C:\Users\deta\hack\rust\sum)
    Finished dev [unoptimized + debuginfo] target(s) in 1.01s
     Running `target\debug\sum.exe 1 a`
thread 'main' panicked at 'THIS IS PARSE ERROR MESSAGE!!!: ParseIntError { kind: InvalidDigit }', src\libcore\result.rs:999:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
error: process didn't exit successfully: `target\debug\sum.exe 1 a` (exit code: 101)

指定したエラーメッセージもちゃんと表示されていますね。

コマンドライン引数のサイズチェック(2未満の場合)

ここまでで変数argsにはコマンドライン引数が格納されているはずです。

main.rs
...
fn main() {
    ...
    if args.len() < 2 {
        writeln!(std::io::stderr(), "Usage: sum <arg1> <arg2> ...").unwrap();
        std::process::exit(1);
    }
    ...
}

これも上ですでに説明した通り、argsのサイズが2未満の場合は、writeln!マクロによって標準エラー出力をしています。unwrapメソッドは標準エラー出力が成功したかどうかをチェックします。失敗していればエラーメッセージを出力します。execptメソッドでも良いが、そこまでする必要はないというときに使用するみたいです。main関数は成功した場合は何も返しません。失敗した場合のみ終了コードを出力します。std::process::exit(1);は終了コード1で明示的に終了するという意味です。
失敗した例を以下に示します。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target\debug\sum.exe`
Usage: sum <arg1> <arg2> ...
error: process didn't exit successfully: `target\debug\sum.exe` (exit code: 1)

メッセージを出力し、終了コードも1となっていますね。

コマンドライン引数のサイズチェック(2の場合)

main.rs
...
fn main() {
    ...
    if args.len()==2 {
        let a = args[0];
        let b = args[1];
        println!("sum({}, {}) = {}", a, b, sum(a, b));
    }
    ...
}

もはや説明するまでもありませんね。格納したコマンドライン引数を取り出し、sum関数に設定して、結果を表示しています。

コマンドライン引数のサイズチェック(3以上の場合)

main.rs
...
fn main() {
    ...
    } else {
        let mut a = args[0];
        for b in &args[1..] {
            a = sum(a, *b);
        }
        println!("sum({:?}) = {}", args, a);
    }
    ...
}

for b in &args[1..] {はfor文です。argsの1番目から取り出しています。
ここで、argsに&演算子が付いています。RustはVectorなどのサイズが大きくなるものを慎重に取り扱います。値の生存期間を明確にし、不要になったメモリが即時に解放されることを保証します。そのため、Vectorの所有権は変数argsにあり、forループではその要素を借用しているということをRustに教えます。つまり、&args[1..]は1番目以降の要素への参照(reference)を借用しているということだそうです。
さらに、a = sum(a, *b);では、借用した参照を持つ変数bに*演算子が付いています。これは、参照解決(dereferences)というもので、参照先の値を返します。つまり、i32の値を返します。
ループ終了後は足し合わされた値が変数aに格納されているはずなので、それをprintln!マクロによって表示します。今までの{}はstd::fmt::Displayを使っていたのに対し、{:?}はstd::fmt::Debugを使っているみたいです。こうすることで、プログラマが見やすいようにフォーマットしてくれるようです。{:#?}を使えばもっとええ感じにしてくれるようです。

今回はここまで。
型やトレイト、所有権、借用、参照などいくつか重要そうな部分が垣間見えました。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away