リポジトリごとにGOPATHを切る環境でのvim-go

↓のようなリポジトリごとにその直下をGOPATHにする環境でGoを書く時、

.
├── github.com/daisuzu/bar
│     ├── bin
│     ├── pkg
│     └── src
│         └── bar
│              └── main.go
└── github.com/daisuzu/foo
       ├── bin
       ├── pkg
       └── src
           └── foo
                └── main.go

両方のリポジトリのファイルを同時に開いて行ったり来たりするのを少し楽にするためにvim-goの設定を切り替える関数を作ることにした。

function! SwitchRepo()
  let toplevel = trim(system('git rev-parse --show-toplevel'))
  if toplevel =~# '^fatal'
    return
  endif

  " GOPATHをリポジトリ直下に変更する
  execute 'GoPath ' . toplevel

  " リポジトリ名をgoimportsの'-local'フラグに渡す
  let g:go_fmt_options = { 'goimports': '-local ' . fnamemodify(toplevel, ':t') }
endfunction

*1

これで他のリポジトリのファイルを開いたらcdした後にcall SwitchRepo()すれば:GoDefでちゃんと飛べるし、
:GoImportsした時もサブパッケージを標準パッケージとは異なるグループにしてくれる。

  • Before
import (
    "bar/handler"
    "bar/model"
    "fmt"
)
  • After
import (
    "fmt"

    "bar/handler"
    "bar/model"
)

autocmdを組み合わせればもっと便利になるかもしれないけど、今のところは必要ない。

*1:普段はg:go_fmt_optionsを使っていないので都度上書き

図のライブプレビュー環境を改善した

PlantUMLで図を書く時もblockdiagと同じようにやろうかなと思ったけど、markdown作ったりpandocのテンプレートを準備するの面倒になったのでその部分をやってくれるサーバを作ってみた。

github.com

  1. go get github.com/daisuzu/liveimgしてから
  2. goemon liveimgで立ち上げたら
  3. ブラウザでhttp://localhost:8080/_liveimg/uml.pngを開けばOK
    • /_liveimg/以降はカレントディレクトリにある任意のイメージ

Goで時刻が絡む単体テストをどうするか考える

Goにはtimeパッケージをmockする仕組みが用意されていない。
そのため、time.Nowを以下のようにして使っているコードを見かけることがある。

var Now = func() time.Time { return time.Now() }

たしかにこうすることでNow()が返す値を自由に設定でき、単体テストが書きやすくなる。
ただ、関数を変数に入れるということは「その関数を処理の中で書き換えるため」という意味を持たせることになり、それをテストでしか書き換えないのであれば読み手に無用な混乱を与えてしまいそうで個人的にはあまり好きではない。

その辺りの意識が開発者の間で統一されていればそれで良いのかもしれないが、新しく入った人なんかはそうもいかないはず。
さすがに意図せず書き換えてトラブってしまうことはまず無いだろうが、自分がよく使いそうな用途でこのような使い方を避けられないか考えてみようと思う。

時刻を含むデータが返ってくる処理

例えばタイムスタンプを付与したリソースを作成するような関数。

type Resource struct {
    Data       string
    CreateTime time.Time
}

func NewResource(data string) *Resource {
    return &Resource{
        Data:       data,
        CreateTime: time.Now(),
    }
}

当然NewResource()のテストでreflect.DeepEqualを使うと通らない。

func TestNewResource(t *testing.T) {
    want := &Resource{Data: "test", CreateTime: time.Now()}

    got := NewResource("test")
    if !reflect.DeepEqual(got, want) {
        // got.CreateTimeとwant.CreateTimeが一致しない
        t.Errorf("NewResource() = %v, want %v", got, want)
    }
}

ただ、ここでCreateTimeの値を確認したいかというと必ずしもそうではない。
なので不要なフィールドは除外して比較する、で十分なケースがほとんどなはず。

func resetFields(r *Resource) *Resource {
    // 元の値は変えず、CreateTimeだけゼロ値にしたコピーを作る
    after := *r
    after.CreateTime = time.Time{}
    return &after
}

func TestNewResource_reset(t *testing.T) {
    want := &Resource{Data: "test"}

    got := NewResource("test")
    after := resetFields(got)
    if !reflect.DeepEqual(after, want) {
        t.Errorf("NewResource() = %v, want %v", after, want)
    }
}

毎回resetFields()のような関数を作るのが面倒であればgo-cmpを使うとcmpopts.IgnoreFieldsに除外するフィールドを設定するだけになるので簡単だ。

func TestNewResource_ignore(t *testing.T) {
    want := &Resource{Data: "test"}
    opt := cmpopts.IgnoreFields(Resource{}, "CreateTime")

    got := NewResource("test")
    if !cmp.Equal(got, want, opt) {
        t.Errorf("NewResource() = %v, want %v", got, want)
    }
}

どうしてもtime.Nowが使われていていることを確認したければ、それはテストではなく静的解析をしてチェックしてあげれば良いんじゃないだろうか。

