Golden Road

好きだという熱こそが最低限で最高の希望

【Go】kingpinをinit()でパースしていたらテストでエラーが出た話 #golang

kingpin

標準のflagパッケージでは、同時に長いオプションと短いオプション(shortオプション)を定義するのが面倒。

package main

import (
    "flag"
    "fmt"
)

var (
    dryRun = flag.Bool("dry-run", false, "dry run mode.")
)

func init() {
    flag.BoolVar(dryRun, "n", false, "dry run mode.")
    flag.Parse()
}

func main() {
    fmt.Printf("dry run mode: %v\n", *dryRun)
}

説明も二重に出てる。

$ go build -o flag
$ ./flag --help
Usage of ./flag:
  -dry-run
        dry run mode.
  -n    dry run mode.

※説明に関してはパッケージ変数Usageを上書きすることで変えられる。

上記のような理由でshortオプションの定義がいい感じのパッケージ無いかな〜と探してたらkingpinを見つけた!
kingpinだといい感じにshortオプションを宣言できる!!

package main

import (
    "fmt"

    "gopkg.in/alecthomas/kingpin.v2"
)

var (
    dryRun = kingpin.Flag("dry-run", "dry run mode.").Short('n').Bool()
)

func init() {
    kingpin.Parse()
}

func main() {
    fmt.Printf("dry run mode: %v\n", *dryRun)
}

Short('n')だけでshortオプションを定義できる。

説明もデフォルトでいい感じ。

$ go run main.go --help
usage: main [<flags>]

Flags:
      --help     Show context-sensitive help (also try --help-long and --help-man).
  -n, --dry-run  dry run mode.

exit status 1

error in test

便利に使っていたkingpinだけど、テスト時に問題が。。。
フラグを指定していなければ何も問題ない。

$ go test
PASS
ok      gist/kingpin    0.008s

が、フラグを指定するとエラーが。。。

$ go test -v
kingpin.test: error: unknown short flag '-t', try --help
exit status 1
FAIL    gist/kingpin    0.008s

何が起きているのだろうか

-tの正体を突き止めるために引数を表示するログを入れてみた。

func init() {
    fmt.Println(os.Args)
    kingpin.Parse()
}

結果

$ go test -v
[/tmp/kingpin.test -test.v=true]
kingpin.test: error: unknown short flag '-t', try --help
exit status 1
FAIL    gist/kingpin    0.008s

-test.v=true←これが原因らしい。

標準のflagパッケージではどうか?

func init() {
    flag.BoolVar(dryRun, "n", false, "dry run mode.")

    fmt.Println(os.Args)

    flag.Parse()
}

結果

$ go test -v
[/tmp/flag.test -test.v=true]
=== RUN   TestRun
--- PASS: TestRun (0.00s)
PASS
ok      gist/flag   0.006s

大丈夫っぽい。
ソースを追ったら、flagパッケージのパッケージ変数CommandLinego testコマンドとテストバイナリで共有しているから大丈夫だったみたい。
(go testでテストバイナリ作る時に、go test用のオプションも含めてバイナリを作っているっぽい。)
確認のため、別なFlagSetを定義してそちらを使ってみる。

package main

import (
    "flag"
    "fmt"
    "os"
)

var (
    tflg   = flag.NewFlagSet("test_flag", flag.ExitOnError)
    dryRun = tflg.Bool("dry-run", false, "dry run mode.")
)

func init() {
    tflg.BoolVar(dryRun, "n", false, "dry run mode.")

    fmt.Println(os.Args)

    tflg.Parse(os.Args[1:])
}

func main() {
    fmt.Printf("dry run mode: %v\n", *dryRun)

    result := run(*dryRun)
    fmt.Printf("result: %v\n", result)
}

func run(dryRun bool) bool {
    if dryRun {
        return false
    }

    return true
}

FlagSetNewFlagSet()で定義できる。
Parse()だけ通常と違い、引数を渡してやる必要がある。

結果

$ go test -v
[/tmp/flag.test -test.v=true]
flag provided but not defined: -test.v
Usage of test_flag:
  -dry-run
        dry run mode.
  -n    dry run mode.
exit status 2
FAIL    gist/flag   0.006s

やはり失敗した。

まとめ

  • kingpin特有のエラーではなく、flagパッケージでも出る。
  • エラーを出さないためにはmain()でパースする必要がある。

基本的な事だけど、init()はテスト時に呼ばれて、main()はテスト時に呼ばれないって事だよね。