Open30

Dartのテストランナーの仕組み調べる

菊池紘菊池紘

https://pub.dev/packages/test

テストランナーがどう動いているのか追いかけてみる。

テストコードの中身って main() で始まる普通の実行可能プログラムじゃないですか。
なのにmainの終端にたどり着いてからtestで設定したテストケースが走り始めてますよね。よく考えたら不思議じゃないですか?
ので、どうなってるのか知りたい。

菊池紘菊池紘

https://github.com/dart-lang/test/blob/master/pkgs/test_core/lib/src/executable.dart#L37-L45
test_coremain を見ると _execute(args) なる関数を呼んでいる。
直下にある runTests というそれっぽい関数も同じ様になっているので、これが本体だろうとあたりをつけて掘り進む。

https://github.com/dart-lang/test/blob/master/pkgs/test_core/lib/src/executable.dart#L149-L150
_execute() を読むと、一般的なコマンドラインプログラムにありがちなオプションによる分岐があるので、これが本体で間違いなさそう。
バージョンを表示して終了、などすぐに終了しない系の箇所を追っていくと、 Runner なるものを作って終了コードを得ている箇所がある。これがテストの実行であると当たりをつける

菊池紘菊池紘

一旦 Runner に戻る
run() メソッドが本体なので、これを追う。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner.dart#L107-L111
コメントを見るとテストの成功失敗に関わらず値をreturnする、とある。
success に値を入れてreturnする流れになっているので、 succsess に代入をしている箇所を見つけて潜っていくことにする。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner.dart#L124-L137
_config.pauseAfterLoad 如何で分岐している。
falseの場合は results.last を最終的な結果としていて、 results.last にあたるのは _engine.run() の結果になることがわかる。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner.dart#L461-L474
_loadThenPause() も最終的に _engine.run() を読んでいる。
次は Engine.run() を掘る。

菊池紘菊池紘

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/engine.dart#L249-L256
suite内のtestを全部実行する、とのこと。これも Runner.run() と同様、真偽値で結果を必ず返すとのこと。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/engine.dart#L299
返すのはフィールドの success なので、どこかのメソッドでこれが変更されているっぽい。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/engine.dart#L262-L290
処理の本体っぽいのは _suiteController から作った subscriptiononData でやっている様子。
中でも _group.add()
多分この中のどれかの処理が success をいじっているはず。

菊池紘菊池紘

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/engine.dart#L89-L100
と思ったら succsess はgetterだった…
suiteがスキップされた場合と成功した場合にtrueを返すようになっていて、これは liveTests が持っている状態を取得して見ている。
その前に _group_runPool の終了を待ち受けているのも気になる。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/engine.dart#L143-L155
liveTestspassed skipped failed active をまとめたもの。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/engine.dart#L175-L177
active の中身は _activeQueueList だった。これに LiveTest を挿入している箇所があるんだろうか。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/engine.dart#L359-L396
referenceで検索してみると _runLiveTest が引っかかった。
これを見ると liveTestrun() メソッドが生えていて、これがテストを実行しているらしい。
実行の仕組みはここを掘っていけば良さそう。

菊池紘菊池紘

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/live_test.dart#L13-L25
LiveTesttest_api パッケージに入ってた。
これ自体はabstructなので、実装クラスが何種類かありそう。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/live_test.dart#L26-L27
https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/live_test.dart#L120-L128
多分 suiterun を見るとどう実行しているかがわかりそう。
コードジャンプで実装クラスを辿れなかったので一旦LiveTestはスキップ。

菊池紘菊池紘

Engineに戻って、 _runLiveTest がどこから呼ばれているのか見る。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/engine.dart#L283
検索で辿っていくと、 _runGroup メソッドに突き当たり、それが Engine.run() から呼ばれている経路があるのがわかる。

とすると、テストケースの実行は以下のコールスタックで実現されていそう。

  1. testmain()
  2. test_coremain()
  3. _execute()
  4. Runner.run()
  5. Engine.run()
  6. Engine._runGroup()
  7. Engine._runLiveTest()
  8. LiveTest.run()

実際に実行されているところを見つけるには、 LiveTest の実装を調べれば良さそうだ。
あと、LiveTestのインスタンスを作っている箇所を見つけること。

