はじめに
golangのコードの自動生成に対しては、これといって定まった方法が無いように見える。
業務ではJSON Schemaからtext/templateを用いてgolangのコードの自動生成をしているのだが、
他にもテキストフォーマット系関数を用いて文字列をつなげる形のコード生成(たとえばlestrratさんのgo-jsval)や、公式に提供されているgo generateによるもの(具体的にはstringer)などあり、それぞれに一長一短があると思う。
ところでgo testの挙動について調べていたら、本家でtext/templateを用いたコード生成の面白い使われ方をしていたので、具体的にコードを追いながら紹介したいと思う。
go testのなかみ
go testのコマンドを実行すると、割愛するが諸々あってrunTestが呼ばれる。runTestは以下のような動作をしている。
func runTest(cmd *Command, args []string) {
...(省略)...
// Prepare build + run + print actions for all packages being tested.
for _, p := range pkgs {
buildTest, runTest, printTest, err := b.test(p)
if err != nil {
str := err.Error()
if strings.HasPrefix(str, "\n") {
str = str[1:]
}
failed := fmt.Sprintf("FAIL\t%s [setup failed]\n", p.ImportPath)
if p.ImportPath != "" {
errorf("# %s\n%s\n%s", p.ImportPath, str, failed)
} else {
errorf("%s\n%s", str, failed)
}
continue
}
builds = append(builds, buildTest)
runs = append(runs, runTest)
prints = append(prints, printTest)
}
// Ultimately the goal is to print the output.
root := &action{deps: prints}
...(省略)...
b.do(root)
}
ここでtest関数を呼び出して*action型のbuild, runs, printsを生成している。
このaction
型については、このコード内での大まかな使われ方として
- test関数で具体的な実行内容を
action.f
挿入する - test関数で依存関係を
action.deps
に挿入する - 一番最後のdo関数によって、
action.deps
をaction
のリストに変換する - do関数内で並列に関数を実行する
という形で用いられる。
実際にaction
の具体的なdo関数内での使われ方を見てみると
// do runs the action graph rooted at root.
func (b *builder) do(root *action) {
// Build list of all actions, assigning depth-first post-order priority.
// The original implementation here was a true queue
// (using a channel) but it had the effect of getting
// distracted by low-level leaf actions to the detriment
// of completing higher-level actions. The order of
// work does not matter much to overall execution time,
// but when running "go test std" it is nice to see each test
// results as soon as possible. The priorities assigned
// ensure that, all else being equal, the execution prefers
// to do what it would have done first in a simple depth-first
// dependency order traversal.
all := actionList(root) // ここでdepsを見ながらactionの配列に変換する
for i, a := range all {
a.priority = i
}
...(中略)...
// Handle runs a single action and takes care of triggering
// any actions that are runnable as a result.
handle := func(a *action) {
var err error
if a.f != nil && (!a.failed || a.ignoreFail) {
err = a.f(b, a)
}
...(中略)...
}
// Kick off goroutines according to parallelism.
// If we are using the -n flag (just printing commands)
// drop the parallelism to 1, both to make the output
// deterministic and because there is no real work anyway.
par := buildP
if buildN {
par = 1
}
for i := 0; i < par; i++ { // 並列度を制限しながら、具体的な実行であるhandleを呼び出している
go func() {
for _ = range b.readySema {
// Receiving a value from b.sema entitles
// us to take from the ready queue.
b.exec.Lock()
a := b.ready.pop()
b.exec.Unlock()
handle(a)
}
}()
}
<-done
}
ここで用いられるaction
はtest関数によって生成されている。
test関数は大まかに
- フォルダを生成し、そこで_testmain.goというファイルにwriteTestmain関数を実行する
- _testmain.goをbuildする
action
であるpmainActionを作成する - 2に依存してrunTestを実行する
action
であるrunActionを作成する
という動作をしている。
func (b *builder) test(p *Package) (buildAction, runAction, printAction *action, err error) {
...(省略)...
// Create the directory for the .a files.
ptestDir, _ := filepath.Split(ptestObj)
if err := b.mkdir(ptestDir); err != nil {
return nil, nil, nil, err
}
if err := writeTestmain(filepath.Join(testDir, "_testmain.go"), p); err != nil {
return nil, nil, nil, err
}
...(省略)...
// Action for building pkg.test.
pmain = &Package{
Name: "main",
Dir: testDir,
GoFiles: []string{"_testmain.go"},
ImportPath: "testmain",
Root: p.Root,
imports: []*Package{ptest},
build: &build.Package{Name: "main"},
fake: true,
Stale: true,
}
...(省略)...
if testC {
// -c flag: create action to copy binary to ./test.out.
runAction = &action{
f: (*builder).install,
deps: []*action{pmainAction},
p: pmain,
target: testBinary + exeSuffix,
}
printAction = &action{p: p, deps: []*action{runAction}} // nop
} else {
// run test
runAction = &action{
f: (*builder).runTest,
deps: []*action{pmainAction},
p: p,
ignoreFail: true,
}
cleanAction := &action{
f: (*builder).cleanTest,
deps: []*action{runAction},
p: p,
}
printAction = &action{
f: (*builder).printTest,
deps: []*action{cleanAction},
p: p,
}
}
return pmainAction, runAction, printAction, nil
}
さて、ここからが本題なのだが、writeTestmain関数は_testmain.goに何を作っているかというと
astパッケージを用いて、_testというsuffixのついたファイルについてTestというprefixのついた関数の一覧を取得し、text/templateパッケージを用いてコードを生成している。
func writeTestmain(out string, p *Package) error {
t := &testFuncs{
Package: p,
}
for _, file := range p.TestGoFiles {
if err := t.load(filepath.Join(p.Dir, file), "_test", &t.NeedTest); err != nil {
return err
}
}
for _, file := range p.XTestGoFiles {
if err := t.load(filepath.Join(p.Dir, file), "_xtest", &t.NeedXtest); err != nil {
return err
}
}
f, err := os.Create(out)
if err != nil {
return err
}
defer f.Close()
if err := testmainTmpl.Execute(f, t); err != nil { // ここでtext/templateを用いてコードを生成している
return err
}
return nil
}
...(中略)...
var testmainTmpl = template.Must(template.New("main").Parse(` // コードを生成するテンプレート
package main
import (
"regexp"
"testing"
{{if .NeedTest}}
_test {{.Package.ImportPath | printf "%q"}}
{{end}}
{{if .NeedXtest}}
_xtest {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
{{end}}
)
var tests = []testing.InternalTest{
{{range .Tests}}
{"{{.Name}}", {{.Package}}.{{.Name}}},
{{end}}
}
var benchmarks = []testing.InternalBenchmark{
{{range .Benchmarks}}
{"{{.Name}}", {{.Package}}.{{.Name}}},
{{end}}
}
var examples = []testing.InternalExample{
{{range .Examples}}
{"{{.Name}}", {{.Package}}.{{.Name}}, {{.Output | printf "%q"}}},
{{end}}
}
var matchPat string
var matchRe *regexp.Regexp
func matchString(pat, str string) (result bool, err error) {
if matchRe == nil || matchPat != pat {
matchPat = pat
matchRe, err = regexp.Compile(matchPat)
if err != nil {
return
}
}
return matchRe.MatchString(str), nil
}
func main() {
testing.Main(matchString, tests, benchmarks, examples)
}
`))
このような形で生成したgolangのコードをbuildしたbinaryについて、
上記の(*builder)runTest関数はコマンドの実行を行い、最終的にテストが行われる。
// runTest is the action for running a test binary.
func (b *builder) runTest(a *action) error {
...(中略)...
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = a.p.Dir
var buf bytes.Buffer
if testStreamOutput {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
} else {
cmd.Stdout = &buf
cmd.Stderr = &buf
}
t0 := time.Now()
err := cmd.Start()
...(中略)...
}
結び
全体の話をするためにかなり割愛したが、中略した部分にも面白い仕組みがあるので、興味が出た人はコードを読んでみるといいと思う。
golang本体のコードはとても参考になるので、随分とお世話になっているし、暇なときに読むと良い意味で新鮮な驚きがある。
そしてgo testで用いられているように、golangのコードの自動生成は上手いこと使うと本当に柔軟に様々なことを可能にするし、実際業務上でもとても役に立っているのでおすすめです。