ということでtime.Nowを書き換える必要は無さそう。

時刻を使用した条件判定がある処理

例えば以下のような関数。

// GetResource は指定されたidのResourceを返す。Resourceの取得に失敗、
// もしくは取得したResourceの有効期限が切れていた場合はエラーを返す。
func (s *service) GetResource(id int64) (*Resource, error) {
    res, err := s.store.Get(id)
    if err != nil {
        return nil, err
    }

    if time.Now().After(res.ExpireTime) {
        return nil, ErrExpired
    }

    return res, nil
}

こういったものは時刻使った判定処理を関数やメソッドに切り出し、その部分を単体でテストできるようにしてしまう。

func IsExpired(r *Resource, t time.Time) bool {
    return t.After(r.ExpireTime)
}

// or

func (r *Resource) IsExpired(t time.Time) bool {
    return t.After(r.ExpireTime)
}

正直GetResource()がエラーを返す場合のテストケースを網羅するよりも、呼び出し元がそのエラーハンドリングを正しく実装できているかを確認する方がよっぽど大事だと思っている。
呼び出し元は次のようなREST APIのハンドラだったりするのでserviceはモックなどにしてテストすることになり、そうなるとtime.Nowを書き換える必要は無くなる。

func (h handler) Get(w http.ResponseWriter, r *http.Request) {
    id, err := getIDFromPath(r.URL.Path)
    if err != nil {
        http.NotFound(w, r)
        return
    }

    res, err := h.service.GetResource(id)
    if err != nil {
        if err == ErrExpired {
            http.NotFound(w, r)
        } else {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
        return
    }

    json.NewEncoder(w).Encode(res)
}

以前から気にはなっていたけど、そういうパッケージやTesting in Go by example: Part 5にあるようなパターンを見てもあまりピンと来ず、
色々な人に聞いてみても「time.Nowを変数に入れるのは仕方ない」という意見ばかりだったのでちょっと考えてみた。

  • 「引数で渡そうとしても結局はどこかでtime.Nowを呼ばなきゃいけなくなる」
    • time.Nowを書き換える必要がなければ良いのでは?
  • 「引数にtime.Timeを渡したくない」
    • 好みの問題ではあるけど、そこまで不自然じゃなければ良いのでは?
    • 渡されたtを基に○○する関数 なんかはそんなに違和感なさそう
  • 「都合によりどうしてもテストしなければならないが、他に良い方法が無い」
    • そういう場合はたしかに
  • 「そもそも何が悪いのかわからない」
    • ...

複雑なケースになってくるとどうなるかわからないけど、うまいこと設計できればなんとかなりそうな気がするのでどこかで試してみたい。

Meguro.vim #8 に行ってきた

Meguro.vim #8 で久しぶりにvimrcを整理し、不要なプラグインや設定なんかを削ったら300行くらい短くすることができた。
ついでにリファクタリングをして、次のような任意のEXコマンドの結果を新しいバッファに表示するコマンド*1を作ってみた。
(今までは特定のEXコマンドに限定していたものを使っていた)

command! -nargs=1 -complete=command L
      \ <mods> new
      \ | setlocal buftype=nofile bufhidden=hide noswapfile
      \ | call setline(1, split(execute(<q-args>), '\n'))

:scriptnamesとか:buffers用のつもりだったけど、ちょうど同じタイミングで開催された Osaka.vim #12@koturn さんが発表しているのを見て、:registers:marksで使っても良さそうだなと思った。

*1:実際はもう少し色々とやっている

gRPCでFirestoreをListenする

昨年の10月にBetaリリースされたCloud Firestoreですが、公式ライブラリによるデータのリアルタイム更新は

しかサポートされていないようです。*1

GAまで待てば良いのでしょうが、今でもgRPCのListenは使えるのため、こちらを利用することでリアルタイム更新の取得が可能です。

Protocol Buffersの定義はGitHubで公開されているのでGoだと↓のようにクライアント(とサーバ)のコードを生成することができます。

github.com

Go以外でもgRPCに対応していれば良く、試しにcppでやってみたのがこちらです。

github.com

  • ListenRequestのフィールドは全て決め打ち
  • firestore.googleapis.comへの認証は面倒だったのでgcloud auth loginで
  • 一応テスト用のGo製サーバも同梱
    • Go + gRPCで簡単にサーバが作れてイイ感じ

*1:2018年1月現在

2017年の振り返り

年末にやったこと無かったけど、たまには振り返ってみる。

今年は転職活動から始まった年だった。
それから初めて勉強会で発表してきたけど、転職したことがきっかけだったと思う。
ちょっと上手く説明できないけど...

喋ってきたのは

  1. Fablic.vim #2
    • tree.vimについて(飛び入り発表)
  2. golang.tokyo #8
  3. golang.tokyo #9
  4. VimConf 2017

で、次はDeNA TechCon 2018になるのかな。

Vimのカバレッジを見る

この記事はVim Advent Calendar 2017の2日目の記事です。

Vimカバレッジcoveralls.iocodecov.io上で見ることができますが、手元で見たくなることもあると思います。

そんな時はMakefileに書いてある通り、次のようにしてカバレッジを計測し、生成されたhtmlから見ることができます。

# Vimのリポジトリ直下に移動

# 各種フラグに--coverageをつけてビルド、他はお好みで
CFLAGS=--coverage LDFLAGS=--coverage ./configure --with-features=huge && make

cd ./src

# まずはゼロカバレッジの初期データを作る
lcov -c -i -b . -d objects -o objects/coverage_base.info

# テストを走らせてカバレッジ情報を作る
make test
lcov -c -b . -d objects/ -o objects/coverage_test.info

# 初期データとテストのカバレッジ情報を結合する
lcov -a objects/coverage_base.info -a objects/coverage_test.info -o objects/coverage_total.info

# 結果をobjects/index.htmlとして生成する
genhtml objects/coverage_total.info -o objects

ビルド時に--coverageをつけることでgcovが有効になり、実行された行が記録されるようになります。
lcov(と付属のgenhtml)はそのフロントエンドツールです。

さて、実行された行が記録されるということは、Vimで特定の操作をした際にどの関数が呼ばれたのか調べることができる、ということです。

しかし、gcovはプログラムの終了直前に保存処理を行うため、目的の操作だけの結果を知るには

  1. Vimの起動
  2. コマンドやキー入力
  3. Vimの終了

の順で操作をしつつ、1と3は除外する必要があります。
幸いgenhtmlには-b(--baseline-file)オプションがあるので次のようにすれば実現できます。

# 上と同じ
lcov -c -i -b . -d objects -o objects/coverage_base.info

# 起動と終了のカバレッジを取得
VIMRUNTIME=../runtime ./vim --clean -c 'q'
lcov -c -b . -d objects/ -o objects/coverage_quit.info
lcov -a objects/coverage_base.info -a objects/coverage_quit.info -o objects/coverage_baseline.info

# カバレッジをリセット
lcov -z -d objects/

# 起動と:smileと終了のカバレッジを取得
VIMRUNTIME=../runtime ./vim --clean -c 'smile | q'
lcov -c -b . -d objects/ -o objects/coverage_smile.info
lcov -a objects/coverage_base.info -a objects/coverage_smile.info -o objects/coverage_result.info

# 生成されるのはcoverage_result.infoからcoverage_baseline.info分のカウントを減らした結果
genhtml -b objects/coverage_baseline.info objects/coverage_result.info -o objects

というわけで:smile*1にはsyntax.c以下のソースが使われていることがわかりました。

f:id:daisuzu:20171202170437p:plain

処理を正確に追うにはデバッガを使うのが確実だとは思いますが、ソースの構造がある程度わかっていないと難しかったりもします。

f:id:daisuzu:20171202173329p:plain

なので、さらっと概要と知りたい時やソースコードリーディングのお供にでも是非カバレッジを活用してみてください。

:smile
                            oooo$$$$$$$$$$$$oooo
                        oo$$$$$$$$$$$$$$$$$$$$$$$$o
                     oo$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o         o$   $$ o$
     o $ oo        o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o       $$ $$ $$o$
  oo $ $ "$      o$$$$$$$$$    $$$$$$$$$$$$$    $$$$$$$$$o       $$$o$$o$
  "$$$$$$o$     o$$$$$$$$$      $$$$$$$$$$$      $$$$$$$$$$o    $$$$$$$$
    $$$$$$$    $$$$$$$$$$$      $$$$$$$$$$$      $$$$$$$$$$$$$$$$$$$$$$$
    $$$$$$$$$$$$$$$$$$$$$$$    $$$$$$$$$$$$$    $$$$$$$$$$$$$$  """$$$
     "$$$""""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     "$$$
      $$$   o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     "$$$o
     o$$"   $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$       $$$o
     $$$    $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" "$$$$$$ooooo$$$$o
    o$$$oooo$$$$$  $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$   o$$$$$$$$$$$$$$$$$
    $$$$$$$$"$$$$   $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     $$$$""""""""
   """"       $$$$    "$$$$$$$$$$$$$$$$$$$$$$$$$$$$"      o$$$
              "$$$o     """$$$$$$$$$$$$$$$$$$"$$"         $$$
                $$$o          "$$""$$$$$$""""           o$$$
                 $$$$o                                o$$$"
                  "$$$$o      o$$$$$$o"$$$$o        o$$$$
                    "$$$$$oo     ""$$$$o$$$$$o   o$$$$""
                       ""$$$$$oooo  "$$$o$$$$$$$$$"""
                          ""$$$$$$$oo $$$$$$$$$$
                                  """"$$$$$$$$$$$
                                      $$$$$$$$$$$$
                                       $$$$$$$$$$"
                                        "$$$""""

Press ENTER or type command to continue

*1:v8.0.1362