菊池紘菊池紘

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/engine.dart#L270-L283
Engine.run() から _runGroup() を呼んでいる箇所をよく見ると、 LiveSuiteController という、どこかで見たことのあるものと名前が似ているものが出てくる。そう、LiveTestと何か関連があるんじゃなかろうか。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/live_suite_controller.dart#L51-L59
どうやら LiveSuite を動かすものらしい。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/engine.dart#L316-L318
再び _runGroup 。liveTestを生成している load() メソッドがある。
load()Test クラスのメソッドだった。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/test.dart#L14-L19
immutableなのだそう。多分、Testがテストケースの定義であり、LiveTestが実行状態を持つものなんだろうな、という推測。
あと load() メソッドで LiveTest を作ってくれるとも書いてあるので、これの実装クラスを調べる必要がありそう。

菊池紘菊池紘

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/runner_test.dart#L21-L22
実装クラスは RunnerTest らしい。コードジャンプですぐ出てきた。LiveTestはコードジャンプできないの何故だ。
遠隔実行されるテストらしい。どういうこっちゃ。ブラウザでもテスト実行できるそうなので、多分DartVM以外の実行環境でも動かせるようになってるんだろうな。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/runner_test.dart#L35-L101
load() の実装を見ると LiveTestController というものを作ってそのままreturnしている。そしてreturn型は LiveTest 型なので、 LiveTestControllerLiveTest 実装クラスだと想像がつく。

菊池紘菊池紘

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/live_test_controller.dart#L17-L22
ビンゴ。 LiveTest を継承しているので、このクラスを調べたらどう実行しているのかの手がかりが掴めそう。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/live_test_controller.dart#L143-L155
フィールドの _onRun をそのまま実行している。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/live_test_controller.dart#L85-L103
コンストラクタに書いてあったわ…
LiveTestController_onRun には、先程の RunnerTest.load() だとラムダ式が渡されている。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/runner_test.dart#L42-L45
基本的には testChannel というのにコマンドを送って、そのままStreamで落ちてくるイベントを待っているだけの様子。
FlutterのMethodChannelみたいなもんか。その向こう側の実装をコードジャンプで追うのは難しそうだ。
もしかして他の Test 実装クラスがあるんじゃないか?

菊池紘菊池紘

ちょっと寄り道。
test_api のbackendディレクトリ内に Suite というクラスがあった。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/suite.dart#L10-L14
Suiteはtestのsetだとのこと。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/suite.dart#L26-L27
メンバにはGroupもある。各Suiteはtop levelに必ず一つのGroupを持っている、ということになりそう。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/group.dart#L12-L15
この Group はtestをグループ化するものと書かれている。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/group_entry.dart#L11-L12
基底クラスの GroupEntryGroupTest のいずれかを表現。

構造的には Suite --(1..1)--> Group --(1..n)--> GroupEntry という形になるんだろうか。

菊池紘菊池紘

Suite のコンストラクタがどこから呼ばれているのかを調べて見たら何かわかるのではあるまいか。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/hooks_testing.dart#L119-L124
呼ばれている箇所のうちの一つは hooks_testing.dart_createTest() というそれっぽい関数名の関数。
引数 bodyLocalTest に突っ込んで test.load()LiveTest を作っている。
LocalTest も気になるけど、この _createTest() がどこで呼ばれているのかまず探ってみる。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/hooks_testing.dart#L32-L33
TestCaseMonitor のコンストラクタで呼ばれている。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/hooks_testing.dart#L20-L29
TestCaseMonitor はテストケースのbodyを取ってその実行状況を監視するためのもの。
staticメソッドとして持っている run または start でインスタンスを作っている。
このどちらかが呼ばれている箇所がテストケース実行の仕組みに迫れそう。
run から見ていくことにする。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/hooks_testing.dart#L35-L56
run は結局 start を呼んで終了を待っているだけっぽい。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/hooks_testing.dart#L58-L84
start_liveTest を走らせ始める。
コードジャンプではどこで呼んでいるかわからなかった。

菊池紘菊池紘

埒が明かないので、テストコードから呼ぶ test() 関数から追ってみることにする。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test/lib/test.dart#L5-L15
test()test パッケージに入っている…ように見えるが、 test_coretest() をそのままexportしただけだったりする。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/scaffolding.dart#L78-L154
テスト時に実行して欲しいロジックは body として渡される。
body_declarer.test() に渡されているので、このメソッドを追っていく。
_declarer がどこから来ているのかも気になるが、一旦はメソッドの方を見ることにする。
declarerってなんて読めば良いんだ?ディクレアラー?

