GoでDBのテストにgithub.com/cockroachdb/copyistを使う
久しぶりにMySQLを使ったシステムを触ることになったものの、現状のテストが遅すぎたので高速化に取り組むことにした。
遅い原因としては、
- テストでDBを使うパッケージが多いので接続時間がそれなりになってしまう
- テストケースごとにTRUNCATE→INSERTでデータを入れ直している
といったところ。
なので今回はDBにアクセスしないでテストができるようになるcopyistを試してみようと思う。
仕組みとしては実行されたSQLを記録しておき、以降はその時のデータを使うという
などと似たようなものとなる。
README.mdにはMySQLは非対応と書いてあったが、普通に使うことができた。
(もしかしたら一部正しく記録/再生できないクエリがあるのかもしれない)
MySQL版のサンプルコードはこちら。
mysql_test.go
package example import ( "database/sql" "log" "testing" "github.com/cockroachdb/copyist" _ "github.com/go-sql-driver/mysql" ) // GetName をテストする。 func GetName(db *sql.DB, id int64) (string, error) { var name string err := db.QueryRow("SELECT name FROM `user` WHERE id=?", id).Scan(&name) return name, err } // resetDBで使うためにグローバル変数にしておく。 var db *sql.DB // 記録時にcopyist.Open()の中で実行される。 func resetDB() { db.Exec("DROP TABLE `user`") db.Exec("CREATE TABLE `user` (`id` INT(11) unsigned NOT NULL AUTO_INCREMENT, `name` VARCHAR(20) NOT NULL, PRIMARY KEY (`id`))") db.Exec("INSERT INTO `user` (id, name) VALUES (?, ?)", 1, "Andy") } func TestMain(m *testing.M) { // ドライバ名は"copyist_mysql"になる。 copyist.Register("mysql", resetDB) // resetDBとテストで使うdbを作る。 var err error db, err = sql.Open("copyist_mysql", "admin:pass@/copyist") if err != nil { log.Fatal(err) } defer db.Close() m.Run() } func TestGetName(t *testing.T) { defer copyist.Open(t).Close() // found name, err := GetName(db, 1) if err != nil { t.Fatal(err) } if name != "Andy" { t.Error("failed test") } // not found if _, err := GetName(db, 100); err != sql.ErrNoRows { t.Error(err) } } // subtest版 func TestGetName_subtest(t *testing.T) { t.Run("found", func(t *testing.T) { defer copyist.Open(t).Close() name, err := GetName(db, 1) if err != nil { t.Fatal(err) } if name != "Andy" { t.Error("failed test") } }) t.Run("not found", func(t *testing.T) { defer copyist.Open(t).Close() if _, err := GetName(db, 100); err != sql.ErrNoRows { t.Error(err) } }) }
1回目は以下のように -record
をつけてテストを実行する。
$ go test -v -record === RUN TestGetName --- PASS: TestGetName (0.09s) === RUN TestGetName_subtest === RUN TestGetName_subtest/found === RUN TestGetName_subtest/not_found --- PASS: TestGetName_subtest (0.12s) --- PASS: TestGetName_subtest/found (0.06s) --- PASS: TestGetName_subtest/not_found (0.06s) PASS
そうするとtestdataディレクトに以下のようなmysql_test.copyistが生成される。
1=DriverOpen 1:nil 2=ConnPrepare 2:"SELECT name FROM `user` WHERE id=?" 1:nil 3=StmtNumInput 3:1 4=StmtQuery 1:nil 5=RowsColumns 9:["name"] 6=RowsNext 11:[10:QW5keQ] 1:nil 7=RowsNext 11:[] 7:EOF "TestGetName"=1,2,3,4,5,6,2,3,4,5,7 "TestGetName_subtest/found"=1,2,3,4,5,6 "TestGetName_subtest/not_found"=1,2,3,4,5,7
この状態で -record
をつけずに実行すると↑のデータが使われる。
$ go test -v === RUN TestGetName --- PASS: TestGetName (0.00s) === RUN TestGetName_subtest === RUN TestGetName_subtest/found === RUN TestGetName_subtest/not_found --- PASS: TestGetName_subtest (0.00s) --- PASS: TestGetName_subtest/found (0.00s) --- PASS: TestGetName_subtest/not_found (0.00s) PASS
なお、記録する前に -record
無しで実行するとpanicになる。
$ go test -v === RUN TestGetName --- FAIL: TestGetName (0.00s) panic: no recording exists with this name: TestGetName [recovered] panic: no recording exists with this name: TestGetName
また、以下のようにクエリを変えて、
--- a/mysql_test.go +++ b/mysql_test.go @@ -12,7 +12,7 @@ import ( // GetName をテストする。 func GetName(db *sql.DB, id int64) (string, error) { var name string - err := db.QueryRow("SELECT name FROM `user` WHERE id=?", id).Scan(&name) + err := db.QueryRow("SELECT name FROM `user` WHERE id=? AND TRUE", id).Scan(&name) return name, err }
記録し直さずにそのまま実行してもpanicになる。
$ go test -v === RUN TestGetName --- FAIL: TestGetName (0.00s) panic: mismatched argument to ConnPrepare, expected SELECT name FROM `user` WHERE id=? AND TRUE, got SELECT name FROM `user` WHERE id=? - regenerate recording [recovered] panic: mismatched argument to ConnPrepare, expected SELECT name FROM `user` WHERE id=? AND TRUE, got SELECT name FROM `user` WHERE id=? - regenerate recording
ただし、プレースホルダーの値が変わっても返ってくるレコードは変わらないのでその点は注意が必要。
例えば以下のようにidを逆にしてもテストはPassしてしまう。
--- a/mysql_test.go +++ b/mysql_test.go @@ -45,7 +45,7 @@ func TestGetName(t *testing.T) { defer copyist.Open(t).Close() // found - name, err := GetName(db, 1) + name, err := GetName(db, 100) if err != nil { t.Fatal(err) } @@ -54,7 +54,7 @@ func TestGetName(t *testing.T) { } // not found - if _, err := GetName(db, 100); err != sql.ErrNoRows { + if _, err := GetName(db, 1); err != sql.ErrNoRows { t.Error(err) } } @@ -63,7 +63,7 @@ func TestGetName_subtest(t *testing.T) { t.Run("found", func(t *testing.T) { defer copyist.Open(t).Close() - name, err := GetName(db, 1) + name, err := GetName(db, 100) if err != nil { t.Fatal(err) } @@ -75,7 +75,7 @@ func TestGetName_subtest(t *testing.T) { t.Run("not found", func(t *testing.T) { defer copyist.Open(t).Close() - if _, err := GetName(db, 100); err != sql.ErrNoRows { + if _, err := GetName(db, 1); err != sql.ErrNoRows { t.Error(err) } })
細かい部分はまだ何とも言えないが、とりあえず使えそうだし高速化にも期待ができそう。
さて、あとはこれをどうやって既存のテストに組み込んでいくか。
既存のテストではxormが使われているが、copyistに対応していないので少し特殊な初期化をしないといけないようだ。
engine, err := xorm.NewEngine("mysql", "") // エラーハンドリングやengineの設定など // ... // DBをsql.Open("copyist_mysql", ...)したものと差し替える engine.DB().DB = db
これを何とかするのが一番大変なのかもしれない...
ISUCON10に初参戦してみた
ゴリラさんと出てみようと話していたものの、申し込みのタイミングを逃してオワタと思っていたら@inductorさんに拾ってもらい、カンガルーと犬とゴリラ
としてなんとかISUCON10に参加することができました。
当日は開始ちょっと前*1にDiscordとGoogleドキュメントでコミュニケーションすることを決め、 開始直後は
- 環境を整備する
- アプリを見る
- ドキュメントを見る
といった感じで役割分担。
30分くらい経って、これからはベンチマークを走らせながらどこから手を付けるか考えようと思っていましたがトラブルのためなかなか実行できず、New Relicの設定をしたあとはしばらくWebアプリをポチポチしたりホストの中をウロウロしたりしていました。
ベンチマークが走ってからは、
- 検索系
- インデックス
- DB分割
- 最終的にはApp1台 + DB2台に
- nazotte
- LIMITでループを抜ける
- それ以上のチューニングは結局できず...
- low_priced
- 静的に返したり、キャッシュしたり
- botからのアクセス
- nginxで弾く
をなんとかすることに。
これでスコアが480→805になったのがだいたい19:00で、そこから都合により20:20くらいまで離席...
戻ってきたタイミングで検索系がさらに最適化されてスコアは1280まで伸びましたが、最終的には1244で競技終了の21:00になりました。
以前に参加した社内ISUCONでは個人でもチームでもスコアアップに繋がるようなことがほとんどできなかったので*2、それに比べれば多少はできるようになったかもしれませんが、まだまだ力不足*3なことを痛感しました。。。
goplsのSymbolMatcherとSymbolStyleオプション
以下の記事で、workspace/symbol
のオプションは補完と同じmatcher
と紹介したが、現在のv0.4.4ではsymbolMatcher
とsymbolStyle
になっている。
daisuzu.hatenablog.com
まずはv0.4.1でmatcher
がsymbolMatcher
に変わり、それまでと同様、
- fuzzy
- caseSensitive
- caseInsensitive(デフォルト)
の3つが補完と独立して設定できるようになった。
その後、v0.4.4でsymbolStyle
が追加され、
- package(デフォルト)
- full
- dynamic
を指定できるようになった。
packageの場合は今までと同様、パッケージ名をクエリに含める際にはコードで使う時のようにcontext.Context
といった形式で検索する。
新たに追加されたfullではgolang.org/x/net/context.Context
のようにすることで同じパッケージ名の別モジュールを区別することが可能になった。
dynamicはpackage→fullの順番でマッチさせるため、パッケージ名を含んだクエリはpackageと同じ結果になり、インポートパスから指定したクエリはfullと同じ結果になる。
Goでprotobufの定義をimportせずにフィールドの値を使う
以下のようなコードでreq
にあるフィールドを使いたい場合、
opt := grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { // ここでreqから特定のフィールド値を取得したい return nil })
型が定義されているパッケージをインポートして、
if v, ok := req.(*pb.Req); ok { // v.Bodyでフィールドにアクセスできる }
のように変換することになる。
この時、インポートしたくない or インポートできないのであれば、protoc
が生成する定義にはGetterも同時に生成されるため、対象フィールドのGetterがあるinterfaceで型アサーションすると手軽にできる。
if v, ok := req.(interface{ GetBody() []byte }); ok { // v.GetBody()でフィールドの値を取得できる }
あまり使い所はないかもしれないが、google.golang.org/appengineは定義がinternal配下にあるため、例えばテストでWithAPICallFuncを使う際など、以下のようにしてtaskqueue.Addのリクエスト(TaskQueueAddRequest)からフィールドの値を取得できる。
var got []byte ctx = appengine.WithAPICallFunc(ctx, func(ctx context.Context, service, method string, in, out proto.Message) error { if service == "taskqueue" && method == "Add" { if v, ok := in.(interface{ GetBody() []byte }); ok { got = v.GetBody() } } return nil }) DoSomething(ctx) // 関数内でtaskqueue.Add()が呼ばれる if !reflect.DeepEqual(got, want) { t.Errorf("body = %q, want %q", got, want) }
なお、フィールドがインポートできない型になっているとinterfaceも作れないのでこの方法は使えない。
それでも必要な場合はencoding/json
パッケージでMarshal
してからmap[string]interface{}
や独自型にUnmarshal
すれば良い。
gopls(v0.4.0)の機能
goplsは更新頻度が高く、何ができるのかをちゃんと把握できていなかったので、LSPのメソッドベースで今の時点(v0.4.0)の機能をざっと調べてみることにしました。
補完(textDocument/completion)
入力中の変数や定数、関数といった各種定義などをbuiltinも含めて補完してくれます。
この機能のためにgopls
を使っているという人もけっこう多いはず。
基本的に文脈に合わせた候補を返したり、優先度が高くなるようになっていますが、
- おかしな候補が返ってくる
- 期待する候補が返ってこない
という場合はコントリビュート(issue報告や修正する)チャンスかもしれません。
例えば、
- スコープに存在しない定義は候補にならない
- スコープ外の変数など
- カーソル位置で使えないキーワードは候補にならない
iota
: const入力中のみrange
: for入力中のみbreak
: for、switch、select内continue
: for内- など
- 型が合わないものは候補から外されることがある
- 代入時
のような制御があります。
また、以下の場合は補完で入力される内容自体が変化します。
- 参照が要求される場合、値には
&
がつく
// vへの代入時、 var v *int i := 1 v = // iは&iになる // func f(i *int)を呼び出す際、 f( // iは&iに変換される
- 値が要求される場合、参照には
*
がつく
// iへの代入時、 var v *int var i int = // vは*vになる // func f(i int)を呼び出す際、 f( // vは*vに変換される
- 可変長引数の場合に
...
が展開される
func fn(v ...int) {} func do(v []int) { fn( // vはv...になる }
- 引数の型がわかる場合はその型が補完される
func(...[]int)
を呼び出す際は[]int{}
が候補になるvar _ []int = make()
の第1引数は[]int
が候補になる
ただし、プレースホルダー*1やスニペット*2が有効になっていないと変換されなかったり、候補として表示されません。
VS Codeはデフォルトで有効になっているはずですが、Vimの場合はプラグインによっては有効になっていないかもしれません。
そしてシグネチャを補完・展開する際にはこれらが必須となっているため、もしうまく動かない時は設定を確認してみましょう。
他にも、次の設定で補完の動作を変えることができます。
initializationOptions.matcher
- デフォルトは
"fuzzy"
なので多少違っている候補も返ってきますが、 "caseSensitive"
や"caseInsensitive"
に変更できます
- デフォルトは
initializationOptions.completeUnimported
- デフォルトは
true
なので未importのパッケージからも候補が返ってきますが、 false
で無効化できます
- デフォルトは
initializationOptions.deepCompletion
- デフォルトは
true
なので構造体のフィールドも候補として返ってきますが、 false
で無効化できます
- デフォルトは
// deepCompletion=trueだと type param struct{ i int } func fn(i int) {} func do(p param) { fn( // p.iが候補として表示される }
それ以外の特殊な動作としては、
if err != nil
のスニペット展開error
を返す関数内のみ
- 公開される
var
のgodoc補完- 次のバージョンでは
const
やfunc
、type
も対象になる
- 次のバージョンでは
があります。
ジャンプ系
自分の場合は補完よりこちらを多用しています。
ただ、definition
とtypeDefinition
は違いがわからないまま使っていました。
textDocument/definition
カーソル位置にあるシンボルの定義元にジャンプする時に使用します。
関数内の変数はその関数内で定義(宣言)された場所にジャンプします。
type myType struct{} func fn() { t := myType{} // ← t.method() // tが宣言されたのは1行上 }
コマンドラインからはgopls query definitions
で使えます。
※次のバージョンからquery
が不要になる
gopls query definition internal/lsp/definition.go:14:67
textDocument/typeDefinition
カーソル位置にあるシンボルの型定義にジャンプする時に使用します。
関数内の変数は実際の型のある場所にジャンプします。
type myType struct{} // ← func fn() { t := myType{} t.method() // tの型定義はfnの上にあるmyType }
textDocument/implementation
カーソル位置にあるシンボルの、
- インタフェースから型
- 型からインタフェース
にジャンプする時に使用します。
コマンドラインからはgopls implementation
で使えます。
gopls implementation internal/lsp/implementation.go:14:10
textDocument/references
カーソル位置にあるシンボルが参照されている(使われている)場所にジャンプする時に使用します。
コマンドラインからはgopls references
で使えます。
gopls references internal/lsp/references.go:14:18
textDocument/documentSymbol
現在開いているファイルのシンボル一覧を取得します。
結果はジャンプするためや、ファイルのアウトライン表示のためにも使われます。
コマンドラインからはgopls symbols
で使えます。
※CLIでうまく動かない? → https://go-review.googlesource.com/c/tools/+/232557
workspace/symbol
プロジェクト(リポジトリ本体と依存パッケージ)からシンボル一覧を検索します。
※LSPの仕様では依存パッケージは含まれないはずですが、その方が便利なのでそういう実装になっています
今のところ結果は100件までに制限されています。
※一度に全部返してしまうとクライアントが高負荷で固まってしまうことがあるため
また、検索方法は補完と同じでinitializationOptions.matcher
に従います。
コマンドラインからはgopls workspace_symbol
で使えます。
gopls workspace_symbol WorkspaceSymbols
Vimからはtagfunc
経由でctags
のように使うこともできます。
設定例は下記参照。
daisuzu.hatenablog.com
その他
Diagnostic
以外はほとんど使っていませんでした。。。
しかし、今回調べたことでrename
がgorenameの代わりになるくらい完成度が高くなっていたり、SuggestedFix
の種類がだんだん増えていることがわかったのは大きな収穫でした。
Diagnostic
go vet
(golang.org/x/tools/go/analysis/passes/...
)やgofmt -s
、staticcheckなどのチェックを行います。
実行するチェッカーは以下のオプションでカスタマイズできます。
initializationOptions.analyses
initializationOptions.staticcheck
結果にSuggestedFixが含まれている場合、codeAction
経由で修正可能です。
SuggestedFix
の例:
return
の返り値の数があっていない時に追加したり、削除したり- 2回目以降の
:=
を=
に変更したり - 未定義変数の
<変数名> :=
を挿入したり
コマンドラインからはgopls check
で使えます。
gopls check internal/lsp/testdata/lsp/primarymod/analyzer/bad_test.go
textDocument/codeAction
SuggestedFix
の実行やimportの追加・削除(go.mod
への追加も含む)を行います。
コマンドラインからはgopls fix
やgopls imports
で使えます。
textDocument/codeLens
調べてもイマイチよくわかっていない機能ですが、現状は//go:generate
コメントのある行でgo generate
が実行できるよ、という情報を返してくれるようです。
また、go.modを開いている場合は依存パッケージのアップデートができるかどうかを教えてくれるようです。
textDocument/hover
カーソル位置のシグネチャや型などの情報を返してくれます。
textDocument/signatureHelp
カーソル位置のシグネチャやヘルプを返してくれます。
コマンドラインからはgopls signature
で使えます。
※<position>
の指定方法がわからず...は関数呼び出しの()
内の位置を指定する必要がある
gopls signature internal/lsp/signature_help.go:21:53
textDocument/documentHighlight
カーソル位置のシンボルが使われている場所をハイライトします。
コマンドラインからはgopls highlight
で使えます。
gopls highlight internal/lsp/highlight.go:17:2
textDocument/documentLink
現在開いているファイルがimportしているパッケージの https://pkg.go.dev へのリンクや、
<org>/<repo>#<number>
形式のコメントからGitHub Issuesへのリンクを返してくれます。
コマンドラインからはgopls links
で使えます。
gopls links internal/lsp/link.go
textDocument/formatting
現在開いているファイルを整形します。
コマンドラインからはgopls format
で使えます。
# -dでdiffを表示する gopls format -d internal/lsp/testdata/lsp/primarymod/format/bad_format.go.in
textDocument/rename
カーソル位置のシンボルをリネームします。
コマンドラインからはgopls rename
で使えます。
# -dでdiffを表示する gopls rename -d internal/lsp/rename.go:14:18 doRename
リネーム可能かどうかを調べるためにはtextDocument/prepareRenameで確認できます。
コマンドラインからはgopls prepare_rename
で使えます。
# rangeが表示されればリネーム可能
gopls prepare_rename internal/lsp/rename.go:14:18
textDocument/foldingRange
コードを折り畳む範囲を返してくれます。
コマンドラインからはgopls folding_ranges
で使えます。
gopls folding_ranges internal/lsp/folding_range.go
workspace/executeCommand
codeLens
の結果からgo generate
やgo get
の実行をしてくれるようです。
また、diagnostic
の結果からgo mod tidy
の実行をしてくれるようです。
未実装
以下のメソッドはまだ実装されていません。
GoでREST APIを呼ぶテストにhttp.FileServerを使う
例えばGET /users/:id
のようなAPIの場合、
testdata └── users └── 1
のように、testdata配下のusersディレクトリにidをファイル名としたJSONファイルを配置しておきます。
そうすると、
http.FileServer(http.Dir("testdata"))
で、testdata配下のパスとリクエストのパスが一致するファイルが
- 存在すれば
200 OK
でそのファイルの中身 - 存在しなければ
404 Not Found
を返すサーバが作れます。
http.Get(os.Getenv("API_URL") + "/users/" + strconv.FormatInt(id, 10))
のような呼び出し方をしていればhttptest
を使って次のようにテストが書けます。
package api import ( "net/http" "net/http/httptest" "os" "reflect" "testing" ) func Test_getUser(t *testing.T) { ts := httptest.NewServer(http.FileServer(http.Dir("testdata"))) defer ts.Close() if err := os.Setenv("API_URL", ts.URL); err != nil { t.Fatal(err) } defer os.Unsetenv("API_URL") type args struct { id int64 } tests := []struct { name string args args want *user wantErr bool }{ { name: "OK", args: args{id: 1}, want: &user{ID: 1, Name: "Alice"}, wantErr: false, }, { name: "NotFound", args: args{id: 2}, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := getUser(tt.args.id) if (err != nil) != tt.wantErr { t.Errorf("getUser() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("getUser() = %v, want %v", got, tt.want) } }) } }
POSTメソッドの場合、
- エンドポイントが
POST /users
のような形式になるのと、 201 Created
や500 Internal Server Error
が返せないため、
http.FileServerよりはhttp.HandlerFuncを使った方が良いでしょう。
また、リクエストパスとファイルが1対1にならない時も同様です。
packagestestのExported.Expectを使ってGoのソースからマーカーを収集する
golang.org/x/tools/go/packages/packagestestパッケージは静的解析など、Goのソースを読み込んで何かしらの処理を行うようなツールのテスト用にダミープロジェクトを作るためのパッケージです。
そういったツールを作る際、テストで期待する結果を完全に決め打ちで_test.go
に書いておいても良いかもしれませんが、パターンを増やす時にはダミーのソースとそれに対応する期待結果をそれぞれ追加していかなければいけないので、数が増えてくるとメンテナンスが大変になってしまいます。
そのため、ダミーのソースの中にマーカーを埋め込んでおいて、それを元に期待する結果をテスト時に組み立てるようにすると、パターンを追加する時の変更箇所が1つになり、またテストコード自体の見通しも良くなります。
具体例はgoplsのテストを見るのが良いですが、簡単にまとめると、
1) テスト関数の本体でpackagestest.TestAllを呼び、
func Test(t *testing.T) {
packagestest.TestAll(t, test)
}
2) test
の中でpackagestest.Exportを使ってダミープロジェクトをセットアップし、
// Moduleにはtestdataにあるモジュールを指定しても良い exported := packagestest.Export(t, e, []packagestest.Module{ { Name: "example.com/pkg", // モジュール名 Files: map[string]interface{}{ "foo/foo.go": `package foo const Foo = 100 //@Foo, aaa(Foo, "const") `, "bar/bar.go": `package bar import "example.com/pkg/foo" const Bar = foo.Foo * 10 //@bbb("Bar", Foo) `, }, }, }) defer exported.Cleanup()
3) その戻り値のExpectメソッドでマーカーを収集する、
if err := exported.Expect(map[string]interface{}{ // @aaa()用 "aaa": func(pos token.Pos, arg string) { t.Logf("pos = %v, arg = %v", pos, arg) }, // @bbb()用 "bbb": func(pos token.Pos, arg token.Pos) { t.Logf("pos = %v, arg = %v", pos, arg) }, }); err != nil { t.Fatal(err) }
という流れになります。
あとは収集したマーカーを使って期待する結果を作ってあげればOKです。
ただExpect
メソッドがどうやって位置に変換しているのか、少しわかりにくかったのでメモを残しておきます。
- マーカーの文字列から
token.Pos
に変換する場合
// OK: ↓をそのまま文字列にする const Foo = 100 //@aaa("Foo", "const") // NG: 同じ行にない場合はエラー //@aaa("Foo", "const") const Foo = 100
- マーカーの識別子から
token.Pos
に変換する場合
// OK: ↓識別子をマーカーにしておく const Foo = 100 //@Foo, aaa(Foo, "const") // どこかにマーカーがあれば別の場所でも使える const Bar = foo.Foo * 10 //@bbb("Bar", Foo) // NG: Barはマーカーになっていないのでエラー const Bar = foo.Foo * 10 //@bbb(Bar, Foo)
// NG: 識別子にドットは使えないのでfoo.Fooにはできない const Bar = foo.Foo * 10 //@bbb("Bar", foo.Foo) // OK: かわりにmarkで識別子の名前を"fooFoo"にしておくと、 const Foo = 100 //@mark("fooFoo", Foo) // 別の場所でfooFooが使えるようになる const Bar = foo.Foo * 10 //@bbb("Bar", fooFoo)
- 別のマーカーを流用して
token.Pos
に変換する場合
// ↓を type A string //@type("AString", "A") // ↓で識別子として使いたい type Alias = A //@bbb("Alias", AString)
Expect
でbbb
が呼ばれる前に、type
をMarkするExpect
を呼んでおく。
if err := exported.Expect(map[string]interface{}{ "type": func(name string, r packagestest.Range, _ []string) { exported.Mark(name, r) }, }); err != nil { t.Fatal(err) }
もし需要がありそうならゴリラ.Goのネタにするかもしれません。