【Rust】がばがばRust独学 - 12. I/O Project
Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)
今回は、I/Oについて学びつつ、気になった点をコード確認した結果を記事にしています。
- versions
$ rustc --version rustc 1.40.0 (73528e339 2019-12-16)
- Accepting Command Line Arguments
- Reading a File
- Refactoring to Improve Modularity and Error Handling
- Developing the Library’s Functionality with Test-Driven Development
- Working with Environment Variables
- Writing Error Messages to Standard Error Instead of Standard Output
- まとめ
- 最後に
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つの問題点を提示しています。
main
関数による2つのタスク(コマンドラインの読み込み、ファイルの読み取り)を実行main
の肥大化への懸念
query
やfilename
、contents
のスコープ- スコープ内の変数が多いほど、変数追跡が難しくなることへの懸念
expect
のメッセージ- 失敗の可能性(ファイルがない場合や権限がない場合)などが分からず、UXが悪い
- コマンドライン引数がない場合の処理
index out of bounds
しか出力されないため、UXが悪い
Separation of Concerns for Binary Projects
問題点を解決するために、 main.rs
と lib.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から繰り返す。
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!
を利用することで、標準エラー出力となる。
まとめ
env::args().collect()
によりコマンドライン引数を利用可能use std::env;
によるenv
の呼び出しが必須
fs::read_to_string
によりファイル呼び出しが利用可能use std::fs;
によるenv
の呼び出しが必須
- メンテナンスのために、
main.rs
、lib.rs
に綺麗に分割が必要 - TDDで開発しよう
env::var("XXXXX").is_err()
により環境変数を利用可能eprintln!
で標準エラー出力
最後に
I/O を絡めた開発手法も含めて、エラーの引き回し方など基本的なプロジェクトの作成方法がおさえられる、良いドキュメントでした。
プロジェクト開発する前に読んでおくべきですね・・・
次は functional language featusです。
【Rust】がばがばRust独学 - 11. Tests - 3 Test Organization
Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)
今回も、Testについて学びつつ、気になった点をコード確認した結果を記事にしています。
- versions
$ rustc --version rustc 1.40.0 (73528e339 2019-12-16)
Test Organization
Test Organization - The Rust Programming Language
両方の種類のテストを作成することが、ライブラリを期待通りに動作させるために重要とのことです。
- unit tests (ユニットテスト)
- integration tests (統合テスト)
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;
と対象のテストファイルに記述することによって、サブモジュールとしての利用が可能になります。
まとめ
- unit tests (ユニットテスト)
src/lib.rs
などsrc
に記述し、#[cfg(test)]
アノテーションを利用可能
- integration tests (統合テスト)
tests
配下にファイルを格納することによってテストが可能- サブモジュール利用の場合は、
test/xxx/mod.rs
とすることによってモジュール可能mod xxx;
をテストコードに記述することにより、モジュールを利用可能
最後に
Unit Testによって、srcが長くなってしまいそうだなぁというのが正直な感想です。
ただ、他言語の場合、単体テストが乱雑に配置されるパターンも往々にあるため、単体テストがしやすい状態ではあるかもしれないです。
大規模開発になった場合、ビジネスロジックが肥大化して、さらに単体テストで圧迫するなどが考えられますね・・・
次回は、I/Oになります。
【Rust】がばがばRust独学 - 11. Tests - 2 Controlling How Tests Are Run
Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)
今回も、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 --help
:test
に対してのヘルプcargo test -- --help
:test
により生成されたテストバイナリにかかるヘルプ
テストバイナリの並列実行数の変更
例えば、各テストに依存関係がある場合、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に上がっていますね。
確かに、 -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が出ていたためつけております。
オプション周りがしっかりしているの、良いですね。 --include-ignored
はオプションなしで動いてほしいですね・・・
次回もテストの残り部分を進めます!