packagestestのExported.Expectを使ってGoのソースからマーカーを収集する
golang.org/x/tools/go/packages/packagestestパッケージは静的解析など、Goのソースを読み込んで何かしらの処理を行うようなツールのテスト用にダミープロジェクトを作るためのパッケージです。
そういったツールを作る際、テストで期待する結果を完全に決め打ちで_test.go
に書いておいても良いかもしれませんが、パターンを増やす時にはダミーのソースとそれに対応する期待結果をそれぞれ追加していかなければいけないので、数が増えてくるとメンテナンスが大変になってしまいます。
そのため、ダミーのソースの中にマーカーを埋め込んでおいて、それを元に期待する結果をテスト時に組み立てるようにすると、パターンを追加する時の変更箇所が1つになり、またテストコード自体の見通しも良くなります。
具体例はgoplsのテストを見るのが良いですが、簡単にまとめると、
1) テスト関数の本体でpackagestest.TestAllを呼び、
func Test(t *testing.T) {
packagestest.TestAll(t, test)
}
2) test
の中でpackagestest.Exportを使ってダミープロジェクトをセットアップし、
// Moduleにはtestdataにあるモジュールを指定しても良い exported := packagestest.Export(t, e, []packagestest.Module{ { Name: "example.com/pkg", // モジュール名 Files: map[string]interface{}{ "foo/foo.go": `package foo const Foo = 100 //@Foo, aaa(Foo, "const") `, "bar/bar.go": `package bar import "example.com/pkg/foo" const Bar = foo.Foo * 10 //@bbb("Bar", Foo) `, }, }, }) defer exported.Cleanup()
3) その戻り値のExpectメソッドでマーカーを収集する、
if err := exported.Expect(map[string]interface{}{ // @aaa()用 "aaa": func(pos token.Pos, arg string) { t.Logf("pos = %v, arg = %v", pos, arg) }, // @bbb()用 "bbb": func(pos token.Pos, arg token.Pos) { t.Logf("pos = %v, arg = %v", pos, arg) }, }); err != nil { t.Fatal(err) }
という流れになります。
あとは収集したマーカーを使って期待する結果を作ってあげればOKです。
ただExpect
メソッドがどうやって位置に変換しているのか、少しわかりにくかったのでメモを残しておきます。
- マーカーの文字列から
token.Pos
に変換する場合
// OK: ↓をそのまま文字列にする const Foo = 100 //@aaa("Foo", "const") // NG: 同じ行にない場合はエラー //@aaa("Foo", "const") const Foo = 100
- マーカーの識別子から
token.Pos
に変換する場合
// OK: ↓識別子をマーカーにしておく const Foo = 100 //@Foo, aaa(Foo, "const") // どこかにマーカーがあれば別の場所でも使える const Bar = foo.Foo * 10 //@bbb("Bar", Foo) // NG: Barはマーカーになっていないのでエラー const Bar = foo.Foo * 10 //@bbb(Bar, Foo)
// NG: 識別子にドットは使えないのでfoo.Fooにはできない const Bar = foo.Foo * 10 //@bbb("Bar", foo.Foo) // OK: かわりにmarkで識別子の名前を"fooFoo"にしておくと、 const Foo = 100 //@mark("fooFoo", Foo) // 別の場所でfooFooが使えるようになる const Bar = foo.Foo * 10 //@bbb("Bar", fooFoo)
- 別のマーカーを流用して
token.Pos
に変換する場合
// ↓を type A string //@type("AString", "A") // ↓で識別子として使いたい type Alias = A //@bbb("Alias", AString)
Expect
でbbb
が呼ばれる前に、type
をMarkするExpect
を呼んでおく。
if err := exported.Expect(map[string]interface{}{ "type": func(name string, r packagestest.Range, _ []string) { exported.Mark(name, r) }, }); err != nil { t.Fatal(err) }
もし需要がありそうならゴリラ.Goのネタにするかもしれません。