go-run meets pstree
以下の内容のメモ
- 現在の環境1で実行されるprocessの概観を掴む方法
- go runがtmp directoryにbuildした結果のバイナリをsubprocessとして実行していること
go runが実行するprocess
例えば以下の様なテキトウなfile serverのようなコードがあるとする2。
main.go
package main import ( "log" "net/http" ) func main() { log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir("/usr/share/doc")))) }
これをgo runで実行する。
$ go run main.go
/usr/share/doc
以下を公開しているので以下の様な形でgoのtos.htmlが取れる。
$ http -b :8080/go/tos.html <!--{ "Title": "Terms of service" }--> <p> The Go website (the "Website") is hosted by Google. By using and/or visiting the Website, you consent to be bound by Google's general <a href="//www.google.com/intl/en/policies/terms/">Terms of Service</a> and Google's general <a href="//www.google.com/intl/en/privacy/privacy-policy.html">Privacy Policy</a>. </p>
まぁ今回の記事の主題はそこではない。とりあえず今回はgo runで実行されたprocessがあるというところが重要。
実行されるprocessの概観を掴む
実行した環境で Ctrl + z をして実行中のプロセスをバックグラウンドに持っていく。そしてこの環境で実行されているprocessを知りたい。
# ctrl + z [1]+ Stopped go run main.go
ここで ps --forest
が便利。個人的には初手 ps -j --forest
が一番わかり易いと思った(linux環境)。
$ ps -j --forest PID PGID SID TTY TIME CMD 9349 9349 9349 pts/2 00:00:00 bash 8612 8612 9349 pts/2 00:00:00 \_ go 8665 8612 9349 pts/2 00:00:00 | \_ main 8795 8795 9349 pts/2 00:00:00 \_ ps
なるほどたしかにgo runはmain.goのprocessを生成している。そんなわけでgo runで立ち上げたprocessをCtrl cなどで終わらせようとした時にもたつくような感じがあったりするわけっぽい(体感的なものかもだけど)。ちなみにたまたまこの記事を書く時に調べて --forest
オプションの存在を知った。ps奥が深い。
ここでpid(process id)の他にpgid(process group id)の方に注目、go run側のprocessを殺してもそのprocessが生成しているsub processまで殺せない場合があるかもしれない、幸いpgidは一緒なので一気にprocessをkillしたい場合にはpgidを指定して実行してあげるのが良さそう。
$ pkill -TERM -g 8612 $ fg go run main.go (wd: ~/venvs/my/individual-sandbox/daily/20190806/example_go/03fileserver) Terminated
ところでこのページを読んでkillでもpgidを指定できることを知った(それとは関係なくプロセスのことをあまり知らないひとの勉強にこのweb book(?)良さそうだなーと想ったりした)。
たしかにmanにも書いてある。kill -<pgid>
でできたんだ。今までpkillを使っていた。
$ man kill ... -n where n is larger than 1. All processes in process group n are signaled. When an argument of the form '-n' is given, and it is meant to denote a process group, either a signal must be specified first, or the argument must be preceded by a '--' option, otherwise it will be taken as the signal to send.
pstree, pkill
ところでps, killだけでできそうならpstree, pkill不要じゃんとかおもったりもしたんだけれど。イメージ的にこういう認識。
- ps, kill -- よりprimitive。詳細が異なる
- pstree, pkill, (pgrep) -- より抽象度が高め。便利(?)
どうやらmacの方のpsには--forest
オプションの存在が無いらしい。そういう意味ではpstreeでの確認方法も知っておくと良いかもしれない。デフォルトではスレッド自体も表示してしまって邪魔なので-Tオプションで無視するようにしている。pid,pgidは知りたいですよね。
# 実行しなおしたのでpidなどが変わっている $ ps aux | ioknife rest | grep "go run" USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND me 9561 0.2 0.1 780048 20996 pts/1 Sl+ 16:19 0:00 go run main.go $ pstree -T -g -p 9561 go(9561,9561)───main(9617,9561)
ちなみに逆方向も実行できて祖先方向も見れる(-s)。
$ pstree -T -g -p -s 9561 systemd(1,1)───systemd(1475,1475)───gnome-terminal-(1855,1855)───bash(1883,1883)───screen(2403+
psの--forest
オプションを含めて記事などを書くときに便利だと想う。
clientがrequestしてそれが終了したらserverを終了させたい
実は元々の裏テーマとして以下の様なことがしたかった。
- serverを立ち上げる (e.g. 冒頭のファイルサーバー)
- clientを立ち上げる (e.g. curl, httpieなどでのrequest)
- clientのrequestが終了したらserverを終了させたい。
これのためにioknifeにtooコマンドを作っていたりしたのだけれど。
$ ioknife too --cmd <server> --cmd <client> Ctrl+c でSIGINT
まぁmakefileとかでも自由にやりたいよね。ということで色々考えたりしていたのだった。暫定的には以下の様なものなのだけれど。まぁterminated的なメッセージがエラー扱いなので微妙だなーと思ったりした。
default: ioknife too --cmd "go run main.go" --cmd "make client"& echo $$! > x.pid && wait $$(cat x.pid) client: sleep 1 http GET :8080/go/tos.html pkill -TERM -g $$(ps -o pgid -p $$(cat x.pid) | ioknife rest) || echo ok
こんな感じ。
$ make ioknife too --cmd "go run main.go" --cmd "make client"& echo $! > x.pid && wait $(cat x.pid) [1] make make[1]: Entering directory /$HOME/venvs/my/individual-sandbox/daily/20190806/example_go/03fileserver' [1] make sleep 1 [1] make http GET :8080/go/tos.html [1] make <!--{ [1] make "Title": "Terms of service" [1] make }--> [1] make [1] make <p> [1] make The Go website (the "Website") is hosted by Google. [1] make By using and/or visiting the Website, you consent to be bound by Google's general [1] make <a href="//www.google.com/intl/en/policies/terms/">Terms of Service</a> [1] make and Google's general [1] make <a href="//www.google.com/intl/en/privacy/privacy-policy.html">Privacy Policy</a>. [1] make </p> [1] make pkill -TERM -g $(ps -o pgid -p $(cat x.pid) | ioknife rest) || echo ok [1] make PGID INFO:ioknife.signalhandle:send signal (Signals.SIGTERM) make: *** [Makefile:2: default] Terminated Terminated
さいごに
そんなわけでioknifeのtooにSIGHUPあたりを送ったら全体にSIGTERMを送るみたいな機能を付けても良いかもなーとおもったりした3。まぁ再帰的には使えないけれど。
-
ここではログインしているシェルの意味↩
-
godocのexampleにあるようなテキトウなコード↩
-
全ての立ち上げたsub processがSIGHUPでのgraceful stopに対応しているということは期待できないのでSIGTERM↩
go用のテストライブラリを作りました(言いたいことは分かります)
go用のテストライブラリを作りました(言いたいことは分かります)。
使いかたはこんな感じです。
noerror.Must(t, noerror.Equal(30).Actual(add(10,20))) noerror.Should(t, noerror.NotEqual(30).Actual(add(10,20)))
以下詳細。
motivation
元気があるときにはtestingパッケージをそのまま使うのが良いという事は分かるし、その方が見通しも良くなることもあるということも分かっています。ただ、元気が無いときやまたは「我gopherぞ?」みたいな自尊心に溢れていない時にはついつい面倒になってしまうんですよね。ifは許容範囲なんですがSprintf形式の文字列を書くのがめんどくさい感じです。
個人的には、特にリリース対象ではないコードや日常的な調査用のメモのコードでも結果を確かめるためにテストコードを書く事があるんですが、そういう時に面倒に感じたりしてます(テストコードが必要な理由は頭があまり良くないせいかもですね。うかつなんです。そういうことにしておきましょう)。
そんなときに今までtestifyを使っていたんすが、これは欲しいことに対して機能が多すぎるかなーと思ったりしてました。
testifyは色々比較用の関数(matcher)がたくさんあるんですが覚えきれないし結局使うのは一部だけ。加えてほかの環境でも常に利用したいかと想うかというとそうでもない。なので網羅して覚えようというモチベーションも少なめでした。。
そんなわけでちょっとしたassertionライブラリが欲しかったんです。
Sometimes, simple is too strict, and easy is too complicated
イメージ的にはそんな感じです。
concepts
せっかく作るんでもうちょっとまじめにコンセプトを考えてみようかなと思いました。
考えてみると、実質エラーチェックだけできれば十分で、細かな記述なしにそれなりの形でエラーメッセージをつくってくれれば十分かもと思ったりしました。
そんなわけで、以下がコンセプト。
- testing library is error check library
- zero dependencies
エラーチェックだけできれば十分なのではというのは仮説です。 (依存がゼロというのは趣味みたいなもので省略します)
ついでにtestifyを使っていてちょっと嫌だなーと思う部分にも対応することにしました。
作っているときに考えていたこと
ここからは作っている時に考えていたことを詳しく。
testing library is error check library
テスト用のライブラリで必要なのはエラーチェックだけで十分なのでは?というのはこういうことです。通常testingパッケージを直接使って手書きしたようなコードでは以下の様なコードになると思います。
// testingをそのまま使ったコード if got := add(10, 20); got != 30 { t.Errorf("expected %d, but actual %d", got) }
if文はともかくErrorfの方がめんどくさいわけですね。一方testifyを使ったコードはこうなります。
// testifyを使ったコード assert.Exactly(t, 30, add(10,20))
これは短いのですが、個別にmatcherが定義されていて、これを適宜使い分けたり存在を認識するのがめんどくさかったりします。元々の目的が日常的な書き捨て用途のものだったりするので(記憶が確かならYAMLなどにも依存するのでgo.modを汚す感じなのも気分があんまり良くなかったりします)。
今回作ったライブラリを使ったコードではこうなります。
// noerrorを使ったコード(今回作ったライブラリ) noerror.Should(t, noerror.Equal(30).Actual(add(10,20)))
testifyより長くはなります。
エラーチェック
そしてassertionは結局エラーチェックだけで良いということの意味なのですが、先程のコード(再掲)のEqual()
は問題があった時にエラーを返すような関数です。そしてShould()
はエラーが渡されたときだけ反応する関数です。
noerror.Should(t, noerror.Equal(30).Actual(add(10,20)))
これらをそれぞれassertion関数、comparison関数と呼んでいます。一般的には以下の様な形で利用されます。
// t is *testing.T // want is expected value // got is actual value <Assertion>(t, <Comparison>(want).Actual(got))
つまりassertion関数はエラーを受け取ってよしなにするだけ、そしてcomparison関数は何かを比較してエラーを返すだけの関数です。これでおしまい。
assertion関数
実際定義されているassertion関数は以下の2つだけです。
- Must()
- Should()
Must()
はtestifyのrequire.NoError()
に対応し、エラーを受け取ったらt.Fatal()
を呼び出します。
Should()
はtestifyのassert.NoError()
に対応し、エラーを受け取ったらt.Error()
を呼び出します。
これでおしまいです(実際にはログ用のLog()
も定義されていたりします)。エラーが発生した場合は以下の様なエラーメッセージになります。
Equal, expected 10, but actual 2
ちなみにエラーメッセージのフォーマット自体はあまり真剣に考えたものではないのでご意見ご要望をお待ちしてます。
少しおもしろいのはエラーを返す関数のテストは以下のようにも書けるところです。まぁ意味合いとしてはNoError(t, err)
なので。。doSomething()
はエラーを返すような関数です。
noerror.Must(doSomething())
以下のようなメッセージがテストに失敗したときに出力されます(pkg/errorsによるスタックトレースの表示にも一応対応しています)。
unexpected error, *ERROR*
このコードの意味が分かるようにすることとEqual()
などのcomparison関数の名前との対応を考えたときのパッケージ名でけっこう頭を悩ませたりしました。noerrorがmustだとなんとなく意味は通じそうな感じです。
(実は最初はeasyとsimpleのちょうど良い塩梅ということを現したくてhandyという名前でした。handy.Must(t, err)
だと意味がわからないですよね)
comparison関数
comparison関数は以下の6つです。意味は関数名からなんとなく解ると思います。JSONEqualだけ特別かもです。それぞれ=
でstrictに比較するもの、reflect.DeepEqual()
でゆるく比較するもの、JSON形式に変換してから再変換をかけてよりゆるく比較するものの3種類です。
Equal()
NotEqual()
DeepEqual()
NotDeepEqual()
JSONEqual()
NotJSONEqual()
これだけなのでわりと見通しが良いです。
True()
, False()
, Nil()
みたいなものを定義していこうとするとやがてtestifyになってしまうということでこれ以上増やすのは止めました(floatのような微小な差分が出てしまいそうなものやunorderedなslicesなどは頑張ってください)。
Actual(<value>)
ここからはtestifyで気になったことについても。
まず、testifyを使った時に気になった部分のひとつ目は比較対象の値がどちらの目的で使われているのかわかりにくい点です。
// 両者の比較 assert.Exactly(t, 30, add(10,20)) noerror.Should(t, noerror.Equal(30).Actual(add(10,20)))
実際にはgodocを読めば assert.Exactly(t, expected, actual)
ということが分かるのですが、そういうことを考えたくない気持ちがありました。
一方でnoerrorの場合は引数として渡す時にそもそも同時に渡すことをやめて、Actual(<value>)
形式でだけactual valueを指定するようにしました(testingパッケージ直打ちの場合には変数名とErrorfに渡すメッセージの部分でどちらか分かるようにしてますよね。当然)。
ちなみに実装の途中でActual()
代わりにExpected()
を利用するようにすると後で面倒な話になりそうと言うことが分かったりしました(これは後で説明します)。
Describe()
エラーメッセージのときにちょっとだけ文脈を追加したいですよね。さすがにエラーメッセージが以下だけだと分かりづらい。
Equal, expected 10, but actual 2
Describe()
を使うとメッセージの先頭部分を変えられます。
noerror.Should(t, noerror.Equal(10).Actual(1 + 1).Describe("1 + 1"))
結果はこう変わります。
1+1, expected 10, but actual 2
rest arguments
もうちょっと情報を付加したいことがあると思います。例えばweb APIのテストを書いている時にstatusが200を期待するテストで間違ったrequestになってしまったときなど。修正後のテストコードでは問題にならない部分なのですが、テストの修正中にはエラーレスポンスのbody自体が確認できると便利だったりします。400でBad Requestのときのエラーレスポンスにエラーの原因が書かれていたりすることがあるので(printデバッグをたまにしちゃったりもします)。
そんなわけでもう少し情報を余分に付加したくなります。これはtestifyに揃えてShould()
などが余分に引数を取れるようにしました。
noerror.Should(t, noerror.Equal(11).Actual(10), ":bomb:")
は以下の様な表示になります。
Equal, expected 11, but actual 10 :bomb:
実はここの部分と先程のエラーレスポンスの中身を表示したいという要求の相性が良くなかったりします。これはtestifyにも関連することなんですが、testingの場合にはif文で分岐されるので一度消費済みのio.ReadCloserが渡されることはないんですが(もちろん無いようにテストコードを書いてますよね?)、ここをeagerに評価してしまうと予期せぬ消費が行われて後のコードでエラーになるみたいなことが発生します。似たような機能を作るときにはlazyにしましょう。
今回の実装ではfmt.Stringerを実装したものならそちらを使う様になってます。なので以下の様なstructがたまに役に立つかもです。
type lazyString struct { toString func() string } func (x *lazyString) String() string { return x.toString() }
ちなみに最初はfunctional optionsのような形で提供していたんですけれど。邪魔に感じたんですよね。Describe()
も含めて。そんなわけでDescribe()
はメソッドにこちらは可変長引数にして対応することにしました。
ボーナスポイント
ここでボーナスポイントです。昔流行った表現で言えばOne more thingと言うやつです。
戻り値として多値でエラーを含んで返す関数があったりしますがこういう関数へのassertionがtestifyでは厄介でした。これを一行で書けるようになってます。
こういうやつですね。
// Count() returns error Count := func() (int, error) { return 0, fmt.Errorf("*ERROR*") } c, err := Count() require.NoError(t, err) assert.Exactly(t, c)
1行で書ける様になってます。ActualWithError()
です。
noerror.Should(t, noerror.Equal(0).ActualWithError(Count()))
実際にはちょっとだけ特殊なことをしていて、Must()
を使っていようがShould()
を使っていようが多値を返す関数でエラーがかえって来たらt.Fatal()
が呼ばれます。途中で実行をストップします。
大抵の場合、この種のコードがテストコード上に現れるのは、テスト対象の実行後のDBの整合性チェックだったりするので。
ちなみにActual()
のかわりにExpected()
を定義する様に実装していくとcomparison関数の種類が膨れ上がります。ツライですね(伏線の回収)。
さいごに
今回はREADMEなどをけっこうまじめに書いたのですが英語があまり得意ではないのでPRなどお待ちしております。もちろん機能要望なども。
ついでにgoのテストについてどこかでだれかと話したりしたいな−と思ったりしています(もちろん不満だーだるいーというような感情の発露みたいな感じではなく、郷に従えーとかgo wayとはーで説教するされるという感じではなく)。