Apply suggestions from code review

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
This commit is contained in:
Shu W. Nakamura
2021-01-11 12:02:33 +09:00
committed by GitHub
parent 761b5de93d
commit 23e8cb3b5d

View File

@@ -26,7 +26,7 @@ translators = ["woodyZootopia"]
## この記事を読む前に
この記事は、(古い版の)[単体テスト][_Unit Testing_]と[結合テスト][_Integration Tests_]の記事を置き換えるものです。この記事は、あなたが[最小のカーネル][_A Minimal Rust Kernel_]の記事を2019-04-27以降に読まれたことを前提にしています。おおよそ、あなたの`.cargo/config.toml`ファイルが[標準のターゲットを設定して][sets a default target]おり、[ランナー実行ファイルを定義している][defines a runner executable]ことが条件となります。
この記事は、(古い版の)[単体テスト][_Unit Testing_]と[結合テスト][_Integration Tests_]の記事を置き換えるものです。この記事は、あなたが[最小のカーネル][_A Minimal Rust Kernel_]の記事を2019-04-27以降に読んだことを前提にしています。主に、あなたの`.cargo/config.toml`ファイルが[標準のターゲットを設定して][sets a default target]おり、[ランナー実行ファイルを定義している][defines a runner executable]ことが条件となります。
<div class="note">
@@ -42,7 +42,7 @@ translators = ["woodyZootopia"]
## Rustにおけるテスト
Rustは[テストフレームワークが組み込まれて][built-in test framework]おり、なにも設定することなく単体テストを走らせることができます。何らかの結果をアサーションを使って確認する関数を作り、その関数のヘッダに`#[test]`属性をつけるだけです。その上で`cargo test`を実行すると、あなたのクレートのすべてのテスト関数を自動で見つけて実行してくれます。
Rustは[テストフレームワークが組み込まれて][built-in test framework]おり、特別な設定なしに単体テストを走らせることができます。何らかの結果をアサーションを使って確認する関数を作り、その関数のヘッダに`#[test]`属性をつけるだけです。その上で`cargo test`を実行すると、あなたのクレートのすべてのテスト関数を自動で見つけて実行してくれます。
[built-in test framework]: https://doc.rust-jp.rs/book-ja/ch11-00-testing.html
@@ -63,7 +63,7 @@ error[E0463]: can't find crate for `test`
### 独自のテストフレームワーク
ありがたいことに、Rustでは、不安定な[<ruby>`custom_test_frameworks`<rp> (</rp><rt>独自のテストフレームワーク</rt><rp>) </rp></ruby>][`custom_test_frameworks`]機能を使えば標準のテストフレームワークを置き換えることができます。この機能には外部ライブラリは必要なく、したがって`#[no_std]`環境でも動きます。これは、`#[test_case]`属性をつけられたすべての関数を集めたものをリストにして、それを引数としてユーザの指定した実行関数を呼び出すことで働きます。こうすることで、(実行関数の)実装内容によってテストプロセスを最大限コントロールできるようにしているのです。
ありがたいことに、Rustでは、不安定な[<ruby>`custom_test_frameworks`<rp> (</rp><rt>独自のテストフレームワーク</rt><rp>) </rp></ruby>][`custom_test_frameworks`]機能を使えば標準のテストフレームワークを置き換えることができます。この機能には外部ライブラリは必要なく、したがって`#[no_std]`環境でも動きます。これは、`#[test_case]`属性をつけられたすべての関数のリストを引数としてユーザの指定した実行関数を呼び出すことで働きます。こうすることで、(実行関数の)実装内容によってテストプロセスを最大限コントロールできるようにしているのです。
[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html
@@ -98,7 +98,7 @@ fn test_runner(tests: &[&dyn Fn()]) {
<div class = "warning">
**補足:** 現在、cargoには`cargo test`を実行すると、いくらかのケースにおいて "duplicate lang item" エラーになってしまうバグが存在します。これは、`Cargo.toml`内のプロファイルにおいて`panic = "abort"`を設定していたときに起こります。これを取り除けば`cargo test`はうまくくはずです。これについて、より詳しく知りたい場合は[cargoのissue](https://github.com/rust-lang/cargo/issues/7359)を読んでください。
**補足:** 現在、cargoには`cargo test`を実行すると、いくらかのケースにおいて "duplicate lang item" エラーになってしまうバグが存在します。これは、`Cargo.toml`内のプロファイルにおいて`panic = "abort"`を設定していたときに起こります。これを取り除けば`cargo test`はうまくくはずです。これについて、より詳しく知りたい場合は[cargoのissue](https://github.com/rust-lang/cargo/issues/7359)を読んでください。
</div>
@@ -122,7 +122,7 @@ pub extern "C" fn _start() -> ! {
テストフレームワークのエントリ関数の名前を`test_main`に設定し、私達の`_start`エントリポイントから呼び出しています。`test_main`関数は通常の実行時には生成されていないので、[条件付きコンパイル][conditional compilation]を用いて、テスト時にのみこの関数への呼び出しが追記されるようにしています。
`cargo test`を実行すると、 `test_runner`からの "Running 0 tests" というメッセージが画面に見えます。これで、テスト関数を作り始める準備ができました:
`cargo test`を実行すると、 `test_runner`からの "Running 0 tests" というメッセージが画面に表示されます。これで、テスト関数を作り始める準備ができました:
```rust
// in src/main.rs
@@ -141,16 +141,16 @@ fn trivial_assertion() {
今、`test_runner`関数に渡される`test`のスライスは、`trivial_assertion`関数への参照を保持しています。`trivial assertion... [ok]`という画面の出力から、テストが呼び出され成功したことがわかります。
テストを実行したあとは、`test_runner`から`test_main`関数へとリターンし、さらに`_start`エントリポイント関数へとリターンします。エントリポイント関数がリターンすることは認められていないので、`_start`の最後では無限ループに入ります。しかし、`cargo test`にはすべてのテストを実行し終わったあとには終了してほしいので、これは問題です。
テストを実行したあとは、`test_runner`から`test_main`関数へとリターンし、さらに`_start`エントリポイント関数へとリターンします。エントリポイント関数がリターンすることは認められていないので、`_start`の最後では無限ループに入ります。しかし、`cargo test`にはすべてのテストを実行し終わった後に終了してほしいので、これは問題です。
## QEMUを終了する
今の所、`_start`関数の最後で無限ループがあるので、`cargo test`を実行するたびにQEMUを手動で終了しないといけません。ユーザによる入力などのないスクリプトでも`cargo test`を実行したいので、これは不都合です。これに対する綺麗な解決法はOSをシャットダウンする適切な方法を実装することでしょう。しかし、これは[APM]か[ACPI]というパワーマネジメント標準規格へのサポートを実装する必要があるので、残念なことに比較的複雑です。
今の所、`_start`関数の最後で無限ループがあるので、`cargo test`を実行するたびにQEMUを手動で終了しないといけません。ユーザによる入力などのないスクリプトでも`cargo test`を実行したいので、これは不都合です。これに対する綺麗な解決法はOSをシャットダウンする適切な方法を実装することでしょう。これは[APM]か[ACPI]というパワーマネジメント標準規格へのサポートを実装する必要があるので、残念なことに比較的複雑です。
[APM]: https://wiki.osdev.org/APM
[ACPI]: https://wiki.osdev.org/ACPI
嬉しいことに、ある「脱出口」があるのです。QEMUは特殊な`isa-debug-exit`デバイスをサポートしており、これを使うとゲストシステムから簡単にQEMUを終了できます。これを有効化するためには、QEMUに`-device`引数を渡す必要があります。これは`Cargo.toml``package.metadata.bootimage.test-args`設定キーを追加することで行えます。
しかし嬉しいことに、ある「脱出口」があるのです。QEMUは特殊な`isa-debug-exit`デバイスをサポートしており、これを使うとゲストシステムから簡単にQEMUを終了できます。これを有効化するためには、QEMUに`-device`引数を渡す必要があります。これは`Cargo.toml``package.metadata.bootimage.test-args`設定キーを追加することで行えます。
```toml
# in Cargo.toml
@@ -220,7 +220,7 @@ pub fn exit_qemu(exit_code: QemuExitCode) {
終了ステータスを指定するために、`QemuExitCode`enumを作ります。成功したら成功`Success`)の終了コードで、そうでなければ失敗(`Failed`の終了コードで終了しようというわけです。enumは`#[repr(u32)]`をつけることで、それぞれのヴァリアントが`u32`の整数として表されるようにしています。終了コード`0x10`を成功に、`0x11`を失敗に使います。終了コードの実際の値は、QEMUの標準の終了コードと被ってしまわない限りはなんでも構いません。例えば、成功の終了コードに`0`を使うと、変換後`(0 << 1) | 1 = 1`になってしまい、これはQEMUが実行に失敗したときの標準終了コードなのでよくありません。QEMUのエラーとテスト実行の成功が区別できなくなります。
というわけで、`test_runner`を更新して、すべてのテストが実行されたあとでQEMUを終了するようにすることができますね:
というわけで、`test_runner`を更新して、すべてのテストが実行されたあとでQEMUを終了するようにできますね
```rust
fn test_runner(tests: &[&dyn Fn()]) {
@@ -263,7 +263,7 @@ test-success-exit-code = 33 # (0x10 << 1) | 1
この設定を使うと、`bootimage`は私達の出した成功の終了コードを、終了コード0へとマップするので、`cargo test`は正しく成功を認識し、テストを失敗したと見做さなくなります。
私達のテストランナーは、自動でQEMUを閉じ、結果を報告するようになりました。QEMUの画面が非常に短い時間開くのはまだ見えますが、短すぎて結果が読めません。QEMUが終了したあともテストの結果が見られるように、コンソールに出力できたら良さそうです。
これで私達のテストランナーは、自動でQEMUを閉じ、結果を報告するようになりました。しかし、QEMUの画面が非常に短い時間開くのは見えますが、短すぎて結果が読めません。QEMUが終了したあともテストの結果が見られるように、コンソールに出力できたら良さそうです。
## コンソールに出力する
@@ -316,7 +316,7 @@ lazy_static! {
}
```
[VGAテキストバッファ][vga lazy-static]のときのように、`lazy_static`とスピンロックを使って`static`なwriterインスタンスを作ります。`lazy_static`を使うことで、`init`メソッドが初めて使われたとき1回だけ呼び出されることを保証できます。
[VGAテキストバッファ][vga lazy-static]のときのように、`lazy_static`とスピンロックを使って`static`なwriterインスタンスを作ります。`lazy_static`を使うことで、`init`メソッドが初回使用時にのみ呼び出されることを保証できます。
`isa-debug-exit`デバイスのときと同じように、UARTはport I/Oを使ってプログラムされています。UARTはより複雑で、様々なデバイスレジスタ群をプログラムするために複数のI/Oポートを使います。unsafeな`SerialPort::new`関数はUARTの最初のI/Oポートを引数とします。この引数から、すべての必要なポートのアドレスを計算することができます。ポートアドレス`0x3F8`を渡していますが、これは最初のシリアルインターフェースの標準のポート番号です。
@@ -372,7 +372,7 @@ fn trivial_assertion() {
}
```
`#[macro_export]`属性を使っているため`serial_println`マクロはルート<ruby>名前空間<rp> (</rp><rt>namespace</rt><rp>) </rp></ruby>の直下にるので、`use crate::serial::serial_println`とインポートするとうまくかないということに注意してください。
`#[macro_export]`属性を使うことで`serial_println`マクロはルート<ruby>名前空間<rp> (</rp><rt>namespace</rt><rp>) </rp></ruby>の直下に置かれるので、`use crate::serial::serial_println`とインポートするとうまくかないということに注意してください。
### QEMUの引数
@@ -435,7 +435,7 @@ fn panic(info: &PanicInfo) -> ! {
}
```
テストパニックハンドラには`println`の代わりに`serial_println`を使い、そのあと失敗の終了コードでQEMUを終了します。コンパイラには、`exit_qemu`の呼び出しのあと`isa-debug-exit`デバイスがプログラムを終了させているということはわからないので、やはり最後に終了しない`loop`を入れないといけないということに注意してください。
テストパニックハンドラには`println`の代わりに`serial_println`を使い、そのあと失敗の終了コードでQEMUを終了します。コンパイラには、`exit_qemu`の呼び出しのあと`isa-debug-exit`デバイスがプログラムを終了させているということはわからないので、やはり最後に無限ループを入れないといけないことに注意してください。
これでQEMUはテストが失敗したときも終了し、コンソールに役に立つエラーメッセージを表示するようになります
@@ -482,12 +482,12 @@ test-args = [
- ブートローダーが私達のカーネルを読み込むのに失敗し、これによりシステムが延々と再起動し続ける。
- BIOS/UEFIファームウェアがブートローダーの読み込みに失敗し、同様に延々と再起動し続ける。
- 私達の関数のどれかの最後で、CPUが`loop{}`文に入ってしまう例えば、QEMU終了デバイスがうまく動かなかったなどの理由で
- CPU例外今後説明しますがうまく捕捉されなかったりして、ハードウェアがシステムリセットを行う。
- 私達の関数のどれかの最後で、CPUが`loop {}`文に入ってしまう例えば、QEMU終了デバイスがうまく動かなかったなどの理由で
- CPU例外今後説明しますがうまく捕捉されなかった場合などに、ハードウェアがシステムリセットを行う。
エンドレスループは非常に多くの状況で発生しうるので、`bootimage`はそれぞれのテスト実行ファイルに対し標準で5分のタイムアウトを設定しています。テストがこの時間内に終了しなかった場合は失敗と印をつけて、"Timed Out" エラーがコンソールに出力されます。この機能により、エンドレスループで詰まったテストが`cargo test`を永遠にブロックしてしまうことがないことが保証されます。
エンドレスループは非常に多くの状況で発生しうるので、`bootimage`はそれぞれのテスト実行ファイルに対し標準で5分のタイムアウトを設定しています。テストがこの時間内に終了しなかった場合は失敗したとみなされ、"Timed Out" エラーがコンソールに出力されます。この機能により、エンドレスループで詰まったテストが`cargo test`を永遠にブロックしてしまうことがないことが保証されます。
これを自分で試すこともできます。`trivial_assertion`テストに`loop {}`文を追加してください。`cargo test`を実行すると、5分後にテストがタイムアウトと印をつけられるのがわかるでしょう。タイムアウトまでの時間は`Cargo.toml``test-timeout`キーで[設定可能][bootimage config]です:
これを自分で試すこともできます。`trivial_assertion`テストに`loop {}`文を追加してください。`cargo test`を実行すると、5分後にテストがタイムアウトしたことが表示されるでしょう。タイムアウトまでの時間は`Cargo.toml``test-timeout`キーで[設定可能][bootimage config]です:
[bootimage config]: https://github.com/rust-osdev/bootimage#configuration
@@ -641,11 +641,11 @@ fn test_println_output() {
ですが、この記事の残りでは、 **結合テスト** を作って、異なる<ruby>構成要素<rp> (</rp><rt>コンポーネント</rt><rp>) </rp></ruby>の相互作用をテストする方法を説明しましょう。
## 結合テスト
Rustにおける[結合テスト][integration tests]は、慣習としてプロジェクトのルートにおいた`tests`ディレクトリ (つまり`src`ディレクトリと同じ階層ですね) にテストプログラムを入れます。標準のテストフレームワークも、独自のテストフレームワークも、自動的にこのディレクトリにあるすべてのテストを実行します。
Rustにおける[結合テスト][integration tests]は、慣習としてプロジェクトのルートにおいた`tests`ディレクトリ (つまり`src`ディレクトリと同じ階層ですね) にテストプログラムを入れます。標準のテストフレームワークも、独自のテストフレームワークも、自動的にこのディレクトリにあるすべてのテストを実行します。
[integration tests]: https://doc.rust-jp.rs/book-ja/ch11-03-test-organization.html#結合テスト
すべての結合テストは、独自の実行可能ファイルを持っており、私達の`main.rs`とは完全に独立しています。つまり、それぞれのテスト独自のエントリポイント関数を定義しないといけないということです。どのような仕組みになっているのかを詳しく見るために、`basic_boot`という名前で試しに結合テストを作ってみましょう:
すべての結合テストは、独自の実行可能ファイルを持っており、私達の`main.rs`とは完全に独立しています。つまり、それぞれのテスト独自のエントリポイント関数を定義しないといけないということです。どのような仕組みになっているのかを詳しく見るために、`basic_boot`という名前で試しに結合テストを作ってみましょう:
```rust
// in tests/basic_boot.rs
@@ -875,19 +875,19 @@ fn test_println() {
### 今後のテスト
結合テストの魅力は、これら完全に独立した実行ファイルとして扱われることです。これにより、実行環境を完全にコントロールすることができるので、コードがCPUやハードウェアデバイスと正しく相互作用していることをテストすることができるのです。
結合テストの魅力は、これら完全に独立した実行ファイルとして扱われることです。これにより、実行環境を完全にコントロールすることができるので、コードがCPUやハードウェアデバイスと正しく相互作用していることをテストすることができるのです。
`basic_boot`テストは結合テストの非常に簡単な例でした。今後、私達のカーネルはもっと機能豊富になり、様々な方法でハードウェアと相互作用するようになります。結合テストを追加することにより、それらの相互作用が期待通り動く(また、期待通り動きつづけている)ことを確かめることができるのです。今後追加できるテストの例としては、以下があります:
`basic_boot`テストは結合テストの非常に簡単な例でした。今後、私達のカーネルは機能がより豊富になり、そして様々な方法でハードウェアと相互作用するようになります。結合テストを追加することにより、それらの相互作用が期待通り動く(また、期待通り動きつづけている)ことを確かめることができるのです。今後追加できるテストの例としては、以下があります:
- **CPU<ruby>例外<rp> (</rp><rt>exception</rt><rp>) </rp></ruby>**: プログラムが不正な操作例えばゼロで割るなどを行った場合、CPUは例外を投げます訳注例外を発することを、英語でthrow an exceptionというのにちなんで、慣例的に「投げる」と表現します。カーネルはそのような例外に対するハンドラ関数を登録しておくことができます。結合テストで、CPU例外が起こったときに、例外ハンドラが呼ばれていることや、例外が解決可能だった場合に実行が継続することを確かめることができるでしょう。
- **ページテーブル**: ページテーブルは、どのメモリ領域が有効でアクセスできるかを定義しています。例えばプログラムを立ち上げるとき、このページテーブルを変更することで、新しいメモリ領域を割り当てることが可能です。結合テストで、ページテーブルに`_start`関数内で何らかの変更を施して、その変更が期待通りの効果を起こしているかを`#[test_case]`関数で確かめることができるでしょう。
- **ユーザー<ruby>空間<rp> (</rp><rt>スペース</rt><rp>) </rp></ruby>プログラム**: ユーザー空間プログラムは、システムの<ruby>資源<rp> (</rp><rt>リソース</rt><rp>) </rp></ruby>に限られたアクセスしか持たないプログラムのことです。これらは例えば、カーネルのデータ構造や、他のプログラムのメモリにアクセスすることはできません。結合テストで、禁止された操作を実行するようなユーザー空間プログラムを起動し、カーネルがそれらをすべて防ぐことを確かめることができるでしょう。
ご想像のとおり、もっと多くのテストが可能です。このようなテストを追加することで、カーネルに新しい機能を追加したときや、コードをリファクタリングしたときに、これらを壊してしまっていないことを保証することができます。これは、私達のカーネルがより大きく、より複雑になったときに特に重要になります。
ご想像のとおり、もっと多くのテストが可能です。このようなテストを追加することで、カーネルに新しい機能を追加したときや、コードをリファクタリングしたときに、これらを壊してしまっていないことを保証できます。これは、私達のカーネルがより大きく、より複雑になったときに特に重要になります。
### パニックしなければならないテスト
標準ライブラリのテストフレームワークは、[`#[should_panic]`属性][should_panic]をサポートしています。これを使うと、失敗しなければならないテストを作ることができます。これは、例えば、関数が無効な引数を渡されたときに失敗することを確かめたりするのに便利です。残念なことに、この機能は標準ライブラリのサポートを必要とするため、`#[no_std]`クレートではこの属性はサポートされていません。
標準ライブラリのテストフレームワークは、[`#[should_panic]`属性][should_panic]をサポートしています。これを使うと、失敗しなければならないテストを作ることができます。これは、例えば、関数が無効な引数を渡されたときに失敗することを確かめる場合などに便利です。残念なことに、この機能は標準ライブラリのサポートを必要とするため、`#[no_std]`クレートではこの属性はサポートされていません。
[should_panic]: https://doc.rust-jp.rs/rust-by-example-ja/testing/unit_testing.html#testing-panics
@@ -1019,7 +1019,7 @@ fn panic(_info: &PanicInfo) -> ! {
## まとめ
テストは、ある要素が望み通りの振る舞いをしていることを保証するのにとても便利なテクニックです。バグが存在しないことを証明することはできないとはいえ、バグを発見したり、特に手戻りを防ぐのに便利なツールであることは間違いありません。
テストは、ある要素が望み通りの振る舞いをしていることを保証するのにとても便利なテクニックです。バグが存在しないことを証明することはできないとはいえ、バグを発見したり、特にリグレッションを防ぐのに便利な方法であることは間違いありません。
この記事では、私達のRust製カーネルでテストフレームワークを組み立てる方法を説明しました。Rustの<ruby>独自<rp> (</rp><rt>カスタム</rt><rp>) </rp></ruby>テストフレームワーク機能を使って、私達のベアメタル環境における、シンプルな`#[test_case]`属性のサポートを実装しました。私達のテストランナーは、QEMUの`isa-debug-exit`デバイスを使うことで、QEMUをテスト実行後に終了し、テストステータスを報告することができます。エラーメッセージを、VGAバッファの代わりにコンソールに出力するために、シリアルポートの単純なドライバを作りました。
@@ -1029,4 +1029,4 @@ QEMU内で現実に近い環境で実行できるテストフレームワーク
## 次は?
次の記事では、**CPU例外**を見ていきます。この例外というのは、CPUによってなにか「不法行為」――例えば、ゼロ割りやマップされていないメモリページへのアクセス(いわゆる「ページフォルト」)――が行われたときに投げられます。これらの例外を捕捉して検分できるようにしておくことは、将来エラーをデバッグするときに非常に重要です。例外の処理はまた、キーボードをサポートするのに必要になる、ハードウェア割り込みの処理に非常に似てもいます。
次の記事では、**CPU例外**を見ていきます。この例外というのは、CPUによってなにか「不法行為」――例えば、ゼロ除算やマップされていないメモリページへのアクセス(いわゆる「ページフォルト」)――が行われたときに投げられます。これらの例外を捕捉してテストできるようにしておくことは、将来エラーをデバッグするときに非常に重要です。例外の処理はまた、キーボードをサポートするのに必要になる、ハードウェア割り込みの処理に非常に似てもいます。