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コマンドを作っていたりしたのだけれど。

github.com

$ ioknife too --cmd <server> --cmd <client>
Ctrl+c でSIGINT

まぁmakefileとかでも自由にやりたいよね。ということで色々考えたりしていたのだった。暫定的には以下の様なものなのだけれど。まぁterminated的なメッセージがエラー扱いなので微妙だなーと思ったりした。

Makefile

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。まぁ再帰的には使えないけれど。


  1. ここではログインしているシェルの意味

  2. godocのexampleにあるようなテキトウなコード

  3. 全ての立ち上げたsub processがSIGHUPでのgraceful stopに対応しているということは期待できないのでSIGTERM

go用のテストライブラリを作りました(言いたいことは分かります)

github.com

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を使っていたんすが、これは欲しいことに対して機能が多すぎるかなーと思ったりしてました。

github.com

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を使った時に気になった部分のひとつ目は比較対象の値がどちらの目的で使われているのかわかりにくい点です。

  • expected value (期待する値, 正答例, 解答)
  • actual value (実際の値, 回答)
// 両者の比較

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とはーで説教するされるという感じではなく)。