菊池紘菊池紘

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/declarer.dart#L171-L225
test() メソッドは Declarer クラスのメソッドだった。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/declarer.dart#L17-L24
その名の通り、テストケースというかSuiteの宣言構造を表現するためのものらしい。
現在のzoneに紐づいたDeclarerは current というstaticなgetterで取得できるらしく、どうやらこれが _declarer であるみたいだ。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/declarer.dart#L106-L107
Zone は昔Crashlyticsのプラグインの中身読んでたときに見た気がするな。キャッチされていない例外を捕まえるために使っていたのが珍しかったのでよく覚えている。
よく知らないので後で改めてよく調べておこう。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/declarer.dart#L312-L336
Declarer の中身を流し読みしていたら build() というメソッドを見つけた。 Group を一つだけ返すメソッドのようだ。
確か Suite が一つだけtop levelの Group を保持する構造になっていたから、これの戻り値が Suite に入るんじゃないのか?

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/remote_listener.dart#L122-L127
ビンゴ。 test_api パッケージの RemoteListener の staticメソッド start() の中で Suite の引数に渡されていた。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/remote_listener.dart#L30-L50
start() のドキュメントコメントとシグネチャを見てみると、 getMain なる、Functionを返すFunctionを引数に取っていることがわかる。
これがテスト用Dartファイルのmainを取得しているんじゃないのか?

菊池紘菊池紘

getMain がどこから来るのかを追ってみる。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/plugin/remote_platform_helpers.dart#L9-L33
RemoteListener.start()test_core パッケージの serializeSuite() 関数から呼ばれている。
こいつも引数から getMain を受け取っている。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/bootstrap/vm.dart#L15-L20
https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/bootstrap/vm.dart#L30-L41
serializeSuite()internalBootstrapVmTest()internalBootstrapNativeTest() から呼ばれていて、どちらも引数から getMain を取っている。
通常のDartのテスト実行はDart VMで行うことが多いように思うので、一旦 internalBootstrapVmTest() の呼び出し元を追いかけることにする。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/vm/platform.dart#L328-L339
コードジャンプでは呼ばれている箇所がなかったが、通常の文字列検索でヒットした。
_bootstrapIsolateTestContents() はDartのソースコードを文字列で生成する関数。
テスト対象のファイルをimportで読み取って、このmain関数を返すようにしたラムダ式が getMain の正体だった様子。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/vm/test_compiler.dart#L88-L100
もう一箇所同じような _TestCompilerForLanguageVersion._generateEntrypoint() があったがほぼ同じソースコードを文字列で生成している。

どちらを追うのが正解かわからないが、一旦 _bootstrapIsolateTestContents() を追いかけて、 dart test コマンドからどうやってここに至るのかわかるまで追いかけたい。

菊池紘菊池紘

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/vm/platform.dart#L293-L308
_bootstrapIsolateTestContents()VMPlatform._bootstrapIsolateTestFile() から呼ばれている。
テスト実行用の一時ファイルを作成してそのUriを返している。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/vm/platform.dart#L206-L228
VMPlatform._spawnIsolate()compilerCompiler.source だったときに VMPlatform._bootstrapIsolateTestFile() を呼んでいたようだ。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/vm/platform.dart#L45-L76
load() メソッドから呼ばれていた。引数の path がテストファイルのパスのようだ。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/platform.dart#L25-L38
load()PlatformPlugin が基底クラスとして持っているメソッド。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/loader.dart#L65-L68
VMPlatform のコンストラクタが呼ばれる箇所を探したら、 Loader のコンストラクタに出てきた。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner.dart#L40-L45
Loader のコンストラクタが呼ばれるのは… Runner の初期化時だ!!!

菊池紘菊池紘

