Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AI時代を見据えたコードカバレッジ計測ツールの開発

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

 AI時代を見据えたコードカバレッジ計測ツールの開発

Avatar for Masaaki Goshima

Masaaki Goshima

February 23, 2026
Tweet

More Decks by Masaaki Goshima

Other Decks in Programming

Transcript

  1. Go Conference mini 2026 in Sendai 自己紹介 • goccy (

    ごっしー ) ◦ Twitter(新X): @goccy54 / GitHub: @goccy • 仙台市泉区出身 • Go リポジトリの総獲得 Star 数:10K+ ◦ 個人OSS: go-json / go-yaml / bigquery-emulator etc. ◦ 企業OSS: grpc-federation etc. • Go Conference mini 2022 in Sendai ◦ BigQueryエミュレータの作り方
  2. Go Conference mini 2026 in Sendai Agenda • Motivation ◦

    なぜ新しいコードカバレッジツールが必要だと思ったのか • Tobari ◦ AI時代を見据えたコードカバレッジツールの紹介 • How it Works ◦ どうやって作ったのか
  3. Go Conference mini 2026 in Sendai AI時代の開発と品質保証 AI (Coding Agentなど)

    により開発速度が上がっている一方、 その品質保証を人間が担う部分で律速になる課題がある • 大量のPRのレビュー / 人が行うQAプロセス 自動テストで品質保証する重要性が高まる 品質保証するために必要十分なテストとは? 課題 テストカバレッジが 100% に近いテストと定義 100%に近いコードカバレッジを 提供するテストを 効率よく生成したい
  4. Go Conference mini 2026 in Sendai Granularity ( 計測粒度 )

    の課題 • テスト全体で通った場所はわかるが、どのテストで通ったかはわからない TestA ( X% coverage ) TestB ( Y% coverage ) TestA + TestB = 100% coverage X is 0% or 30 % or 50% or 100% ….?
  5. Go Conference mini 2026 in Sendai Isolation ( 計測対象の隔離 )

    の課題 • runtime/coverage を使った go test 以外の計測を考える ◦ e.g.) HTTP / gRPC Server に対するリクエストによる計測 • テストと関係のないカバレッジを取るリスクがある 1 5 2 6 Coverage Request A Coverage Request B Normal Request Goroutines 3 4 gRPC or HTTP Server 数字: 処理順 6 を処理したあとに カバレッジを取得すると、 1~6 すべてが結果に含まれる 本当は 1と6の結果だけ欲しい
  6. Go Conference mini 2026 in Sendai Coding Agent がテストを生成する際の影響 •

    テストごとのカバー範囲が正確にわからないため、全体で 100% に近づける ようにテストケースを作成する • 同じカバー範囲のテストが大量に作られる可能性がある ◦ CI の時間・費用、レビューやメンテナンスのコストが増える CI Time Review Cost Maintenance Cost
  7. Go Conference mini 2026 in Sendai Tobari - 帷 -

    • github.com/goccy/tobari • テストごとに通過した場所を正確に記録することができる • go test で使ったり、 runtime/coverage と同じように Server でも使える ◦ Goのカバレッジ計測ツールの代わりに使える • Install: go install github.com/goccy/tobari/cmd/tobari@latest
  8. Go Conference mini 2026 in Sendai How to use •

    with go test1 ◦ GOFLAGS=$(tobari flags) go test ./… ▪ tobari flags: -cover -toolexec=tobari を出力 ▪ 実行すると tobari/tobari.(json|toon) が作られる ▪ テスト対象を一切変更する必要がない • NOT go test1 ◦ gRPC Server InterceptorやHTTP adapterの中で、カバレッジリクエストのと きに tobari.CoverWithName(name, entryFunc) を呼ぶ ▪ name : カバレッジを分けたい単位で設定する識別子 ▪ entryFunc : カバレッジを取得し始めるエントリ関数
  9. Go Conference mini 2026 in Sendai (前提) Go のカバレッジ計測の仕組み 1.

    go (test|build|run) のときに -cover option をつける 2. Goコンパイラがカバレッジ対象のパッケージを特定し、 そのパッケージのファイルを引数に go tool cover を呼び出す 3. go tool cover の中で受け取ったソースコードを AST に変換 4. AST からカバレッジ計測ポイント ( 関数の先頭や Ifによる分岐など ) を見つけ、 カウンタを挿入したコードを生成 5. 生成したファイルの場所を go tool cover の -outfilelist option で指定したファイルに書く 6. go tool compile が生成したコードを対象に走ってカバレッジ付きのバイナリになる go test -cover ./ Go compiler go tool cover Instrumented Code go tool compile Binary with coverage Identify cover targets AST transform Insert counters
  10. Go Conference mini 2026 in Sendai Key Idea: Coverage with

    GoroutineID 1. go tool cover の実行を自前のツールに差し替える 2. ASTを編集して計測ポイントを追加する際に、親と自分の GoroutineID も一緒に保存する 3. tobari.CoverWithName(name, entryFunc) API を提供 ◦ アプリ側で、識別子とカバレッジを取りたい関数を指定させる 4. tobari.CoverWithName の entryFunc 呼び出し時の GoroutineID を記録すれば、 GoroutineID ベースでどこを通ったかがわかる tobari.CoverWithName(name, entryFunc) entryFunc ( GID 10 ) foo ( GID 20, PGID: 10 ) bar ( GID 30, PGID: 20 ) GID: Goroutine ID PGID: Parent Goroutine ID
  11. Go Conference mini 2026 in Sendai How to replace go

    tool cover ? • Toolexec を利用する ◦ go (build|run|test) などで利用できる option ( -toolexec ) ◦ go tool compile などの tool 呼び出しをフックできる ▪ go build -toolexec=foo ./ • foo compile … or foo link … のような引数で foo が呼ばれる • go tool cover をフックすることもできる ◦ foo cover …
  12. Go Conference mini 2026 in Sendai How to get (

    parent ) GoroutineID ? • runtime.getg() で現在の Goroutine 情報がわかる ◦ runtime.getg().goid : GoroutineID ◦ runtime.getg().parentGoid : Parent Goroutine ID • Toolexec を利用して go tool compile の runtime package の compile をフックして以下の内容を加えると動的に公開 APIが作れる package runtime func GID() uint64 { return getg().goid } func PGID() uint64 { return getg().parentGoid }
  13. Go Conference mini 2026 in Sendai ビルドキャッシュの分離 • コンパイル時にファイルを置き換える場合、ビルドキャッシュが効くと go

    tool compile がそもそも呼ばれないので、キャッシュ管理が必須 • Go コンパイラは各 go tool を呼び出す前に、必ず go tool compile -V=full の ように -V=full 付きのリクエストを行う ◦ -V=full の標準出力の結果を使って Build Cache ID が決まる仕様 ◦ 出力結果を変えるとキャッシュが別になるのでフルビルド ◦ @sivchari さんに教えてもらった • Tobari では、Tobari binary の hash値、置き換え処理で利用するファイルの hash値 などを組み合わせて Build Cache ID を作っている ◦ tobari: <replace-file-hash> exe:<binary-hash>
  14. Go Conference mini 2026 in Sendai 依存パッケージの管理 • 置き換えたファイルが新しい依存を持っている場合、そのパッケージをビルドした上で、 go

    tool compile がコンパイルできるように、ビルドしたパッケージへのパスを 教える必要がある • go tool compile には -importcfg import.cfg という option があり、 import.cfg が依存パッ ケージのリスト管理している ◦ packagefile <package-name>=<package-path> の羅列 ◦ -x option をつけてビルドすると簡単に中身が見れる e.g.) go build -x ./ • import.cfg にビルド結果を追記すれば良い • ビルドは go list -deps -export -json <package> を使う ◦ <package> をビルドし、依存 package の path も含めて JSON 形式で取得できる
  15. Go Conference mini 2026 in Sendai 計測コードの挿入時の課題 • go tool

    cover に代わって自前のコードを挿入する際、次のようなコードを追加したい ◦ tobari.Trace(runtime.PGID(), runtime.GID(), startLine, startCol, …) • もともとカバレッジ対象の package が runtime や tobari package に 依存していない場合、 import.cfg への追記が必要になり処理が複雑になる package foo import ( “runtime” “github.com/goccy/tobari” ) func Foo() { tobari.Trace(runtime.PGID(), runtime.GID(), 6, 11, …) }
  16. Go Conference mini 2026 in Sendai linkname を利用した依存解決 • linkname

    directive を利用することで runtime や github.com/goccy/tobari package を import せずにコンパイルできる • ただし、go tool link 時に指定する import.cfg には必ず runtime や github.com/goccy/tobari への依存が必要なので注意 import _ “unsafe” //go:linkname runtime_GID runtime.GID func runtime_GID() uint64 //go:linkname tobari_Trace github.com/goccy/tobari/internal/tobari.Trace func tobari_Trace(uint64, uint64, int, int, int, int, int, int) func Foo() { tobari_Trace(runtime_PGID(), runtime_GID(), 6, 11, …) }
  17. Go Conference mini 2026 in Sendai 静的解析による到達範囲推定 • go tool

    cover で渡されたコードを静的解析し、関数間の依存関係を記録する • Tobari のカバレッジ測定エントリポイントから呼ばれた関数を記録 ◦ tobari.CoverWithName(name, func() { foo() }) • 実際に呼ばれた関数 ( foo )が依存する関数全てを静的解析の結果から導き、 それを通過すべき範囲とする • foo から絶対に呼ばれない関数は、通過すべき範囲に含まれない
  18. Go Conference mini 2026 in Sendai まとめ • Go標準の仕組みの代わりに使えるコードカバレッジ計測ツール、 Tobari

    を開発 • GoroutineID を使って、テストごとのカバレッジデータを正確に測定 • Agent Skills などを通して Coding Agent に有益な情報を提供することが目的 • GOFLAGS=$(tobari flags) go test ./ でなぜ測定できるのか ◦ Toolexec の活用 ▪ go tool cover をフックして GoroutineID 付きで計測 ▪ go tool compile をフックしてコンパイル対象を差し替え ◦ linkname を活用しながらカバレッジ計測コードを挿入 ◦ 静的解析を使ってカバー範囲を推定
  19. Go Conference mini 2026 in Sendai Coverage Metadata and Countdata

    • Go のカバレッジデータは内部的に Metadata と Countdata に分かれている • Metadata ◦ go tool cover を実行した際に作成 ◦ package の名前やファイルパスなど静的に決まる情報を保持 • Countdata ◦ ソースコードの場所に紐づくカウントを保持 • coverprofile 形式で出力する場合は Metadata と Countdata を合成する • go test では、裏で作る testmain.go で合成し、結果を出力
  20. Go Conference mini 2026 in Sendai Overlay • 標準ライブラリのコンパイル対象を変更できる ◦

    replace: $GOROOT/src/runtime/stubs.go => /tmp/stubs.go ◦ add: $GOROOT/src/runtime/new.go => /tmp/new.go • Goコンパイラ側が置き換えるべきファイルを解析し、 ビルドキャッシュを使うか判断したり、 package の依存解決も行う • 標準ライブラリにもともと書いてあったかのように振る舞う • $GOMODCACHE 以下のファイルには適用できない ◦ toolchain directive を利用してインストールされる Go は $GOMODCACHE 以下に配 置されるため、置き換えられない ◦ GOTOOLCHAIN=local が必須
  21. Go Conference mini 2026 in Sendai Toolexec Tips: Importcfg •

    go tool compile と go tool link で指定される • go tool compile ◦ Signature の Check に利用される ◦ I/F が一緒なら、中身はなんでも良い • go tool link ◦ 依存している package すべての symbol が同じものである必要がある ◦ 例えば異なる Overlay ファイルからビルドされた同じ名前の package が依 存にあると fingerprint mismatch error が発生するので注意
  22. Go Conference mini 2026 in Sendai Toolexec Tips: BuildID •

    Toolexec では go tool (compile|cover|vet|link) が別プロセスで起動する が、 go build ごとにユニークな共通のデータにアクセスしたいことがある • go tool の呼び出しは go build の子プロセスになるため、 os.Getppid() を 使うことで go build プロセスの PID を取得できる • go build ごとにユニークな ID として利用できるため、これを使って 一時ディレクトリを作ってリソースを共有することができる go build ( PID: 10 ) go tool compile … (PID: 20, PPID: 10 ) go tool cover … ( PID: 30, PPID: 10 ) go tool link … ( PID: 40: PPID: 10 )
  23. Go Conference mini 2026 in Sendai How to build github.com/goccy/tobari

    package ? • github.com/goccy/tobari の package を link 時にビルドするため、 filepath.Join(os.TempDir(), “tobari”, os.Getppid(), “app”) 配下に main.go と go.mod を配置 ◦ main.go: import _ “github.com/goccy/tobari” を追加 ◦ go.mod: require github.com/goccy/tobari <version> を追加 ▪ <version> は Tobari 自身が知っているのがポイント • “app” の下で go list -deps -export -json -toolexec=tobari github.com/goccy/tobari を実行すると build した package への path が手に入る
  24. Go Conference mini 2026 in Sendai テスト実行時のカバレッジ出力方法 GOFLAGS=$(tobari flags) go

    test ./… でカバレッジをテスト単位で出力するため、 testing package の関数を置き換えている GS=$(tobari flags) go test ./…FLAGS=$(tobari package testing import _ "unsafe" //go:linkname tobari_cover github.com/goccy/tobari/internal/tobari.Cover func tobari_cover(name, entryID string) func (t *T) Run(name string, f func(*T)) bool { // オリジナルの (*testing.T).Run を呼び出す return t.orgRun(name, func(t *T) { // Test名とGoroutineIDを紐づける tobari_cover(t.Name(), t.Name()) f(t) }) } package testdeps // カバレッジ出力のために呼び出される関数 func coverTearDown( coverprofile string, gocoverdir string, ) (string, error) { // メタデータとカウントデータを設定 // tobari/tobari.json に出力 } testing/internal/testdeps.coverTearDown testing.(*T).Run
  25. Go Conference mini 2026 in Sendai 静的解析時の依存グラフの調整 • 探索範囲を最適化 ◦

    runtime, net/http, google.golang.org/grpc に到達したら打ち切る • runtime: GC 関連の API が全関数への依存を持つため無視 • net/http or google.golang.org/grpc: 全ハンドラへの参照を持つため無視
  26. Go Conference mini 2026 in Sendai 埋め込み Option ( –embed-code

    ) の提供 • go test を利用しないパターンで coverage を取得した場合、 ビルドに利用したソースコードが手元にないことが多い ◦ e.g.) Container Image を作るタイミングでソースコードが消えている • Tobari では、 toolexec の option として —embed-code option をサポート ◦ go tool cover の処理を行う際に引数で渡されたソースコードを埋め込む ◦ tobari.ReadCoverArchivedFile() API を呼び出すと埋め込んだソースコー ドを tar.gz 形式で取得できる • Tobari の HTML 表示コマンドにはソースコードを渡すために tar.gz を渡す option や、ファイルを埋め込んだバイナリ自体を指定する option があり、 それを利用すると簡単に HTML 化ができる