blog.syfm

徒然なるままに考えていることなどを書いていくブログ

Go 1.13 にアップデートするとテスト時に "flag provided but not defined" エラーが発生するケース

Go でテストを実行する際は動的に生成された main 関数がエントリポイントとなっています。
Go 1.13 からはテスト実行時のテストフラグ (-v-coverprofile など) の登録がこの生成された main 内で行われるようになったため、 main より先に実行される init 内でフラグの登録を行っているテストではフラグのパースがうまく動かず、テストが落ちるようになります。

たとえば、自分が開発している Evans では golden files testing のために --update フラグを e2e パッケージで用意し、これが有効になっている場合のみ golden files を更新するような設定になっていました。

var (
    update = flag.Bool("update", false, "update goldens")
)

func init() {
    flag.Parse()
    if *update {
        os.RemoveAll(filepath.Join("testdata", "fixtures"))
        if err := os.MkdirAll(filepath.Join("testdata", "fixtures"), 0755); err != nil {
            panic(fmt.Sprintf("failed to create fixture dirs: %s", err))
        }
    }
}
$ go test -v
flag provided but not defined: -test.v
Usage of /var/folders/p8/yjvb_xcn54d3dsfjz76rywvh0000gn/T/go-build261037663/b001/e2e.test:
  -update
        update goldens
exit status 2
FAIL    github.com/ktr0731/evans/e2e    0.018s

これを回避するためには Go 1.13 で新たに追加された testing.Init()init 内で呼びます。
これはテストフラグを明示的に登録してくれる関数で、これをフラグのパース前に呼ぶことで正常にパースすることができるようになります。

var (
    update = flag.Bool("update", false, "update goldens")
)

func init() {
    testing.Init()
    flag.Parse()
    if *update {
        os.RemoveAll(filepath.Join("testdata", "fixtures"))
        if err := os.MkdirAll(filepath.Join("testdata", "fixtures"), 0755); err != nil {
            panic(fmt.Sprintf("failed to create fixture dirs: %s", err))
        }
    }
}

モノレポで Go Modules を使う

Multi-Module Repository

Go Modules では Git で管理されているリポジトリ内に複数のモジュールを定義することが可能です。*1
これを利用すると、たとえばプロジェクト内で使う Go ツールの依存管理をプロジェクトのコードとは異なる go.mod で行うことができます。

$ go mod init # create project go.mod
$ mkdir tools && cd tools
$ go mod init # create tools go.mod
$ cat <<EOF
// +build tools

package tools

import (
        _ "github.com/rakyll/statik"
)
EOF
$ go get -tags tools ./... # add tool dependencies to go.mod

このとき、ディレクトリはこのような構造になっています。

.
├── go.mod
├── go.sum
└── tools
    ├── go.mod
    ├── go.sum
    └── tools.go

モジュール単位のバージョニング

Go modules はモジュール単位でバージョニングを行うため、モジュールを分ければ当然バージョニングも別々にできるようになります。
Go modules では VCS のタグから prefix を解釈することができ、マルチモジュール構成の場合は prefix を付けることでモジュール単位でバージョニングできます。
prefix はリポジトリ内のパスに対応し、ルートモジュールの prefix は空文字列であるため、単純に v0.1.0 といった見慣れたタグ名になります。 上記のマルチモジュール構成の場合、 tools のバージョニングには tools を prefix としてつけ、 tools/v0.1.0 といったタグ名になります。

モノレポと Go Modules

既存のベンダリングツールではリポジトリ単位でしかバージョニングができませんでしたが、Go modules を使うと上記のようにモノレポでも異なるリリースサイクルを持つことができるようになります。

また、Go プロジェクトのモノレポにおいて Renovate *2 のような依存パッケージ・モジュールの更新を行ってくれる bot との相性が悪くなっていました。
たとえば google-cloud-go は、GCP のサービス群のライブラリがすべて置かれているリポジトリですが、従来ではリポジトリ単位でしかバージョニングできなかったため、プロジェクトで使っていないパッケージの更新によりバージョンが上がった場合でも Renovate が動作してしまうといった問題がありました。
しかし、Go modules により必要なモジュールだけを依存対象に含められるようになっため、このような問題は解消することができるようになりました。

google-cloud-go はマルチモジュール構成のお手本としても参考になります。

Releases · googleapis/google-cloud-go · GitHub

今回紹介したことは Go Wiki にも記載されているため、詳しくはこちらを見てみてください。

Modules · golang/go Wiki · GitHub

オフ会

オフ会というとどことなくライトなイベントであるイメージが湧くけど、それでもオフ会という言葉が適切だと思う。

自分が春に就職し、社会人一年目となったこの歳に昔からのコミュニティと初めてオフ会をした。
当時自分は中学 1 年生だったから、実に 10 年越しとなった。

当時はガラケースマホも、PC すらも持っていなく、ただ誕生日プレゼントに親からもらった DSi だけを使ってうごメモはてなを通してインターネットにつながっていた。当時、「高速にはてなスターを付ける方法」といった手法を紹介するメモだとか、メモのコメント欄を使ったチャットルームとかがたくさんあって。IT の欠片もわからない子供だからただひたすらに DSi にしがみついて手動で再読み込みを押し続けたり、うごレターっていうメールみたいな機能で DM をやりとりしたり。中学生前半の頃は親のしつけが厳しくてベッドで寝たふりをしながら布団の中で DSi を起動していたり。

今じゃハイスペックなスマホを持って Mac でスイスイネットサーフィンができるわけで、当時やっていたことを考えるとやっぱりありえないなー。と思うけど、今日居酒屋でその話が出たとき、みんな共感していて面白かった。一度もリアルであったことのない人たちと 10 年前の話で意気投合できるんだよ。初めて会った人ばかりなのに昔からの友人と地元で遊んでいるみたいな感覚に陥るし。当日もいきなりボドゲから始まって、カラオケ、飲み会をしてから二次会でようやく自己紹介をし始めたりして。

自分の中学・高校の青春のかなりの割合はこのコミュニティと接してきていて、自分の黒歴史のすべてもここから生まれていたけれど、それでもかけがえのない思い出になっているし、今している仕事に行こうと思ったきっかけでさえここが原点だからとにかく言葉だけでは語り尽くせない。

きっとこれから、今日来れなかったメンバーも含めてまた定期的に集まることがあるのだろうけど、今日を超える衝撃を得ることは今後ないと思う。

今はもううごメモはてなはなくなってしまったし、代替のチャットルームも過疎化が進んでいたけれど、それでもこの関係は長く続いていくだろうし、続いて欲しい。

f:id:ktr_0731:20190916002307j:plain