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

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

 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 化ができる