TL;DR
-
go test
すると標準出力にはテストケースの成功も失敗もターミナルで白文字はわかりづらいよね - 他にもツールあるけど
rakyll/gotest
使うとターミナル・CircleCIとかの出力に色付けされてわかりやすいよ - ツール入れて便利だけど、せっかくだからソースコードも読んでみるよ
そんな記事です。兎にも角にもまずはインストールからはじめます。
インストール
公式リポジトリに従いインストールして実行してみます。
# install
$ go get -u github.com/rakyll/gotest
# run test
$ gotest -v
rakyll/gotest
の結果はこのようになります。はい。
詳細
リポジトリを覗いてみるとrakyll/gotest
はmain.go
のみで実装されているシンプルなツールです。
os.Args[:1]
をexec.Command()
に渡しているのでgo test
と同じオプション引数が使用できます。
// 一部抜粋
func main() {
setPalette()
enableOnCI()
os.Exit(gotest(os.Args[1:]))
}
func gotest(args []string) int {
var wg sync.WaitGroup
wg.Add(1)
defer wg.Wait()
r, w := io.Pipe()
defer w.Close()
args = append([]string{"test"}, args...)
cmd := exec.Command("go", args...)
cmd.Stderr = w
cmd.Stdout = w
cmd.Env = os.Environ()
go consume(&wg, r)
if err := cmd.Run(); err != nil {
if ws, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok {
return ws.ExitStatus()
}
return 1
}
return 0
}
func consume(wg *sync.WaitGroup, r io.Reader) {
defer wg.Done()
reader := bufio.NewReader(r)
for {
l, _, err := reader.ReadLine()
if err == io.EOF {
return
}
if err != nil {
log.Print(err)
return
}
parse(string(l))
}
}
標準出力への色付けはfatih/colorに依存しています。
デフォルトではテストケースが「successは緑色」「failは赤色」となっています。
// 一部抜粋
var (
success = color.New(color.FgGreen)
fail = color.New(color.FgHiRed)
)
READMEに書いている通り、環境変数GOTEST_PALETTE
を指定することでsuccess
、fail
の色を変更することができます。
$ GOTEST_PALETTE=yellow,blue gotest -v
指定できる色はmapで管理しています。
// 一部抜粋
var colors = map[string]color.Attribute{
"black": color.FgBlack,
"hiblack": color.FgHiBlack,
"red": color.FgRed,
"hired": color.FgHiRed,
"green": color.FgGreen,
"higreen": color.FgHiGreen,
"yellow": color.FgYellow,
"hiyellow": color.FgHiYellow,
"blue": color.FgBlue,
"hiblue": color.FgHiBlue,
"magenta": color.FgMagenta,
"himagenta": color.FgHiMagenta,
"cyan": color.FgCyan,
"hicyan": color.FgHiCyan,
"white": color.FgWhite,
"hiwhite": color.FgHiWhite,
}
rakyll/gotest
ではRun、Pass、Failの3種類の出力を判定して色分けしている。
出力の判定はstrings.HasPrefix()
を使って文字列を判別するシンプルな実装。
// 一部抜粋
var c *color.Color
func parse(line string) {
trimmed := strings.TrimSpace(line)
switch {
case strings.HasPrefix(trimmed, "=== RUN"):
fallthrough
case strings.HasPrefix(trimmed, "?"):
c = nil
// success
case strings.HasPrefix(trimmed, "--- PASS"):
fallthrough
case strings.HasPrefix(trimmed, "ok"):
fallthrough
case strings.HasPrefix(trimmed, "PASS"):
c = success
// failure
case strings.HasPrefix(trimmed, "--- FAIL"):
fallthrough
case strings.HasPrefix(trimmed, "FAIL"):
c = fail
}
if c == nil {
fmt.Printf("%s\n", line)
return
}
c.Printf("%s\n", line)
}
cmd.Run()
でgo test
を実行していますがcmd.Run()
はコマンドが終わるまで処理をブロックしてしまうため、main関数とは別のgoroutineを生成し、そこでtest結果をio.Pipe()
を経由して最終的に色付けを行っている。
ここの部分は以前にgo test
の結果をすべて読み取れないケースのバグがあったようで、mattnさんがPull Requestを送り修正されていました。
// 一部抜粋
// ここのgoroutineでgo testの結果をEOFになるまで読み取り、色をつけて出力してる
go consume(&wg, r)
if err := cmd.Run(); err != nil {
if ws, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok {
return ws.ExitStatus()
}
return 1
}
詳しい解説はこちらに。goroutine でドハマリした。
簡単ではありますが、以上がrakyll/gotest
が行っている処理でした。
余談ですがソースコードを読んでみてio.Pipe()
に関して勉強になりました。
終わりに
issueにもありますがt.Skip()
した時にswitchの条件に一致せず直前のループで処理した色を出力してしまう実装になっているので、Skip用の色付けを行う修正PR送って見るのもありかもしれません。
と思ったらPRあった...