rakyll/gotestでテスト結果に色をつける。そしてソースも読んでみる

## TL;DR
- `go test`すると標準出力にはテストケースの成功も失敗もターミナルで白文字はわかりづらいよね
- 他にもツールあるけど`rakyll/gotest`使うとターミナル・CircleCIとかの出力に色付けされてわかりやすいよ
- ツール入れて便利だけど、せっかくだからソースコードも読んでみるよ

そんな記事です。兎にも角にもまずはインストールからはじめます。

## インストール
[公式リポジトリ](https://github.com/rakyll/gotest)に従いインストールして実行してみます。

```bash
# install
$ go get -u github.com/rakyll/gotest

# run test
$ gotest -v
```

`rakyll/gotest`の結果はこのようになります。はい。
<img width="502" alt="gotest_result.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72518/1422a052-b26a-09ba-7b16-950467162d74.png">

## 詳細
リポジトリを覗いてみると`rakyll/gotest`は`main.go`のみで実装されているシンプルなツールです。
`os.Args[:1]`を`exec.Command()`に渡しているので`go test`と同じオプション引数が使用できます。

```main.go
// 一部抜粋
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](https://github.com/fatih/color)に依存しています。
デフォルトではテストケースが「successは緑色」「failは赤色」となっています。

```main.go
// 一部抜粋
var (
	success = color.New(color.FgGreen)
	fail    = color.New(color.FgHiRed)
)
```

READMEに書いている通り、環境変数`GOTEST_PALETTE`を指定することで`success`、`fail`の色を変更することができます。

```bash
$ GOTEST_PALETTE=yellow,blue gotest -v
```
上記コマンドで色変更するとこのように出力されます。はい。
<img width="465" alt="gotest_palette.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72518/554ba7b6-04b1-41b3-d140-0d92ece30947.png">

指定できる色はmapで管理しています。

```main.go
// 一部抜粋
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()`を使って文字列を判別するシンプルな実装。

```main.go
// 一部抜粋
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](https://github.com/rakyll/gotest/pull/7)を送り修正されていました。

```main.go
    // 一部抜粋
    // ここの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 でドハマリした。](https://mattn.kaoriya.net/software/lang/go/20180124171404.htm)

簡単ではありますが、以上が`rakyll/gotest`が行っている処理でした。
余談ですがソースコードを読んでみて`io.Pipe()`に関して勉強になりました。

## 終わりに
issueにもありますが`t.Skip()`した時にswitchの条件に一致せず直前のループで処理した色を出力してしまう実装になっているので、Skip用の色付けを行う修正PR送って見るのもありかもしれません。
と思ったら[PR](https://github.com/rakyll/gotest/pull/11)あった...

## 参考
- [io.Pipe関数の２つのdeadlockポイント](https://qiita.com/iwaaya/items/59a51706644a6b86b5d6)
- [Golangでのstreamの扱い方を学ぶ](https://christina04.hatenablog.com/entry/2017/01/06/190000)
- [goroutine でドハマリした。](https://mattn.kaoriya.net/software/lang/go/20180124171404.htm)

## go testの出力をリッチにしてくれるツールは色々あるよ
- https://github.com/kyoh86/richgo
- https://github.com/izumin5210/cgt