Fork me on GitHub

cob

モチベーション

  • Go で cli を作りたい

  • 軽量かつ良質な cli ツールのコードを読むことで cli の作り方のエッセンスを学ぶ

  • Go の書き方のエッセンスを学ぶ

どんなツールなのか

コミット前後でベンチマークが悪化していたらテストを落とすGoのCI用ツール に書いてある。シュッと作ってあるのでソード全体が読める。

コードリーディング

コミット履歴を置いながらコードを追ってみることにする。コミット履歴を追わなくても、シンプルに master の main.go を読めばよい、という話でもある。

cli は urfave/cli を使うと良いっぽい。他にもよく見る。こんな感じ。

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/urfave/cli/v2"
)

func main() {
    app := &cli.App{
        Name:  "cob",
        Usage: "Continuous Benchmark for Go project",
        Action: func(c *cli.Context) error {
            fmt.Println("hello world")
            return nil
        },
    }

    err := app.Run(os.Args)
    if err != nil {
        log.Fatal(err)
    }
}
// hello world

https://play.golang.org/p/bbcLV5goqE6


実際に cli で動かしたいロジックを記述している

func run(c *cli.Context) error {
    args := []string{"test", "-bench"}
    args = append(args, c.Args().Slice()...)
    out, err := exec.Command("go", args...).Output()
    if err != nil {
        return err
    }

    b := bytes.NewBuffer(out)
    s, err := parse.ParseSet(b)
    if err != nil {
        return err
    }
    fmt.Println(s)
    return err
}

初期化時に config する項目を構造体にして別ファイルとして作成

config.go
type config struct {
    args           []string
    onlyDegression bool
    threshold      float64
    bench          string
    benchmem       bool
    benchtime      string
}

func newConfig(c *cli.Context) config {
    return config{
        args:           c.Args().Slice(),
        onlyDegression: c.Bool("only-degression"),
        threshold:      c.Float64("threshold"),
        bench:          c.String("bench"),
        benchmem:       c.Bool("benchmem"),
        benchtime:      c.String("benchtime"),
    }
}

フラグとして入力時に input する項目の設定

main.go
Flags: []cli.Flag{
    &cli.BoolFlag{
        Name:  "only-degression",
        Usage: "Show only benchmarks with worse score",
    },
    &cli.Float64Flag{
        Name:  "threshold",
        Usage: "The program fails if the benchmark gets worse than the threshold",
        Value: 0.1,
    },
    &cli.StringFlag{
        Name:  "bench",
        Usage: "Run only those benchmarks matching a regular expression.",
        Value: ".",
    },
    &cli.BoolFlag{
        Name:  "benchmem",
        Usage: "Print memory allocation statistics for benchmarks.",
    },
    &cli.StringFlag{
        Name:  "benchtime",
        Usage: "Run enough iterations of each benchmark to take t, specified as a time.Duration (for example, -benchtime 1h30s).",
        Value: "1s",
    },

テストは stretchr/testify を使っているみたい。assert が便利ということだろうか。(使ったことがないのでわからない)

結果を出力するときに、最初は fmt.Println("...") を使っておいて、後から fmt.Fprintln(w, "...") とするのはリファクタリングのテクとしてありそう。呼び出し側で os.Stdout を渡せば同じ動作になる。

main.go
-func showResult(rows [][]string, benchmem bool) {
-    fmt.Println("\nResult")
-    fmt.Printf("%s\n\n", strings.Repeat("=", 6))
+func showResult(w io.Writer, rows [][]string, benchmem bool) {
+    fmt.Fprintln(w, "\nResult")
+    fmt.Fprintf(w, "%s\n\n", strings.Repeat("=", 6))

// ..

-showResult(rows, c.benchmem)
+showResult(os.Stdout, rows, c.benchmem)