go testの動作とコードの自動生成

  • 18
    いいね
  • 0
    コメント

はじめに

golangのコードの自動生成に対しては、これといって定まった方法が無いように見える。

業務ではJSON Schemaからtext/templateを用いてgolangのコードの自動生成をしているのだが、
他にもテキストフォーマット系関数を用いて文字列をつなげる形のコード生成(たとえばlestrratさんのgo-jsval)や、公式に提供されているgo generateによるもの(具体的にはstringer)などあり、それぞれに一長一短があると思う。

ところでgo testの挙動について調べていたら、本家でtext/templateを用いたコード生成の面白い使われ方をしていたので、具体的にコードを追いながら紹介したいと思う。

go testのなかみ

go testのコマンドを実行すると、割愛するが諸々あってrunTestが呼ばれる。runTestは以下のような動作をしている。

https://github.com/golang/go/blob/master/src/cmd/go/test.go#L401

src/cmd/go/test.go

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型については、このコード内での大まかな使われ方として

  1. test関数で具体的な実行内容をaction.f挿入する
  2. test関数で依存関係をaction.depsに挿入する
  3. 一番最後のdo関数によって、action.depsactionのリストに変換する
  4. do関数内で並列に関数を実行する

という形で用いられる。
実際にactionの具体的なdo関数内での使われ方を見てみると

https://github.com/golang/go/blob/master/src/cmd/go/build.go#L1262

src/cmd/build.go

// 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関数は大まかに

  1. フォルダを生成し、そこで_testmain.goというファイルにwriteTestmain関数を実行する
  2. _testmain.goをbuildするactionであるpmainActionを作成する
  3. 2に依存してrunTestを実行するactionであるrunActionを作成する

という動作をしている。

https://github.com/golang/go/blob/master/src/cmd/go/test.go#L654

src/cmd/gotest.go

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パッケージを用いてコードを生成している。

https://github.com/golang/go/blob/master/src/cmd/go/test.go#L1321

src/cmd/go/test.go

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関数はコマンドの実行を行い、最終的にテストが行われる。

https://github.com/golang/go/blob/master/src/cmd/go/test.go#L1100

src/cmd/go/text.go

// 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のコードの自動生成は上手いこと使うと本当に柔軟に様々なことを可能にするし、実際業務上でもとても役に立っているのでおすすめです。