リポジトリごとに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
これで他のリポジトリのファイルを開いたら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のテンプレートを準備するの面倒になったのでその部分をやってくれるサーバを作ってみた。
go get github.com/daisuzu/liveimg
してからgoemon liveimg
で立ち上げたら- ブラウザで
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
で使っても良さそうだなと思った。
発表資料です.
— koturn@きらら女子大学 (@koturn) 2018年2月17日
Slide (reveal.js): https://t.co/MV2OkfTd9e
GFM: https://t.co/q6gdqrmdat#osakavim
*1:実際はもう少し色々とやっている
gRPCでFirestoreをListenする
昨年の10月にBetaリリースされたCloud Firestoreですが、公式ライブラリによるデータのリアルタイム更新は
- Web(Javascript)
- Node.js
- iOS(Swift or Objective-C)
- Android
しかサポートされていないようです。*1
GAまで待てば良いのでしょうが、今でもgRPCのListenは使えるのため、こちらを利用することでリアルタイム更新の取得が可能です。
Protocol Buffersの定義はGitHubで公開されているのでGoだと↓のようにクライアント(とサーバ)のコードを生成することができます。
Go以外でもgRPCに対応していれば良く、試しにcppでやってみたのがこちらです。
- ListenRequestのフィールドは全て決め打ち
- firestore.googleapis.comへの認証は面倒だったのでgcloud auth loginで
- 一応テスト用のGo製サーバも同梱
- Go + gRPCで簡単にサーバが作れてイイ感じ
*1:2018年1月現在
2017年の振り返り
年末にやったこと無かったけど、たまには振り返ってみる。
今年は転職活動から始まった年だった。
それから初めて勉強会で発表してきたけど、転職したことがきっかけだったと思う。
ちょっと上手く説明できないけど...
喋ってきたのは
- Fablic.vim #2
- tree.vimについて(飛び入り発表)
- golang.tokyo #8
- golang.tokyo #9
- VimConf 2017
で、次はDeNA TechCon 2018になるのかな。
Vimのカバレッジを見る
この記事はVim Advent Calendar 2017の2日目の記事です。
Vimのカバレッジはcoveralls.ioやcodecov.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と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
以下のソースが使われていることがわかりました。
処理を正確に追うにはデバッガを使うのが確実だとは思いますが、ソースの構造がある程度わかっていないと難しかったりもします。
なので、さらっと概要と知りたい時やソースコードリーディングのお供にでも是非カバレッジを活用してみてください。
: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