いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-243 (flag.FlagSetを使う)

概要

以下、よく忘れるので自分用のメモです。

Goには標準ライブラリにアプリケーション引数を処理してくれる flag パッケージがあります。

flag package - flag - Go Packages

通常は以下のように

opt1 := flag.Int("opt1", 100, "option 1")
flag.Parse()

すると、勝手にコマンドライン引数を処理してくれてとっても便利。

ですが、たまにユニットテストでも使えるようにしたい場合とかがあります。

そんなときに flag.FlagSet を利用すると便利です。

以下、サンプル。

サンプル

普通にflagパッケージを利用

package main

import (
    "flag"
    "fmt"
)

func main() {
    var (
        a = flag.Int("a", 0, "")
        b = flag.String("b", "", "")
        c = flag.Bool("c", false, "")
    )

    flag.Parse()

    fmt.Printf("a=%v\tb=%v\tc=%v\n", *a, *b, *c)
}

flag.Parse() を呼び出すと自動で os.Args[1:] の値を使ってパースしてくれます。

flag.FlagSet を利用

package main

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

func main() {
    var (
        flags = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
        a     = flags.Int("a", 0, "")
        b     = flags.String("b", "", "")
        c     = flags.Bool("c", false, "")
    )

    flags.Parse(os.Args[1:])

    fmt.Printf("a=%v\tb=%v\tc=%v\n", *a, *b, *c)
}

flag.FlagSet の Parse メソッドは、自分で値を渡す必要があります。

ユニットテストを書く場合はこっちの方が便利です。

ユニットテスト

以下のような処理があるとして

package args

import (
    "flag"
    "io"
)

type (
    Value struct {
        A int
        B string
        C bool
    }
)

var (
    Empty = &Value{0, "", false}
    Error = &Value{1, "error", false}
)

func Parse(options []string) (*Value, error) {
    if len(options) == 0 {
        return Empty, nil
    }

    var (
        flags = flag.NewFlagSet("command", flag.ContinueOnError)
        a     = flags.Int("a", 0, "")
        b     = flags.String("b", "", "")
        c     = flags.Bool("c", false, "")
    )
    flags.SetOutput(io.Discard)

    err := flags.Parse(options)
    if err != nil {
        return Error, err
    }

    return &Value{*a, *b, *c}, nil
}

それのユニットテストを以下のようにかけます。

package args_test

import (
    "reflect"
    "strconv"
    "testing"

    args "github.com/devlights/try-golang/examples/singleapp/flag_pkg/unittest"
)

func TestArgs(t *testing.T) {
    tests := []struct {
        in  []string
        out *args.Value
    }{
        {[]string{}, args.Empty},
        {[]string{"-a"}, args.Error},
        {[]string{"-a", "-b"}, args.Error},
        {[]string{"-a", "-b", "-c"}, args.Error},
        {[]string{"-a", "1"}, &args.Value{1, "", false}},
        {[]string{"-a", "1", "-b", "hello"}, &args.Value{1, "hello", false}},
        {[]string{"-a", "1", "-b", "hello", "-c"}, &args.Value{1, "hello", true}},
        {[]string{"-a", "1", "-b", "hello", "-c", "true"}, &args.Value{1, "hello", true}},
        {[]string{"-a", "bbb", "-b", "hello", "-c"}, args.Error},
        {[]string{"-a", "bbb", "-b", "hello", "-c", "world"}, args.Error},
    }

    for i, tt := range tests {
        tt := tt
        t.Run(strconv.Itoa(i), func(t *testing.T) {
            sut, err := args.Parse(tt.in)
            if !reflect.DeepEqual(tt.out, sut) {
                t.Errorf("[want] %v\t[got] %v (%v)", tt.out, sut, err)
            }
        })
    }
}

ついでに fuzz テスト

上記のようなパターンは fuzzing しやすいのでついでに用意

// # REFERENCES
//   - https://future-architect.github.io/articles/20220214a/
//   - https://qiita.com/s9i/items/de45b820aaeb6597c9a2
package args_test

import (
    "strconv"
    "testing"

    args "github.com/devlights/try-golang/examples/singleapp/flag_pkg/unittest"
)

func FuzzArgs(f *testing.F) {
    f.Add(1, "hello")
    f.Add(10, "helloworld")
    f.Add(100, "こんにちは世界")
    f.Add(-1, "こんにちは世界")

    f.Fuzz(func(t *testing.T, i int, s string) {
        options := []string{"-a", strconv.Itoa(i), "-b", s, "-c"}

        sut, err := args.Parse(options)
        if sut == args.Error {
            t.Errorf("i=%v, s=%v, err=%v", i, s, err)
        }
    })
}

Taskfile.yml

version: "3"

tasks:
  default:
    cmds:
      - task: run-normal
      - task: run-flagset
      - task: run-unittest
      - task: run-fuzztest
  run-normal:
    dir: normal
    cmds:
      - go run main.go -a=1 -b=hello --c
  run-flagset:
    dir: flagset
    cmds:
      - go run main.go -a=1 -b=hello --c
  run-unittest:
    dir: unittest
    cmds:
      - go test -count=1 .
  run-fuzztest:
    dir: unittest
    cmds:
      - go test -fuzz . -fuzztime=5s

実行すると以下のようになります。

gitpod /workspace/try-golang (master) $ task -d examples/singleapp/flag_pkg/
task: [run-normal] go run main.go -a=1 -b=hello --c
a=1     b=hello c=true
task: [run-flagset] go run main.go -a=1 -b=hello --c
a=1     b=hello c=true
task: [run-unittest] go test -count=1 .
ok      github.com/devlights/try-golang/examples/singleapp/flag_pkg/unittest    0.002s
task: [run-fuzztest] go test -fuzz . -fuzztime=5s
fuzz: elapsed: 0s, gathering baseline coverage: 0/4 completed
fuzz: elapsed: 0s, gathering baseline coverage: 4/4 completed, now fuzzing with 16 workers
fuzz: elapsed: 3s, execs: 318426 (106116/sec), new interesting: 2 (total: 6)
fuzz: elapsed: 5s, execs: 536648 (103307/sec), new interesting: 2 (total: 6)
PASS
ok      github.com/devlights/try-golang/examples/singleapp/flag_pkg/unittest    5.119s

上記のサンプルは以下にアップしています。ご参考まで。

try-golang/examples/singleapp/flag_pkg at master · devlights/try-golang · GitHub

参考情報

future-architect.github.io

Go言語による並行処理

Go言語による並行処理

Amazon


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。