Upgrade to Pro — share decks privately, control downloads, hide ads and more …

マイコンでもRustのtestがしたい/KernelVM Kansai 11

マイコンでもRustのtestがしたい/KernelVM Kansai 11

embedded-test crateを使うとマイコンターゲット(no_std)向けのRustでもテストができるよという話です。
内部的な実装の話やボツ案は資料限定です。

Avatar for Toshifumi NISHINAGA

Toshifumi NISHINAGA

May 11, 2025
Tweet

More Decks by Toshifumi NISHINAGA

Other Decks in Programming

Transcript

  1. テストとは? • プログラムが想定した動作をしているか確かめること • テストを積み上げることで…… • テストした部分の動作を保証できる • 開発中のデグレを検知し、手戻りを防げる •

    その他嬉しいこと多数 • テストを先に書いて開発するテスト駆動開発(TDD)などもある • 現代的なプログラミングはテストは必須かつhigh priority • 適度かつ適切なテストを作ればtotalの開発コストは減る • ……と個人的に思っている 2025/05/11 5
  2. test 1 (with test library) test runner 現代的なテスト TEST_CASE(should_err) {

    should_err(err()) } ... run result run result 人間 CI (1) 実行 (2) 各種テスト手順の実施 (3) 実施結果の集計 (4) 結果確認 test N (with test library) TEST_CASE(should_ok) { should_ok(ok()) } fn pass() { return Ok } fn err() { return Err; } テスト対象 コード ※ 結果の活用等は省略 2025/05/11 10
  3. 現代的なテスト • テストフレームワークを使ってテストの保守性・生産性をUP • テストをtest caseごとにわけて登録 • 全test caseをrunner/frameworkが実行・成否判定・集計 •

    失敗時はどこで失敗したかを報告 • 人間は成功・失敗を見るだけにする • CIとの相性も良い • テスト結果活用の仕組み導入もセット • slackで通知したり、githubのPR blockerにしたり • 今回は省略 2025/05/11 11
  4. 現代的なテストに必要なもの(C/C++) • テストランナー • この資料では「複数のテストを実施して結果を集計するもの」 • 例: ctestなど • テストライブラリ

    • プログラム内でテストを簡単に行うためのヘルパーを提供 • assert関数、testcase記述機能、複数testcase実施機能等、mock作成支援 • 例: • google test(https://github.com/google/googletest) • catch2(https://github.com/catchorg/Catch2) • Unity(https://github.com/ThrowTheSwitch/Unity) 2025/05/11 12
  5. std Rustのテスト • unit test • ソースコード内に testコードを追記 するだけ •

    integration test • tests/*.rs にテスト を書く • doc test • コードのドキュメ ント内にテストが かける(!?) • 今回は省略 参考: https://doc.rust-lang.org/rust-by- example/testing/integration_testing.html unit test integration test 2025/05/11 14 コード: https://github.com/tnishinaga/20250511_kernelvm_kansai_sample/tree/main/no01_std_tests
  6. rustのテストの実現方法 • 以下を行ってテスト用バイナリを生成し、実行している • cargoがrustcにtest用オプション(--test)を渡す(※) • rustcがテスト用マクロやハーネス等を展開しつつビルド • cfg(test)の有効化 •

    マクロ展開 • test用のハーネスを生成 • cargo testはtestの実行と集計を担当している(= runner) • っぽい(読み切れていない) • 詳細は公式のドキュメントを確認 • https://github.com/rust- lang/rust/blob/7295b08a17d1107155acd4b552069e3705b0ab1f/src/doc/rustc- dev-guide/src/test-implementation.md ※: cargo build --tests --lib -vvv するとコンパイルオプションが取れる 2025/05/11 17
  7. --testオプションが渡されたrustcの挙動 • --test optionの受取 • rustc_session::options::Options型に格納 • https://github.com/rust- lang/rust/blob/651e9cf327358b28db7e37a2ae61727f4a2ef232/compiler/rustc_session/src/config.rs#L2669 •

    https://github.com/rust- lang/rust/blob/651e9cf327358b28db7e37a2ae61727f4a2ef232/compiler/rustc_session/src/config.rs#L2746 • 以降、rustc_session::session::is_test_crate関数で確認 • cfg(test) の有効化 • https://github.com/rust-lang/rust/blob/master/compiler/rustc_session/src/config/cfg.rs#L295-L298 • test harnessなどの生成 • harness: テスト専用main関数 • Cargo.tomlのharnessオプションで消せもする • 参考 • https://github.com/rust-lang/rust/blob/master/compiler/rustc_builtin_macros/src/test_harness.rs#L66-L90 • https://doc.rust-lang.org/test/fn.test_main_static.html • https://rust-exercises.com/advanced-testing/09_test_harness/00_intro#test-harness • proc macroの展開 • #[test] の展開 • (たぶん) ふつーのマクロ展開と同じ ※cargo build --tests --lib -vvv するとコンパイルオプションがわかる 2025/05/11 19
  8. rustのテストの実現方法 • 以下を行ってテスト用バイナリを生成し、実行している • cargoがrustcにtest用オプション(--test)を渡す(※) • rustcがテスト用マクロやハーネス等を展開しつつビルド • cfg(test)の有効化 •

    マクロ展開 • test用のハーネスを生成 • cargo testはtestの実行と集計を担当している(=runner) • っぽい(読み切れていない) • 詳細は公式のドキュメントを確認 • https://github.com/rust- lang/rust/blob/7295b08a17d1107155acd4b552069e3705b0ab1f/src/doc/rustc- dev-guide/src/test-implementation.md ※: cargo build --tests --lib -vvv するとコンパイルオプションが取れる 2025/05/11 20
  9. これまでのまとめ • 現代的なテストの仕組みには以下が必要 • テストランナー • テストライブラリ・フレームワーク • Rustはテストランナー・ライブラリを持っている •

    テストランナー • cargo test • ライブラリ・フレームワーク • rustcの生成するharness • libtest等 • Rustのテストはコンパイラが実行バイナリを生成している • マクロ展開 • ハーネス追加 2025/05/11 21
  10. • テスト方法を大雑把に以下の4つに分類 • host環境(64bit std)テスト • テスト対象がアーキテクチャ・ポインタ幅・ペ リフェラル等に依存しない • 64bit

    std環境(host環境)でテスト可 • 32bit std環境テスト • 対象がポインタ幅にのみ依存する場合 • 32bit std環境(arm32 linux, i386 linux等)でテスト可 • 実機でもqemu上でも可 • qemu test • ポインタ幅・アーキテクチャに依存 • アーキテクチャを合わせたqemu上でテスト可 • 実機テスト • 実機(アーキテクチャ・ポインタ幅・ペリフェラ ル等)に依存 • 実機上でしかテストできない no_std testの整理 2025/05/11 25 自由度 高 自由度 低 実行時間 短い 忠実性 低 優先度 低 実行時間 長 忠実性 高 優先度 高 実機 host (std) 32bit std qemu
  11. 実機テスト • testが実行できない原因を取り除いて実機でテストする • 実機 + debugger + test用の何らかの仕組みを用いる •

    pros • 忠実性が高い • 「実機で動くこと」が最優先事項よ! • コードに手を加える部分が少ない • featureでの切り替え等がいらない • cons • 実行時間が(比較的)長い • Raspberry Pi Picoで実行すると書き込みだけで約2秒程度かかる • 故障リスクがある • FLASHの書き換え上限・謎のcrashなどなど • CI上での実施に難あり • ハードウェアリソースが乏しい • 難しいテスト・大量のテストは実施できない 2025/05/11 26 DebuggerとRasPi Pico
  12. 実機テスト用の仕組み・ライブラリ • custom test frmeworks を使う • https://os.phil-opp.com/testing/ • https://doc.rust-lang.org/beta/unstable-book/language-features/custom-

    test-frameworks.html • embedded-test + probe-rs を使う ←オススメ • https://crates.io/crates/embedded-test 2025/05/11 27
  13. custom test frameworksを使う方法 • 通常rustcが用意する test harnessを全部自前で用意する方法 • custom test

    frameworks(unstable feature)を使うとharnessを自作できる • https://doc.rust-lang.org/beta/unstable-book/language-features/custom-test- frameworks.html • 作り込み次第ではあるが、動作確認+α程度の仕組みはできる • やり方は「Writing an OS in Rust」に詳しく書かれている • https://os.phil-opp.com/testing/ • hikaliumさんのOS自作本にも書かれているらしい(まだ読んでない) • [作って学ぶ]OSのしくみⅠ, hikalium著, 技術評論社 2025/05/11 28 https://github.com/tnishinaga/20250511_kernelvm_kansai_sample/tree/main/no03_nostd_custom_test_frameworks
  14. custom test frameworksを使ったテスト方法 1. lib.rsに以下を追記 1. #![no_main] 2. #![feature(custom_test_frameworks)] 1.

    custom_test_frameworksを有効化 3. #![test_runner(test_runner)] 1. test runnerとしてtest_runner関数を指定 4. #![reexport_test_harness_main = "test_main"] 1. harnessとして追加する関数名をmainか らtest_mainに変更 2. test_runner関数を追記 1. 関数ポインタを受け取って実行する 2. 右図参照 2025/05/11 31 test runner
  15. custom test frameworksを使ったテスト方法 1. mod testsにtest_caseを登 録 1. #[test] を

    #[test_case]に置 き換えるだけ 2. mod testsにentry関数を追 加 1. 通常のマイコンエントリ ポイントからtest_mainを 呼ぶ 1. 必要なら先にsetupをする 2. test_mainがtest_runnerを呼 び、テストが実行される 2025/05/11 32
  16. custom test frameworksのpros/cons • pros • primitiveな方法なので潰しが効く • アーキテクチャ依存等も特になさそう •

    cons • 実装コストが高め • 全部自分で作る必要があってだるい • できないことも多い • テストの集計ができない • どのテストが成功・失敗したかわからん • should_panic等のテストもできない • panicしたらそこで終了 • nightlyが必要(=stableが使えない) • 業務利用はきびしそう 2025/05/11 33 わざとpanicさせた場合の出力 panic_probeでログ出力すればどこで落ち たかぐらいはわかる
  17. embedded-test + probe-rsを使う方法 • macroを1行足すだけで、cargo testが マイコンでも実行できるようになる crate • 必要なもの・こと

    • マイコンのdebugger • probe-rsのサポート • semihostingに対応していること 2025/05/11 34 https://github.com/tnishinaga/20250511_kernelvm_kansai_sample/tree/main/no04_nostd_embedded_test embedded-test使用時のlib.rs
  18. embedded-testの仕組み • crateとprobe-rsを使って実現 • embedded-test • マイコン上でテストを実施し、cargo test用の出力を出す • cargo

    testが解釈できるjsonを出してるっぽい(詳細未確認) • probe-rs • マイコンにテスト用firmwareをロードする • マイコン・cargo test間の通信を仲介する 2025/05/11 37 マイコン probe-rs Cargo test semihosting stdio
  19. embedded-testの使い方 • 依存crateをCargo.tomlに足す • [dev-dependencies] embedded-test = { version =

    "0.6.0" } • harness生成を消す • [lib] harness = false • .cargo/config.tomlに追記 • rustflagsにemmbedded-test.xを足す • "-Clink-arg=-Tembedded-test.x", • runnerにprobe-rsを指定する • runner = "probe-rs run --chip RP2040 --protocol swd" • lib.rsの #[cfg(test)]の下に1行追加 • #[embedded_test::tests()] 2025/05/11 38 embedded-test利用時のlib.rs
  20. embedded-testのpros/cons • pros (custom test frameworksに比べて) • 手順が少なくて簡単 • nightly不要

    • should_panicやタイムアウト等も対応可 • cons • semihostingに依存 • arm, riscv, x86_64(qemu)では動きそう。その他は無理かも。 • debuggerの繋げない環境では動かなさそう • シリアルで書き込むターゲット(debugger less pico)でも動いたら嬉しいが…… • ライブラリに手を入れればsemihosting依存は切れるかも? • 次ページ 2025/05/11 39
  21. embedded-testのsemihosting依存を切る案 • 原理的にはcargo testとrunnerから起動したプログラムで双方向通信 ができれば動くはず • 案1: シリアル経由で動かす • 以下のrunnerプログラムを作る

    • シリアル経由でfirmwareをマイコンに書き込む • シリアル経由でマイコンと通信し、cargo testとの双方向通信を仲介する • library側に手をいれる • semihosting使ってる部分をtraitにして、semihostingをoptionalにする 2025/05/11 40 マイコン Runner Cargo test serial stdio
  22. • テスト方法を大雑把に以下の4つに分類 • host環境(64bit std)テスト • テスト対象がアーキテクチャ・ポインタ幅・ペ リフェラル等に依存しない • 64bit

    std環境(host環境)でテスト可 • 32bit std環境テスト • 対象がポインタ幅にのみ依存する場合 • 32bit std環境(arm32 linux, i386 linux等)でテスト可 • 実機でもqemu上でも可 • qemu test • ポインタ幅・アーキテクチャに依存 • アーキテクチャを合わせたqemu上でテスト可 • 実機テスト • 実機(アーキテクチャ・ポインタ幅・ペリフェラ ル等)に依存 • 実機上でしかテストできない no_std testの整理 2025/05/11 41 自由度 高 自由度 低 実行時間 短い 忠実性 低 優先度 低 実行時間 長 忠実性 高 優先度 高 実機 host (std) 32bit std qemu • テスト方法を大雑把に以下の4つに分類 • host環境(64bit std)テスト • テスト対象がアーキテクチャ・ポインタ幅・ペ リフェラル等に依存しない • 64bit std環境(host環境)でテスト可 • 32bit std環境テスト • 対象がポインタ幅にのみ依存する場合 • 32bit std環境(arm32 linux, i386 linux等)でテスト可 • 実機でもqemu上でも可 • qemu test • ポインタ幅・アーキテクチャに依存 • アーキテクチャを合わせたqemu上でテスト可 • 実機テスト • 実機(アーキテクチャ・ポインタ幅・ペリフェラ ル等)に依存 • 実機上でしかテストできない
  23. qemu + embedded-test • ターゲットとなるべく同じqemuマシン上でテストする • 今回はPicoとおなじcortex-m0のmicrobitのmachineを対象にテスト • pros •

    (実機よりは)高速 • 故障等の可能性もない • 命令セット・アーキテクチャ依存までならテスト可 • cons • コードに手をいれる必要は少しある • qemu featureを作って実機依存を切り離す必要あり • 実機に比べると忠実性が下がる • メモリ・FLASHの制約は実機と変わらない • Cortex-M系はvirt machineが使えないため • (unit test専用のマイコン向けvirt machineほしい) 2025/05/11 42
  24. qemu + embededd-testのやり方 • Cargo.tomlを修正 • rp_picoとqemu featureを追加 • qemu

    feature有効時はmicrobit crate等を読み込む • libのharnessをfalseにする • [lib] harness = false • build.rsを修正してlinker scriptを切り替え • memory.xをout以下に生成 • .cargo/config.tomlを分離して、適切なファイルをcargoのオプション で指定 • runnerをpicoとqemuで切り替え • qemu用のrunner scriptを追加 • semihostingを有効化して起動 2025/05/11 43 細かい部分は以下を参照 https://github.com/tnishinaga/20250511_kernelvm_kansai_sample/tree/main/no05_nostd_embedded_test_qemu
  25. qemu+embededd-testの課題 • 複数のテストを実行できない • defmt-printが入力を受け付けないため • defmt-print: defmt encodeされたテキストをdecodeするプログラム •

    入力を受け付けるdefmt-print的な物があれば解決 • コスパは良くないかも • ライブラリの切り替えから何から結構めんどい • 実機を使い潰せるなら実機のほうが楽そう • 実機と同じmachineがqemuにあるなら便利かも? 2025/05/11 45