コンピュテーション式の展開結果を可視化するツールComVuを作った
この記事はの13日目です。
今回は過去に作ったComVuというツールの話をします。
https://github.com/pocketberserker/ComVu
nugetでダウンロードできます。
これはなに
ComVuはコンピュテーション式を機械的に展開し、結果を表示するライブラリとツールの総称です。
名前の由来ですが、F#談話室終了後の食事の席で「こういうツールほしい」という話になった際、「こんぴゅてーしょんえくすぷれっしょんびじゅあらいざー…昆布だ!」という流れで決まりました。
ComVu.Coreが解析部分、ComVuがWPFによるツールとなっています。
外部ライブラリの解析
FSharp.Compiler.Serviceを利用してコンパイルや解析を行っているので、#I
や#r
を使えば外部ライブラリのコンピュテーション式も解析可能です。
sequence expressionsの疑似展開
sequence expressionsの実装はコンピュテーション式ではありませんが、にたような形式で作れるよという意味合いを込めて展開結果を表示できるようにしています。
問題点
- 一部のオペレータや関数名の表示が微妙
- 記号や関数名ではなくCompiledNameで表示されてもわかるでしょ、という気持ちがあったためさぼりました
- インデント深すぎ
- YC.PrettyPrinterを使ったらこんな感じに…外部からインデント幅を指定することが簡単にはできそうにないので、独自実装するしかないのかなぁ
- Windows以外では使えない
- ComVu.Coreを使えばあとはUIの問題だけなので、どなたか作ってください
余談
作ったは良いものの、私自身はツール無くてもどうにかなっているので拡張する気力があまり起きないんですよね…。 とはいえよさそうな提案があれば実装するつもりはあるので、issueお待ちしています。
2016年時点でF# 用のライブラリを.NET Core対応させるのは時期尚早だった?
この記事はF# Advent Calendar 2016の12日目の記事です。 また、.NET Core Advent Calendar 2016のの12日目の記事でもあります。
結論
先に個人的な結論を述べておきます。
現在のステータス
若干古めですが、GitHub上のvisualfsharpリポジトリのwikiに色々書いていあります。
https://github.com/Microsoft/visualfsharp/wiki/F%23-for-CoreCLR---Status
問題になる点
いくつかのライブラリで.NET Core対応を行った感想です。
- NuGetのFSharp.Coreに依存させたくない場合は過去と同じ要領でプロジェクトをわけるしかない
- project.jsonのせい(安らかに眠れ…)
- NuGetのやつに依存させたくない人は一定数存在する
- F# 2.0に対応させようとするとこの選択肢しかとれない
- Visual Studio 2015での開発の厳しさ
- ハイライト効かない(F# 対応していないので当たり前か)、nugetの挙動が怪しい
- IDE使わない場合はそんなに苦でもなかった
- FSharp.Compiler.Serviceが.NET Coreにまだ対応していない
- わりとネック
これに加えて
- project.jsonが負債と化すことが確定している
- TypeProviderはまだサポートしていない
- Paketもまだ対応できているとは言い難い(?)
- 9月頃の話。もしかしたら色々と変化しているかも。
といったことを考えると、そこまで急いで対応させる必要はないかと思います。
ただ、AWS LambdaでF#を使うみたいなことをしたい場合、ライブラリが必要なら対応させるしかないでしょうね。
おわりに
まぁ、基本はおとなしくツールが出揃うまで待ちましょう。
F# 4.1から一部のキーワードがunreserveされる話
この記事はF# Advent Calendar 2016の9日目の穴埋め用記事です。
本日はキーワードの話です。
幾つかのキーワードがidentifierとして利用可能になります。 理由は様々ですが(RFCに理由が書かれています)、キーワードとして使うことはないだろうと判断されたものが取り除かれた感じです。
個人的に面白いと思ったのはfunctor
ですね。
functor - If F# added parametereized modules, we would use "module M(args) = ..."
え、そんなこと考えているの、という気持ちになりました。
F# 4.1からCallerLineNumber, CallerFileName, CallerMemberNameが機能するようになる話
この記事はF# Advent Calendar 2016の7日目の穴埋め用記事です。
今回は標題にあげたCallerLineNumber, CallerFileName, CallerMemberNameの話。
C# 5.0から導入されたCaller Information機能がF# 4.1から使えるようになります。
- 省略可能なパラメータに属性を付ける
- デフォルト値を定義したいならdefaultArg関数を使いましょう
- 関数は対象外
- Optional Parametersはメソッドにしか定義できないので仕方ない
何が嬉しいかって、これのためだけにFSharp.Compiler.Serviceに依存しなくても良くなることですね。
個人的な利用用途の一つにPersimmonでの情報量増加があります。 assertionに失敗した時にファイル名、行番号が簡単にだせるので嬉しいですね*1。
*1:F# 4.1しか表示できない問題は残る
F# 4.1のResult型
この記事はF# Advent Calendar 2016の6日目のものです。 穴埋め用記事です。
今回はF# 4.1から標準となるResult typeの話です。
ケース名をどうするかみたいな話がありますが、最終的には以下の形に落ち着いたようです。
type Result<'T,'TError> = | Ok of 'T | Error of 'TError
また、Result
モジュールにはbind
、map
、mapError
が定義されています。
さて、標準から成功/失敗を表す型が提供されたのでめでたしめでたし…というわけにはいきません。
- ライブラリ側は古いF#ともおつきあいしなければならない関係上、独自のResultやChoiceが消えることは当分ないはず
- F# 4.1と全く同じ型、関数を提供するライブラリをつくって古いバージョンでも同じように書けるようにする手もあるが、うーん?
- アプリケーション側はある程度置き換わるはず…とはいえ関数が足りないのでオレオレライブラリが氾濫する可能性は残されている
まぁ、もうすぐ2016年は終了するわけですし、2017年はF# 4.0以下を窓から投げ捨てる覚悟をしても良いかもしれませんね?
FSharp.Control.ImperativeAsyncの紹介
この記事はF# Advent Calendar 2016の8日目のものです。 大遅刻しました…orz
今回はFsharp Bootcamp Tokyo 2016 with Tomas Petricekで議論した結果生まれたライブラリの紹介です。
https://github.com/pocketberserker/FSharp.Control.ImperativeAsync
Asyncでもreturn
で大域脱出できるようになるよ、という代物です。
このライブラリではImperativeAsync
という型を用意していますが、ユーザがこの型を明示的に生成することはできません。
あくまで制御用の型です。
中身はAsync<'T option>
をラップしただけのものですが、別名をつけておいた方がわかりやすいのでこうしました。
とまぁ、こんな感じで皆さんも気軽にコンピュテーション式を提供するだけのライブラリを作ってみてはいかがでしょうか?
.NET Core用のテストランナーを作る
[2017/02/18追記].NET Core SDK RC以降で実装方法が変わったのでこの記事を読むべきではありません
これは.NET Core Advent Calendar 4日目の記事です。
.NET Coreに対応したユニットテスト
さて、世の中にはすでに.NET Coreに対応済みのテスティングフレームワークがいくつか存在します。
ここで、.NET Core CLI用のランナーを提供しているプロジェクトをみてみましょう。
- dotnet-test-nunit
- dotnet-test-xunit
- dotnet-test-mstest
- dotnet-test-mspec
- コンソールでは動きますが、2016/12/04時点では後述のプロトコルに対応していません
- https://github.com/machine/machine.specifications/issues/303
主要フレームワークはすでにpreviewやbeta版がリリースされているようですね。 Visual Studioにべったり(?)だったMSTestも次のバージョンからNuGetで取得できるようになったのが見所かもしれません。
ちなみに、私もF#向けテスティングフレームワーク開発に携わっている一人としてrunnerを作っていたりします。
- dotnet-test-persimmon
- いろいろ不十分ですが…
今回はこういったランナーを実装するにはどうすれば良いか、という話を雑に書いていきます。
注意事項
- 結局個人メモみたいな形になってしまったのでわかりづらいかも…
- 2016/12/04時点での情報なので、正式リリースの際には仕様が変わっているかもしれないことに注意してください。
- コンソールのみで動くランナーを作る場合にはここまでの知識は必要ありません
- 逆を言えば、Visual Studio上でテストを動かしたいなら対応必須です
test communication protocol
.NET Core CLIにはtest communication protocolというテスト用のプロトコルが定められています。 Visual Studio 2017以降のテストエクスローラーはこのプロトコルを使って.NET Coreなテストを実行しているようですね。
.NET Core CLI test communication protocol | Microsoft Docs
機械翻訳?もあるみたいです。
.NET Core CLI テスト通信プロトコル | Microsoft Docs
必要になる知識はこれを読めば全部手に入るのですが、これを読んだだけで実装できたら苦労はしない…。
というわけで、以降では実装に必要なものなどをあげていきます。
パッケージ名
パッケージ名に関してはproject.json
時代の話を書くので、状況次第では役に立たなくなるかもしれません。
パッケージ名はdotnet-test-foo
のfoo
部分にプロジェクト名をつけます。
これはproject.json
に"testRunner": "foo"
と記述すると、.NET CLIが依存関係の中からdotnet-test-foo
パッケージを探索するという決まり事があるためです。
先に紹介したドキュメントのシーケンス図内ではdotnet-test-runner
という表現がなされています。
Microsoft.Extensions.Testing.Abstractions
作成したプロジェクトの依存関係に次のパッケージを追加します。
このパッケージにはAdapterやdotnet testとの通信で使う型やインターフェースが定義されています。
コマンドラインオプション
ここに列挙するオプションはAdapter, dotnet test, ランナーが連携する際に必要となります。
Test discovery(テスト探索)とTest execution(テスト実行)に共通するオプションとそうでないオプションがあります。
共通
designtimeオプション
design modeを示すオプションです。 このオプションがついている場合はコンソール実行ではないと考えたほうが良いでしょう。
--designtime
portオプション
Adapterとランナー、あるいはdotnet testとランナー間でTCP通信するためのポート番号がオプションで渡されます。
--port ポート番号
テスト対象のアセンブリ一覧
テスト対象のアセンブリはオプション名なしのスペース区切りで渡されます。
testAssembly1 testAssembly2
Test discoveryで必要になるオプション
listオプション
テストのリストアップを要求するオプションです。 このオプションが渡された場合、ランナーはテストを実行する必要はありません。
--list
Test executionで必要になるオプション
wait-commandオプション
実行するテスト一覧を取得するために待機する必要があることを示すオプションです。
--wait-command
実装面
探索したテストの送信やテスト結果の送信はITestDiscoverySinkもしくはITestExecutionSinkを実装したクラスを使って行います。 この2つのインターフェースはMicrosoft.Extensions.Testing.Abstractionsにあります。
ポートが指定されていない場合
Microsoft.Extensions.Testing.Abstractionsに用意されているStreamingTestDiscoverySinkとStreamingTestExecutionSinkを使うのが手っ取り早いです。 おそらくコンソール上で実行しているはずなので、Console.OpenStandardOutputを渡せばよいと思います。
ポートが指定されている場合
独自にITestDiscoverySink, ITestExecutionSinkを実装したクラスを用意します……といっても基本はdotnet-test-nunitのやつをライセンスつけて持って来ればよいと思います。
NUnitのやつはSocket, NetworkStream, BinaryWriterを使って通信するようになっています。
IPアドレスは自分のPCがターゲットなのでIPAddress.Loopback
固定で大丈夫なはずです。
Adapterやdotnet testとの通信はMessage型を使ってやりとりされます。 そして、このMessage型のPayloadはNewtonsoft.Jsonを使っているので、送信時は必然的にNewtonsoft.Jsonを使ってシリアライズしてから送信します。
wait-commandが指定された場合
ITestExecutionSink#SendWaitingCommand
で待機中であることを送信するMessage#MessageType
に指定する文字列はTestRunner.WaitingCommand
- BinaryReader等で送られてきたJSONをMessageとしてデシリアライズ
- ここで送られてくる
Message#Payload
の型はRunTestsMessage
なので、message.Payload.ToObject<RunTestsMessage>().Tests
などとすれば実行すべきテスト一覧が取得できる
ここで取得できるテスト一覧はFullyQualifiedNameとなっています。 これを用いてテスト実行前にフィルタリングしていくことになります。
回りくどいなーという気持ちになるかもしれませんが、FullyQualifiedName自体が長かったりそもそもテスト数が多かったりしてコマンドラインのサイズ上限を容易に超えてしまうので仕方がないですね。
メッセージの送信
Adapterやdotnet testに情報を送信する際にもMessage型を使います。
MessageはMessageType
というstringのプロパティを持ちます。
で、このMessageType
に指定する文字列は決まっているわけですが、定数として定義されているわけではないのでランナー側で各自定義するしかないようです。
シリアライズ/デシリアライズのことを考えてstringなのはまぁ許せるとして、どうして定数をパッケージ側で用意しなかったのでしょうね…。
TestFound
--list
オプションが指定されている時にテストを発見した場合はITestDiscoverySink#SendTestFound
を使ってテストを送信します。
Message#MessageType
に指定する文字列はTestDiscovery.TestFound
です。
SendTestFound
や後述するSendTestStarted
ではTest型のオブジェクトを送信する必要があるのですが、Test#Properties
のvalueにシリアライズできないような値を突っ込むと落ちるので気をつけましょう。
TestStarted
--designtime
オプションが指定されている時、テスト実行前にITestExecutionSink#SendTestStarted
を使ってテスト開始を通知します。
Message#MessageType
に指定する文字列はTestExecution.TestStarted
です。
TestResult
--designtime
オプションが指定されている時、テスト実行後にITestExecutionSink#SendTestResult
を使ってテスト結果を報告します。
Message#MessageType
に指定する文字列はTestExecution.TestResult
です。
なお、F#のAsyncを使ってParallelに実行しつつITestExecutionSink#SendTestResult
を呼び出したところ、テストを2つ以上実行しようとした際にSocketExceptionが投げられました。
同期的な通知に修正したらこの例外は発生しなくなったので、もしかしたら2016/12/04時点ではITestExecutionSink#SendTestResult
を同期的に呼び出す必要があるかもしれません(イマイチ確証が持てないですが…)。
後片付け
--designtime
が指定されている場合は、ITestDiscoverySink#SendTestCompleted
かITestExecutionSink#SendTestComleted
を使って完了を通知します。
Message#MessageType
に指定する文字列はTestRunner.TestCompleted
です。
コンソールで実行している場合は別途コンソール用の出力を実装したり、XMLレポートをちゅつ力してあげれば良いでしょう。
おわりに
テストランナーを実装するには色々とお作法に従う必要があることが理解できたかと思います。
しかし、Visual Studioのテストエクスプローラー用拡張を実装するよりは格段に楽になっています。
VS拡張ではドキュメントがほとんどなく、Microsoft.VisualStudio.TestPlatform.ObjectModel
というNuGetにpublishされていないdllと格闘する必要があったためです。
今回言及していない点としては、AppDomainどうするの問題があります。 現行の.NET CoreにはAppDomainに相当する機能がないため、どうすれば良いのかよくわからないというのが正直なところです。 xUnit.NETではAppDomainがある環境とない環境でifdefを駆使してるみたいですが、果たしてこれが正解なのか…?
とまぁ、長くなりましたが、実装するかどうかはともかく自分たちが使っているプロトコルの仕様を読んでみるのも一興かと思います。