Charaken 技術系ブログ

技術系に関して学んだことや、気になったことを記事にしていく。

【Rust】がばがばRust独学 - 12. I/O Project

f:id:charaken:20191223210559p:plain

Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)

doc.rust-lang.org

今回は、I/Oについて学びつつ、気になった点をコード確認した結果を記事にしています。

  • versions
$ rustc --version
rustc 1.40.0 (73528e339 2019-12-16)


Accepting Command Line Arguments

An I/O Project: Building a Command Line Program - The Rust Programming Language

引数読み取り

env::args().collect() により、引数の読み取りを実施できます。

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
}
bash-3.2$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/minigrep`
["target/debug/minigrep"]
bash-3.2$ cargo run needle haystack
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/minigrep needle haystack`
["target/debug/minigrep", "needle", "haystack"]

引数の格納

&args[x] により引数を格納することが可能です。

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);

    let query = &args[1];
    let filename = &args[2];

    println!("Searching for {}", query);
    println!("In file {}", filename);
}
$ cargo run searchstring example-filename.txt
["target/debug/minigrep", "searchstring", "example-filename.txt"]
Searching for searchstring
In file example-filename.txt

もちろん、直でindexを指定しているため、引数が足りていない場合はpanicを引き起こします。

bash-3.2$ cargo run 
   Compiling minigrep v0.1.0 (/Users/k-hara/Work/training-rust/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/minigrep`
["target/debug/minigrep"]
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14/src/libcore/slice/mod.rs:2796:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Reading a File

Reading a File - The Rust Programming Language

demoコードと同様に poem.txt を作成し、下記のように fs::read_to_string を利用することでファイルを取得可能です。

use std::env;
use std::fs;

fn main() {
    //
    // Accepting Command Line Arguments
    //
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);

    let query = &args[1];
    let filename = &args[2];

    println!("Searching for {}", query);
    println!("In file {}", filename);

    //
    // Reading a File
    //
    let contents = fs::read_to_string(filename).expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}
bash-3.2$ cargo run the poem.txt
   Compiling minigrep v0.1.0 (/Users/k-hara/Work/training-rust/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.33s
     Running `target/debug/minigrep the poem.txt`
["target/debug/minigrep", "the", "poem.txt"]
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

Refactoring to Improve Modularity and Error Handling

Refactoring to Improve Modularity and Error Handling - The Rust Programming Language

公式ドキュメントでは、作成したプログラムに対して4つの問題点を提示しています。

  1. main 関数による2つのタスク(コマンドラインの読み込み、ファイルの読み取り)を実行
    • main の肥大化への懸念
  2. queryfilenamecontents のスコープ
    • スコープ内の変数が多いほど、変数追跡が難しくなることへの懸念
  3. expect のメッセージ
    • 失敗の可能性(ファイルがない場合や権限がない場合)などが分からず、UXが悪い
  4. コマンドライン引数がない場合の処理
    • index out of bounds しか出力されないため、UXが悪い

Separation of Concerns for Binary Projects

問題点を解決するために、 main.rslib.rs に綺麗に分割した方が良いです。

公式ドキュメントより、 main.rs の責務を掻い摘むと、下記のようになります。

  • ロジックは lib.rs に分割して、呼び出しを main.rs の責務とする
  • エラーが含まれるロジックに対してのエラー処理を main.rs の責務とする

Developing the Library’s Functionality with Test-Driven Development

Developing the Library’s Functionality with Test Driven Development - The Rust Programming Language

search 関数をTDDを利用して作成するということでした。

記載されている手順をまとめると、下記の流れでした。

  1. 失敗するテスト作成・実行し、予測通りになっているか確認
  2. テストに合格する十分なコードを作成
  3. 作成したコードをリファクタリングして、テストをパスか確認
  4. 手順1から繰り返す。

Working with Environment Variables

Working with Environment Variables - The Rust Programming Language

env::var("CASE_INSENSITIVE").is_err() により環境変数を利用可能とする。

デモコードでは、CASE_INSENSITIVE により、大文字・小文字を識別するかどうかを変更している。


Writing Error Messages to Standard Error Instead of Standard Output

Writing Error Messages to Standard Error Instead of Standard Output - The Rust Programming Language

eprintln! を利用することで、標準エラー出力となる。


まとめ

github.com

  • env::args().collect() によりコマンドライン引数を利用可能
    • use std::env; による env の呼び出しが必須
  • fs::read_to_string によりファイル呼び出しが利用可能
    • use std::fs; による env の呼び出しが必須
  • メンテナンスのために、 main.rslib.rs に綺麗に分割が必要
  • TDDで開発しよう
  • env::var("XXXXX").is_err() により環境変数を利用可能
  • eprintln!標準エラー出力

最後に

I/O を絡めた開発手法も含めて、エラーの引き回し方など基本的なプロジェクトの作成方法がおさえられる、良いドキュメントでした。

プロジェクト開発する前に読んでおくべきですね・・・

次は functional language featusです。

【Rust】がばがばRust独学 - 11. Tests - 3 Test Organization

f:id:charaken:20191223210559p:plain

Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)

doc.rust-lang.org

今回も、Testについて学びつつ、気になった点をコード確認した結果を記事にしています。

  • versions
$ rustc --version
rustc 1.40.0 (73528e339 2019-12-16)


Test Organization

Test Organization - The Rust Programming Language

両方の種類のテストを作成することが、ライブラリを期待通りに動作させるために重要とのことです。

Unit Tests

#[cfg(test)] アノテーションcargo test 時にのみ実行され、 cargo build 時には実行されないため、Integration Testsに比べてコンパイルタイムが短縮可能です。

また、private functionについてもテストすることが可能です。

pub fn add_two(a: i32) -> i32 {
    internal_adder(a, 2)
}

fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn internal() {
        assert_eq!(4, internal_adder(2, 2));
    }
}
running 1 test
test tests::internal ... ok

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

Integration Tests

tests/xxx.rs としてテストを記述することが可能です。

これにより、ライブラリのパブリックAPIのテストが可能になります。

例えば、公式ドキュメントのデモコード(Test Organization - The Rust Programming Language)を利用した場合、

use demo_test_organization;

#[test]
fn it_adds_two() {
    assert_eq!(4, demo_test_organization::add_two(2));
}
$ cargo test
    Finished test [unoptimized + debuginfo] target(s) in 0.00s
     Running target/debug/deps/demo_test_organization-ead944c9e6e8c86e

running 1 test
test tests::internal ... ok

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

     Running target/debug/deps/integration_test-dc19df7dcdc5b51f

running 1 test
test it_adds_two ... ok

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

   Doc-tests demo_test_organization

running 0 tests

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

上記のようにテストすることが可能です。

また、Integration Testの単一テストファイルだけをテストしたい場合は、 --test オプションを利用することによって実行可能です。

$ cargo test --test integration_test
    Finished test [unoptimized + debuginfo] target(s) in 0.00s
     Running target/debug/deps/integration_test-dc19df7dcdc5b51f

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
テスト時のサブモジュール

テスト用のサブモジュールを利用する場合、サブモジュールとして tests/common.rs のように直下に配置してしまった場合は、 cargo test の中に含まれてしまいます。

そのため、 tests/common/mod.rs としてモジュール化し、

mod common;

と対象のテストファイルに記述することによって、サブモジュールとしての利用が可能になります。


まとめ

github.com

  • unit testsユニットテスト
  • integration tests (統合テスト)
    • tests 配下にファイルを格納することによってテストが可能
    • サブモジュール利用の場合は、 test/xxx/mod.rs とすることによってモジュール可能
      • mod xxx; をテストコードに記述することにより、モジュールを利用可能

最後に

Unit Testによって、srcが長くなってしまいそうだなぁというのが正直な感想です。

ただ、他言語の場合、単体テストが乱雑に配置されるパターンも往々にあるため、単体テストがしやすい状態ではあるかもしれないです。

大規模開発になった場合、ビジネスロジックが肥大化して、さらに単体テストで圧迫するなどが考えられますね・・・

次回は、I/Oになります。

【Rust】がばがばRust独学 - 11. Tests - 2 Controlling How Tests Are Run

f:id:charaken:20191223210559p:plain

Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)

doc.rust-lang.org

今回も、Testについて学びつつ、気になった点をコード確認した結果を記事にしています。

  • versions
$ rustc --version
rustc 1.40.0 (73528e339 2019-12-16)


Controlling How Tests Are Run

公式ドキュメント:Controlling How Tests Are Run - The Rust Programming Language

デモコード: github.com

コマンドラインオプションには2種類存在します。

  • cargo test --helptest に対してのヘルプ
  • cargo test -- --helptest により生成されたテストバイナリにかかるヘルプ

テストバイナリの並列実行数の変更

例えば、各テストに依存関係がある場合、1つずつテストしたい場合があります。

そのため、threads数を1として並列実行させないようにすることも可能です。

$ cargo test -- --test-threads=1

関数出力の表示

公式ドキュメントのデモコード通りに実行すると、エラーのテストの println!("I got the value {}", a); 出力されません。

fn prints_and_returns_10(a: i32) -> i32 {
    println!("I got the value {}", a);
    10
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn this_test_will_pass() {
        let value = prints_and_returns_10(4);
        assert_eq!(10, value);
    }

    #[test]
    fn this_test_will_fail() {
        let value = prints_and_returns_10(8);
        assert_eq!(5, value);
    }
}
running 2 tests
test tests::this_test_will_pass ... ok
test tests::this_test_will_fail ... FAILED

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:19:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.


failures:
    tests::this_test_will_fail

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

しかし、 --nocapture を利用する事によって、エラーのテストに対してもアウトプットされ利用になります。

$ cargo test -- --nocapture
running 2 tests
I got the value 8
I got the value 4
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:20:9test tests::this_test_will_pass ... 
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
ok
test tests::this_test_will_fail ... FAILED

failures:

failures:
    tests::this_test_will_fail

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

テスト名を引数として単一テストやフィルタリング

下記のような関数名を利用した場合、テスト名は this_test_will_pass となります。

#[test]
fn this_test_will_pass()

この時、 this_test_will_pass を引数として渡すと、単一テストが可能です。

$ cargo test this_test_will_pass
running 1 test
test tests::this_test_will_pass ... ok

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

また、

$ cargo test this_

として実行した場合、 this_ から始まるテストを実施することが可能です。

running 2 tests
test tests::this_test_will_pass ... ok
test tests::this_test_will_fail ... FAILED

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:20:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.


failures:
    tests::this_test_will_fail

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

一部テストの無視

#[ignore] を利用することで、通常テストの実行時にはテスト項目から除外することが可能です。

    #[test]
    #[ignore]
    fn expensive_test() {
        // code that takes an hour to run
    }
running 3 tests
test tests::expensive_test ... ignored
test tests::this_test_will_pass ... ok
test tests::this_test_will_fail ... FAILED

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:20:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.


failures:
    tests::this_test_will_fail

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

除外したテストのみ実施したい場合は

$ cargo test -- --ignored

によって実行可能です。

ちなみに、除外有無関係無しに実行できるオプションがあるのですが、死にます。

$ cargo test -- --include-ignored
error: The "include-ignored" flag is only accepted on the nightly compiler

下記については、issueに上がっていますね。

github.com

確かに、 -Zunstable-options オプションを付けるとよしなに動いてくれますね・・・

$ cargo test -- --include-ignored -Zunstable-options
running 3 tests
test tests::expensive_test ... ok
test tests::this_test_will_pass ... ok
test tests::this_test_will_fail ... FAILED
...

まとめ

  • -- のセパレータによってオプション対象を分けられる
    • cargo test --help
    • cargo test -- --help
  • テストバイナリへのオプションは様々ある
    • --test-threads
    • --nocapture
    • --ignored
  • テストから除外したい場合は #[ignore] を利用する

最後に

他のオプションも試してみたいですね。

今回作成したデモコードに #![allow(dead_code)] がございますが、こちらは、メインで prints_and_returns_10 を利用していないがために、しつこくWarningが出ていたためつけております。

github.com

オプション周りがしっかりしているの、良いですね。 --include-ignored はオプションなしで動いてほしいですね・・・

次回もテストの残り部分を進めます!