これで dart test コマンドからテストファイルがどう実行されるかの流れは見当がついた。
一応ファイルパスが渡されるはずの VMPlatform.load() がどこから呼ばれるか追いかけておく。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/loader.dart#L151-L158
コードジャンプで探したところ、 Loader.loadFile() から呼ばれていることがわかった。引数の path がそのまま VMPlatform.load() に渡されている。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/loader.dart#L132-L149
Loader.loadFile() が呼ばれている箇所が2箇所あったが、一箇所は Loader.loadDir() だった。
ディレクトリの中身をpathとして渡しているのがわかる。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner.dart#L258-L270
Loader.loadFile()Loader.loadDir() も、どちらも Runner._loadSuites() で使用されていた。
_config.testSelections.entries がテストファイルのパスと Set<TestSelection> のMapになっていて、key側のファイルパスがpathとして渡されている。これで疑問は大体解決した。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner.dart#L107-L118
一応もっと掘り下げて(浮上して?)いくと Runner._loadSuites()Runner.run() で呼ばれている。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/executable.dart#L148-L150
Runner.run() は以前にも見た _execute() から呼ばれている。これでコールスタックは大体わかった!

菊池紘菊池紘

ざっくりまとめると、

  • 我々(test系パッケージの)ユーザーが書いてるテストコードは、テスト実行用のdartファイルにimportされており、mainは関数として実行される(一般的なエントリーポイントとして使われているわけではない)
  • その中で実行された test() 関数によって、Zoneごとにテストの実行計画?が作られる

ということらしい。
更に掘り下げるならば、テスト実行用のdartファイルがどのように実行されるのかと、Zoneの概念だろうか。

菊池紘菊池紘

https://dart.dev/articles/archive/zones#storing-zone-local-values
使われていた Zone.current[#key] 的なやつはzone local valueというやつっぽい。
Javaのローカルスレッドっぽい。

まだ推測だけど、テスト用のソースコードが例外吐いても中止せずに処理を続行したいのでテスト用のソースコードごとにZoneを作っていたりするんだろう。その単位でZone作っていて、Declarerも作ってるとしたら納得。

手がかりになりそうなのは #test.declarer なるシンボル。
これが zoneValues でセットされている箇所が4箇所。
全部 Declarer のメソッドで、thisを #test.declarer に対応するインスタンスとして渡している。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/declarer.dart#L165-L169
1つ目は declare() というそのまんまなメソッド。自身を参照できるようにしたZoneでbodyの関数を実行するだけ。
これは RemoteListener.start() で呼ばれている。
https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/remote_listener.dart#L120
ご丁寧に引数が main だ。
https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/remote_listener.dart#L68-L70
このmainはユーザー定義のテストコードの main関数の様子。
ここから、このZoneはテストケース定義収集の間だけ使われていて、テストケース実行時は別のZoneっぽいことがわかる。

declare()group() からも呼ばれており、これは子グループ用に新しく作ったDeclarerインスタンスを渡すために使われていた。

残りの #test.declarer でzoneValue作ってる箇所は setUpAll や tearDownAll に対応した部分、test関数に対応した部分だった。各テストのコールバックも共通のDeclarerが使えるようになっている様子。
https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/declarer.dart#L213-L220

菊池紘菊池紘

あと調べるとしたらテスト実行用に作られた一時ファイルがどう実行されるか、か。
VMPlatform._spawnIsolate() から辿ってみる。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/vm/platform.dart#L223-L228
一時ファイルの生成は _bootstrapIsolateTestFile() でやっていて、その File インスタンスが _spawnIsolateWithUri() に渡されているのがわかる。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/vm/platform.dart#L249-L253
Isolate.spawnUri() でIsolateを起動している。
Isolateはいわゆるスレッドのようなもので、Dartの場合、Isolate単位でイベントループが管理されている…んじゃなかったっけ…?復習する必要がある。

菊池紘菊池紘

https://dart.dev/language/concurrency#isolates

https://www.cresc.co.jp/tech/java/Google_Dart2/language/isolates/isolates.html

Erlangのアクターモデルみたいなもの、と言われるとすんなり理解できる。
昔々にElxilrを触ろうとしてErlangをやったことが理解の助けになろうとは。

とりあえず指定したファイルを別Isolateで実行するメソッドがある。
https://api.flutter.dev/flutter/dart-isolate/Isolate/spawnUri.html
ドキュメントによれば、これがmainを実行するので、この機能を使ってテスト実行用一時ファイルを呼び出している、と。

菊池紘菊池紘

internalBootstrapVmTest() の中を見ると、 MultiChannel とか IsolateChannel とかのクラスが出てくる。
おそらくこれがテストランナー本体と、実行中のテストファイルの間の通信を取り持っているのだけれど、どんなものなのかわからないので調べておく。

https://pub.dev/packages/stream_channel
どちらもこのパッケージのクラス。Readmeに test パッケージの話が書かれている。
ブラウザでも動かすため、VMでのIsolateの動作とブラウザでの動作を抽象化して取り扱うため、どちらもStreamChannelとして取り扱えるようにするために使っている様子。

https://pub.dev/documentation/stream_channel/latest/stream_channel/StreamChannel-class.html
StreamChannel は送受信が可能なStream…ということらしい。
StreamとStreamShinkの両方の機能を持っているものといえば StreamController が思いつくけれども、これじゃ駄目なの?
まあabstract classなので様子見。

https://pub.dev/documentation/stream_channel/latest/stream_channel/MultiChannel-class.html
何らかの通信経路を使って、複数の仮想チャンネルを双方向につなぐものらしい。
virtualChannel() で仮想チャンネルを払い出し、チャンネルに対応するIDのチャンネルを相手方で作れば双方向通信ができると。
通信経路はStream/StreamShinkで抽象化できるものならなんでも良いっぽいので、これでSoketとReceivePort/SendPortの抽象化を実現しているっぽい。
なんとなくわかった

菊池紘菊池紘

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/vm/platform.dart#L72-L86
VMPlatform.load() の中に対応する MultiChannel を作っていると思しき箇所があった。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/vm/platform.dart#L94-L96
送られてきたIDを元に、Platform側でもvirtualChannelを発行している様子が見て取れる。

菊池紘菊池紘

Platformでは受け取ったSuiteをどうしているのか?

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/vm/platform.dart#L140-L142
先程の払い出されたvirtualChannelから落ちてきた値をdeserializeしてそうな deserializeSuite() 関数がある。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/plugin/platform_helpers.dart#L22-L46
deserializeすると RunnerSuiteController ができるらしい。

菊池紘菊池紘

コードを見ていると Test と名の付くクラスがたくさん出てくる。
一体誰がどんな役割をしているのだろう。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/test.dart#L14-L19
まずは名前の通りの Testtest_api にいる。
抽象クラスであり、メソッドは実装を持っていない。
一回使い切りのLiveバージョンのテストを払い出す load() というメソッドの定義がある。
https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/test.dart#L29-L35

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/live_test.dart#L13-L25
LiveTesttest_api にいる。
これは実行状態だとかを持ったテスト1件あたりを表現するクラスらしい。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/live_test_controller.dart#L17-L22
LiveTest を実装しているのが LiveTestController 。これも test_api のもの。
同じライフサイクルで LiveTest が動くのを保証するらしい。
こいつは Invoker のコンストラクタで生成されていて、 Invoker._onRun()LiveTestController.onRun() で呼び出される様になっている。
で、 Invoker._onRun() の中にテストのbody関数を呼び出すコードが有る。
https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/invoker.dart#L377-L394
この _testLocalTest という型らしい。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/invoker.dart#L23-L24
LocalTesttest_api 所属。
このIsolateに置けるTestとのことで、こいつはテスコードのの実装であるbody関数をメンバに持っている。
https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/invoker.dart#L37-L38

話は戻って Invoker._onRun() 、それを呼び出す LiveTestController.onRun() 、更にそれを呼び出す箇所を辿っていくと RemoteListener._runLiveTest() に出てくる。
https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/remote_listener.dart#L238-L239
更に呼び出し元をたどると RemoteListener._serializeTest() の中で testChannel にセットされたstreamのlistenerの中で、'run' なるcommandが来た時に呼ばれているのがわかる。
https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_api/lib/src/backend/remote_listener.dart#L209-L222
つまり、テストランナーはテストファイルの数だけ通信できるIsolateを立ち上げ、立ち上げた先のIsolate内でテストを実行させて結果を報告させている、ということみたい。

菊池紘菊池紘

test_api ばかりだったが、 test_core にも Test 継承クラスがある。

https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/runner_test.dart#L21-L22
RunnerTest は多分テストランナー本体内におけるテストケースに対応するもの、という意味でRunnerTestなんだろう。
load() メソッドの実装を見ると、対になっている RemoteListener'run' commandを送っているのがわかる。
https://github.com/dart-lang/test/blob/a5c4f010368d64563dd3d0b31bab5162d6a7caec/pkgs/test_core/lib/src/runner/runner_test.dart#L36-L43