カジュアルに使えるスタブサーバとしてhttpstubとgrpcstubを作って使っている

このエントリは Go Advent Calendar 2023 12 日目の記事です。

Goのテスティングパッケージで一番好きなパッケージは net/http/httptest です。

テスト実行時に実際にHTTPサーバを立ててHTTPリクエストを受けるというシンプルかつ強力なアプローチが良いです。

クライアント側にエンドポイントを変える仕組みさえあればクライアントのリクエストを受け付ける形でテストを構築することができるので、選択肢に入れておきたいテスト構成です。

ところで、私たちは runn (ランエヌ)というシナリオテスティングツールを開発しています。

github.com

runnはHTTPクライアントでありgRPCクライアントでもあるのですが*1、そのrunn自体のテストのためにhttpstubとgrpcstubを作って使用しています。

httpstub

github.com

httpstubはHTTPスタブサーバを立てるテストヘルパーです。

一番簡単な使い方としては httpstub.NewServer(t) でスタブサーバを立てた後、メソッドチェインで必要な分だけ各パスのレスポンスを定義してあげたら出来上がりです。

package myapp

import (
    "io"
    "net/http"
    "testing"

    "github.com/k1LoW/httpstub"
)

func TestGet(t *testing.T) {
    ts := httpstub.NewServer(t)
    t.Cleanup(func() {
        ts.Close()
    })
    ts.Method(http.MethodGet).Path("/api/v1/users/1").Header("Content-Type", "application/json").ResponseString(http.StatusOK, `{"name":"alice"}`)

    res, err := http.Get(ts.URL + "/api/v1/users/1")
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        res.Body.Close()
    })
    body, err := io.ReadAll(res.Body)
    if err != nil {
        t.Fatal(err)
    }
    got := string(body)
    want := `{"name":"alice"}`
    if got != want {
        t.Errorf("got %v\nwant %v", got, want)
    }
    if len(ts.Requests()) != 1 {
        t.Errorf("got %v\nwant %v", len(ts.Requests()), 1)
    }
}

grpcstub

github.com

grpcstubはgRPCスタブサーバを立てるテストヘルパーです。

一番簡単な使い方としては .protoファイルを指定して grpcstub.NewServer(t, "protobuf/proto/*.proto") でスタブサーバを立てた後、メソッドチェインで必要な分だけ各パスのレスポンスを定義してあげたら出来上がりです。 そう、ほぼhttpstubと同じです。

package myapp

import (
    "io"
    "net/http"
    "testing"

    "github.com/k1LoW/grpcstub"
    "github.com/k1LoW/myapp/protobuf/gen/go/routeguide"
)

func TestClient(t *testing.T) {
    ctx := context.Background()
    ts := grpcstub.NewServer(t, "protobuf/proto/*.proto")
    t.Cleanup(func() {
        ts.Close()
    })
    ts.Method("GetFeature").Response(&routeguite.Feature{
        Name: "hello",
        Location: &routeguide.Point{
            Latitude:  10,
            Longitude: 13,
        },
    })
    // OR
    // ts.Method("GetFeature").Response(map[string]any{"name": "hello", "location": map[string]any{"latitude": 10, "longitude": 13}})

    client := routeguide.NewRouteGuideClient(ts.Conn())
    if _, err := client.GetFeature(ctx, &routeguide.Point{
        Latitude:  10,
        Longitude: 13,
    }); err != nil {
        t.Fatal(err)
    }
    {
        got := len(ts.Requests())
        if want := 1; got != want {
            t.Errorf("got %v\nwant %v", got, want)
        }
    }
    req := ts.Requests()[0]
    {
        got := int32(req.Message["longitude"].(float64))
        if want := int32(13); got != want {
            t.Errorf("got %v\nwant %v", got, want)
        }
    }
}

その他細かい機能とまとめ

httpstubもgrpcstubも基本は指定された通りにレスポンスを返すスタブなわけですが、他にも細かい機能があります。

  • 受け取ったレスポンスを確認する機能
  • リクエストやレスポンスが仕様に沿っているかバリデーションする機能
  • 動的にレスポンスを返す機能
  • OpenAPI Specの examples: セクションの値を使ってレスポンスを返す機能

など、便利に使えています。

実はギョームでもガッツリ使っているので、生じたニーズに応じて都度機能を追加していっています。

httpstubとgrpcstubの使い勝手がほぼ同じなのも便利です。

こういうテストヘルパーを作れるのも、カジュアルにgoroutineでサーバを立てることができるGoの強みの1つですね。

*1:他にもDBクライアントでありCDPクライアントであり任意のコマンド実行ツールでもあります

Go Conference mini 2023 Winter IN KYOTOに参加してきた #kyotogo

京都に行ってきました。

Go Conference mini 2023 Winter IN KYOTO

kyotogo.connpass.com

すでに、ne-sachirouさんによりスライド資料などが集められています。感謝!

scrapbox.io

「miniとは?」となるGoで濃縮された1日になりました。運営の皆様、本当にありがとうございました!

私はと言うと、登壇はしたものの完全にカンファレンスを楽しんでいました。

本編

以下、一言感想です。

Deep dive into log/slog package

既に使ってはいましたが、実装は特にみていませんでした。普通に使えるイメージ。

「nAttrInline = 5」の理由がなるほどなあという理由でした。これ再度計測してレポートしたら変わってくるような値なんですかねー。

Goプログラムがビルドされるまで(コンパイラーの仕組みを探る)

まさにタイトルの通りで、非常にわかりやすい丁寧で面白い発表でした。コンパイラがわかった気になってしまうくらいのわかりやすい発表である意味注意しないといけない発表でした。

Goにおけるcall graphを用いた大規模コードベースの影響調査

私の発表前で、残念ながらリアルタイムでは聴講できませんでした。XのTLがざわついていたのが印象的でした。

あとで資料を拝見させてもらったのですが、影響調査のためのコールグラフを使うというアプローチはなかなかな腕力だなと思いました。すごい。

最近PHP界隈でも「ASTが同じならOK」みたいな腕力が流行っている?ようですけどなんかブームですかね?

Parsing case study in Go

私の発表です。

speakerdeck.com

公開できる事例の多さでは自信があったので(OSSなのでそれはそう)、とにかく事例を詰め込みました。

何かしら持ち帰ってもらえれば幸いです。

日時処理の新スタンダード: Synchro によるタイムゾーン安全、楽々開発

Xで開発しているときから知っていたのですが、とても良いパッケージだと思いました。

じゃあこれを明日からプロダクションに採用するためにはあと何の検討が必要かなーと考えていました。

Go1.22で導入予定だったzeroというbuilt-inについての紹介

大どんでん返しと貴重な経験のお話で面白かったです。

Go Getでのchecksum不一致に遭遇した話とその対応

これは...逆にやらかしたことがあるので、逆にすみませんと言う気持ちになっていました。

CUE+Goで安全かつ簡単に設定ファイルを自動生成してみた

CUEもずっといつか必要になるかも!?と思っているのですが、普段は360YAMLからは一桁二桁小さい数しか扱わないので、いつかのために今後もウォッチしておこうと思います。

context.WithoutCancelについて語る

「伝搬してきた値は使いたいがキャンセルされたくない」というニーズは分かっていたのですが、しっくりきていませんでした。しかし、「contextは値を伝搬する要件とキャンセルの要件の2つを担っていて、現状それを分けることが難しい」という説明にしっくりきました!

Golangを使ったDB用負荷テストツールの開発

流石の tools でした。コードも読んでみようと思いました。あとご挨拶できなかったのが残念(失敗の1つ)。

インターフェースのラッパーを作る際の落とし穴

ラップすると型アサーションに失敗する挙動については知らなかったです。全部作るのは確かに面倒そうだなと思いました。

生成AIによる静的解析ツールの自動生成

静的解析に求められる正しさと生成AIがもたらすまだ不安定な出力の相性って悪いんじゃないかなと思っていたのですが、 確かに https://github.com/gostaticanalysis/skeleton にあるようにある程度テンプレート化されているコードの穴埋めという意味だと「いけそう」と思いました。

The Future of encoding/json

「encoding/jsonJSONエンコードしてくれればいいんじゃ?」と思っていたのですが、この発表で「なるほど良い」と思いました。便利パッケージが1つ増えるのを待つ気持ちです。

続) TinyGo で作る自作キーボードの世界

びっくりした!あんなに簡単にキーボードできるのか!!!!!!!!!

触って理解するGoコンパイラの最適化

感想はこちらになります。

開発チーム横断タスクフォース「Goサブ会」の運用事例と今後の展望

ペパボでもチャプターやGoの情報共有会はあるのですが、Goサブ会はまた違うものでした。 懇親会でも詳しく聞いたのですが、雑な理解で言うと「権限と責任がある」横断組織となっているようでした。

そういう組織がプログラミング言語のくくりであることが「強い!」と思いました。

プロダクションで使うGo Pluginの利便性とパフォーマンス性

プラグインとは」からはじまりGoでできるプラグインのアプローチの紹介でした。

公式見解キツいw

ちなみに私は git-style が好きです。

Revolutionising Inheritance with Generics: A Fresh Approach in Go

Genericsの効果的な活用のお話でした。時間切れだったのであとで資料を拝見しましたが、Genericsを使って親structをつくりつつその親の構造体の1フィールドに子を渡しておくというのはなるほどと思いました。 Embeddingとちょうど依存が逆になっているから「責任の逆転」と言っているという理解でした。

net/http から net.Conn を掘り起こす

なんかreflectパッケージとかを使って引っ張り出すのかな?と思ったらちゃんと正攻法で取得できていました。 知らなかったので知識として覚えておこうと思いました。

GoとテストとインプロセスDB

テストでインプロセスDBを使う話。httptestスキーとしては興味津々で聞きました。

使うためにバグを薙ぎ倒していくのが印象的でした。

個人的な使い方としては syumai さんのポストの方法がよいなあと思いました。

ブログのリアクションから始めるGoのパフォーマンス分析入門

カッコ良すぎるムーブでした。見習いたい。

GoのProtocプラグインを活用した効率的なgRPC負荷試験戦略

.protoファイルからの生成、良いアプローチだと思っていて、私ももっとプラグイン書いて生成していかないとなあと思いました。

作ってよかったgraceful shutdownライブラリ

「Hatenaの知見」「Goサブ会の知見」となっているのが最高だと思いました。

runn開発者会議 in 鴨川

runnの開発者が揃ったので恒例のオフラインでの開発者会議をしていました。懇親会までの時間、鴨川で。

今回出た機能検討は覚えている限りだと次のような感じ

  1. ファイルの存在有無などで実行するステップを変えたい
    • ビルトイン関数への実装以外で何か良いアイデアはないか
    • Fileランナーを作っていいのだろうか
  2. 複数のAPIサーバへリクエストをするようなシナリオで、一部のAPIサーバだけモックしたい
    • 同じシナリオを使ってそれぞれのAPIサーバ開発でテストを行うイメージ
    • https://github.com/vcr/vcr 的な挙動が必要?
  3. 成功したステップをスキップしつつ成功時の値はキャッシュとして利用したい
    • ユースケースとしては2のほうと同じなので、あくまで実現方法の違い
  4. APIテストを担うことが多いが、テストランナーの現状のワンライナーしか許容しない記法があまり可読性良くない
    • 別の記法「も」サポートしたほうが良さそう

どれも実装にいたるまでは詰められなかったので、一旦寝かせてあとはオンラインで引き続き。難しい。。。

懇親会

Gopherのたしなみとして「ORMは何を...?」などから始めつつ、いろいろお話ししました。話が楽しかったのですが、一方でもっといろんな人に挨拶すればよかったなあなどと思いました。

あと、これは課題です。

あとはtenntennさんにPull Requestのレビューを直接お願いするというオフラインカンファレンスムーブをキメるなどしていました*1

2次会

はてな社さんにお邪魔させていただきました。ただ、まあ、もうダメでした。楽しかったです。

感想

最初にも書きましたが本当に濃いGoな1日でした。

お疲れ様でした! ありがとうございました!

*1:後日レビューしていただきました

GitHub Actions上で安定したベンチマーク環境を得る(ただし、相対比較専用)

現在、実験的にですが少しパフォーマンスを気にしたいパッケージを書いています。 ちなみに、その1つはリバースプロキシです。

github.com

「気にしたいパフォーマンス」というのは以下の2つです。

  1. 現在のGoの実装がNGINX(のリバースプロキシ機能)と比べてリクエストあたりの処理時間が小さいか
  2. 現在の実装が以前の実装(Pull Requestであればmainブランチ)と比べてリクエストあたりの処理時間が極端に大きくなっていないか

GitHubリポジトリがあるのでCI環境としてはGitHub Actionsがあります。

そこで、上記2点をGitHub Actions上で継続的に計測したいと考えました。

まず、1つ目の「NGINXとの比較」ですが、これはただNGINXを使ったベンチマークと自分の実装を使ったベンチマークをそれぞれ実行することにしました。

NGINXを使ったベンチマークの結果(ns/op)と、同じようにGoの実装を使ったベンチマークの値(ns/op)を比較するだけです。

それぞれのベンチマークgo test -bench で計測しoctocovのカスタムメトリクス機能を使って結果を表示します。

k1low.hatenablog.com

GitHub Actionsで提供されるRunnerのスペックはいつも同じとは限らない

2つ目の「以前の実装との比較」です。

当初はoctocovの比較機能を使い、GitHub Artifactsに保存してある以前のベンチマーク計測結果と比較するようにしたのですが、ベンチマーク結果が安定しませんでした。

原因はGitHub Actionsで提供されるRunnerのスペックがいつも同じとは限らないことでした。

このRunnerのスペックの違いを、CPU命令数とCPUキャッシュのヒット率を使って乗り越える方法を実現した方がいます。

www.mizdra.net

私も手元ではうまくいったように見えたので、Cachegrindの結果をoctocovのカスタムメトリクスで取得できる仕組みまで作ったのですが、残念ながら期待した結果にはなりませんでした。おそらく、ベンチマーク対象のコードにCPU以外に大きく影響をうけるような箇所があるんだろうなと推測しています。

github.com

同じRunnerで実装前後のベンチマークを比較する

ようは、実装前後の2値の比較だけができればいいわけです。

ということで愚直に同一Job上で実装前後のベンチマークを取得し比較するようにしました。そうすれば少なくともRunnerのスペックの違いに悩まされることはなくなります。

actions/checkoutk1LoW/octocov-action の機能を駆使します。

作成したGitHub ActionsのWorkflowは次のとおりです。

# .github/workflows/benchmark.yml
name: benchmark

on:
  pull_request:

jobs:
  benchmark:
    name: Benchmark
    runs-on: ubuntu-latest
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - name: Check out source code
        uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version-file: go.mod

      - name: Set up octocov-go-test-bench
        run: go install github.com/k1LoW/octocov-go-test-bench/cmd/octocov-go-test-bench@latest

      - name: Check out source code (main)
        uses: actions/checkout@v4
        with:
          ref: main
          path: main

      - name: Run benchmark (main)
        run: go test -bench . -benchmem -benchtime 10000x | octocov-go-test-bench --tee > custom_metrics_benchmark.json
        working-directory: main

      - name: Run octocov (main)
        uses: k1LoW/octocov-action@v0
        with:
          config: .octocov.benchmark.main.yml
        env:
          OCTOCOV_GITHUB_REF: refs/heads/main
          OCTOCOV_GITHUB_SHA: none
          OCTOCOV_CUSTOM_METRICS_BENCHMARK: main/custom_metrics_benchmark.json

      - name: Run benchmark
        run: go test -bench . -benchmem -benchtime 10000x | octocov-go-test-bench --tee > custom_metrics_benchmark.json

      - name: Run octocov
        uses: k1LoW/octocov-action@v0
        with:
          config: .octocov.benchmark.yml
        env:
          OCTOCOV_CUSTOM_METRICS_BENCHMARK: custom_metrics_benchmark.json

順を追って説明します。

まず、Check out source code Set up Go Set up octocov-go-test-bench ステップまでは通常通りですが、Check out source code (main) でmainブランチのソースコードを mainディレクトリにチェックアウトします。

      - name: Check out source code (main)
        uses: actions/checkout@v4
        with:
          ref: main
          path: main

次にmainブランチのソースコードベンチマークを実行します。ベンチマーク結果を octocov-go-test-bench でパースしてoctocovのカスタムメトリクスJSONとして、main/custom_metrics_benchmark.json に保存します。

      - name: Run benchmark (main)
        run: go test -bench . -benchmem -benchtime 10000x | octocov-go-test-bench --tee > custom_metrics_benchmark.json
        working-directory: main

mainブランチのoctocovカスタムメトリクスJSONを使ってoctocovでレポートを作成します。

      - name: Run octocov (main)
        uses: k1LoW/octocov-action@v0
        with:
          config: .octocov.benchmark.main.yml
        env:
          OCTOCOV_GITHUB_REF: refs/heads/main
          OCTOCOV_GITHUB_SHA: none
          OCTOCOV_CUSTOM_METRICS_BENCHMARK: main/custom_metrics_benchmark.json

mainブランチのカバレッジレポートであることを記録するために環境変数 GITHUB_REFGITHUB_SHA を上書きします。GitHub Actions上では直接 GITHUB_REFGITHUB_SHA を上書きすることはできないので OCTOCOV_GITHUB_REFOCTOCOV_GITHUB_SHA で上書きします*1

使用する設定ファイル .octocov.benchmark.main.yml は次のように指定します。

# .octocov.benchmark.main.yml
repository: ${GITHUB_REPOSITORY}/benchmark
coverage:
  if: false
codeToTestRatio:
  if: false
testExecutionTime:
  if: false
report:
  datastores:
    - local://main # レポートをローカルの main/ ディレクトリ以下に保存する

コードメトリクスの取得は全てOFFにし、レポートファイルをローカルディレクトリに保存するようにしています。

最後はPull Requestの実装のベンチマークを取得してoctocovにカスタムメトリクスとして読み込ませます。

      - name: Run benchmark
        run: go test -bench . -benchmem -benchtime 10000x | octocov-go-test-bench --tee > custom_metrics_benchmark.json

      - name: Run octocov
        uses: k1LoW/octocov-action@v0
        with:
          config: .octocov.benchmark.yml
        env:
          OCTOCOV_CUSTOM_METRICS_BENCHMARK: custom_metrics_benchmark.json

ここで比較対象として、先ほどローカルの保存したmainブランチのoctocovレポートを指定します。

.octocov.benchmark.yml は次のようになります。

# .octocov.benchmark.yaml
repository: ${GITHUB_REPOSITORY}/benchmark
coverage:
  if: false
codeToTestRatio:
  if: false
testExecutionTime:
  if: false
diff:
  datastores:
    - local://main # 比較対象としてローカルの main/ ディレクトリ以下に保存されているレポートを使用する
comment:
  if: is_pull_request
summary:
  if: true

実際のレポート結果

  1. Goの実装とNGINX(のリバースプロキシ機能)との比較
  2. 現在の実装と以前の実装(Pull Requestであればmainブランチ)との比較

なのでちょうど次のようなマトリクスになります。

以前の実装(main) Pull Requestの実装
NGINX
Goの実装

そして、今回説明したWorkflowをPull Requestで実行すると以下のようになります。

Metadataを展開するとちゃんと実行環境のCPUが揃っていることがわかります。

まとめ

今回、2つの要件を満たすためのGitHub Actions上での安定したベンチマーク環境を得る方法を紹介しました。

ただ、残念ながら継続的なベンチマーク環境とはいかず、あくまで相対比較専用となっています。

それでも、Pull Requestごとに数値で確認できるのは大きなメリットです。

個人的には、Cachegrindの手法(もしくはそれに準じる手法)が「GitHub Actions上での安定した継続的ベンチマーク環境」を得る方法だと思っているので、折を見て再挑戦したいと思っています。

スタブの役割をするためだけに生まれてきたバイナリstubin

複数のコンポーネントが連動するアプリケーションのテストを書くとき、「どこまでを本物を使ってどこからモックするか」みたいな話は開発の現場のいたるところであると思います。 「要はバランス」だったり「答えは現場の数だけあ」ったり*1

外部コマンドの実行をスタブしたい

ある外部コマンドの実行を含むコードのテストを書こうとしたのですが、

  • 外部コマンドの用意自体が意外に面倒くさいタイプのものだった
  • バイナリの存在チェックだったり実行だったり、コードの各所でその外部コマンドに関係するコードがあった
  • 一方で簡単な標準出力やステータスコードを返せばテストとしては成り立ちそうだった

という状況で、コード内に外部コマンドの実行をモックできる仕組みを作るにはちょっとコストが高いなと感じて、「適当なスタブ用バイナリを用意すればいいのでは」と考え作ってみました。

stubin

github.com

stubinは任意のコマンドのスタブの役割をするためだけに生まれてきたバイナリです。普通に実行すると何も出力せずExist status 0で終了します。

stubinに挙動を渡す方法は環境変数です。

通常は環境変数 STUBIN_STDOUT にstubin実行時の標準出力の内容、 STUBIN_STDERR に標準エラーの内容を、STUBIN_STATUS にExist statusを事前にセットしておくと実行時に環境変数に沿った動きをします。

$ export STUBIN_STDOUT="Hello, stubin!
"
$ stubin
Hello, stubin!
$

スタブのための機能

上記で「通常は」と書いたのは、stubinは少しアドバンストな使い方ができるからです。

バイナリをリネームすると対応する環境変数も変わる

スタブしたいバイナリは1つとは限りません。stubinはそのための機能を持っています。

stubin というバイナリを例えば fakecmd にリネームすると、バイナリに対応する環境変数FAKECMD_STDOUT FAKECMD_STDERR FAKECMD_STATUS に変わります。

このようにstubinをコピーしてリネームするだけで必要な数だけスタブコマンドを増やすことができます。

条件を指定して出力内容を変えることができる

引数が全て格納されているargs という変数をつかって条件を指定することで条件に合致した場合の出力も環境変数で設定できます。

具体的には [バイナリ名]_EXPECT_COND という環境変数に条件記載して、[バイナリ名]_EXPECT_STDOUT[バイナリ名]_EXPECT_STDERR[バイナリ名]_EXPECT_STATUS に条件を合致した時の出力内容やExit statusを指定しておけばその動きをします。

複数の条件と出力を設置したいときはサフィックスを追加することで条件と出力の対応を増やすことができます。

次の例では _HELP_TEST サフィックスをつけています。

$ export STUBIN_EXPECT_COND_HELP="'-h' in args"
$ export STUBIN_EXPECT_STDOUT_HELP="Usage: stubin [-h]
"
$ export STUBIN_EXPECT_COND_TEST="'test' in args"
$ export STUBIN_EXPECT_STDOUT_TEST="This is test.
"
$ stubin
$ stubin -h
Usage: stubin [-h]
$ stubin test
This is test.

スタブの役割をするためだけに生まれてきたバイナリ

スタブ用なので自分のバージョン表示をすることもなく環境変数で挙動を変えるだけのバイナリstubin。作っていてちょっと寂しい気持ちになりました。

もし必要になることがあれば思い出してあげてください。

*1:ちなみに私は積極的にミディアムテストにしてしまう派です。

octocovで任意のメトリクスを記録できるようにした(カスタムメトリクス)

octocov

octocovはコードカバレッジのためのツールキットです。

github.com

コードカバレッジなどのコードメトリクスを手元のターミナルで確認したり、GitHub ActionsのActionとしてPull Requestにレポートしたりできます。

計測したコードメトリクスを、さまざまなデータストアに蓄積することもできます。

octocovはコードカバレッジ、Code to Test ratio、テスト実行時間の3つのコードメトリクスを計測しますが、今回、任意のメトリクスに対応しました。

カスタムメトリクス

カスタムメトリクスの使用方法は簡単です。

  1. 計測したメトリクスを指定のフォーマットで保存する(カスタムメトリクスJSON
  2. octocovに環境変数経由でカスタムメトリクスJSONの保存パスを渡す

これだけです。

1. 計測したメトリクスを指定のフォーマットで保存する(カスタムメトリクスJSON

カスタムメトリクスが対応できるのはKey-Value形式で、かつValueが数値であるメトリックです。

それを次のようなJSONフォーマットで保存します。

{
  "key": "benchmark_0",
  "name": "Benchmark-0",
  "metrics": [
    {
      "key": "N",
      "name": "Number of iterations",
      "value": 1000
    },
    {
      "key": "NsPerOp",
      "name": "Nanoseconds per iteration",
      "value": 676.5,
      "unit": " ns/op"
    }
  ]
}

JSONスキーマはこちらに定義しています

2. octocovに環境変数経由でカスタムメトリクスJSONの保存パスを渡す

カスタムメトリクスJSONが保存しているパスを OCTOCOV_CUSTOM_METRICS_プレフィックスにつけた任意の環境変数でoctocovに渡します。 カスタムメトリクスJSONは、OCTOCOV_CUSTOM_METRICS_1 OCTOCOV_CUSTOM_METRICS_2 のようにいくつ渡しても構いません。

例えばGitHub Actionsでoctocov-actionを使っている場合は次のように指定します。

steps:

[...]

      - name: Run octocov
        uses: k1LoW/octocov-action@v0
        env:
          OCTOCOV_CUSTOM_METRICS_BENCHMARK: custom_metrics_benchmark.json

これで設定は終わりです。

あとはoctocovがもともと対応している3つのコードメトリクスと同じようにPull Requestにレポートしたり比較したりしてくれます。

octocov-go-test-bench

1つの例として go test -bench の結果をoctocovのカスタムメトリクス形式に変換するコマンドを作成しました。

github.com

次のように go test -bench の標準出力をoctocov-go-test-benchに渡すだけでoctocovのカスタムメトリクス形式に変換して出力してくれます。

そのままJSONファイルに保存すればよいです。

同時にベンチマークの出力結果も確認したい場合は --tee オプションを付与してください。

$ go test -bench . -benchmem | octocov-go-test-bench --tree > custom_metrics_benchmark.json
goos: linux
goarch: amd64
pkg: github.com/k1LoW/rp
cpu: Intel(R) Xeon(R) Platinum 8272CL CPU @ 2.60GHz
BenchmarkNGINX-2       10000        635221 ns/op       17720 B/op        139 allocs/op
BenchmarkRP-2          10000        376138 ns/op       64199 B/op        191 allocs/op
PASS
ok      github.com/k1LoW/rp 25.807s

GitHub Actions上での具体的な設定例としては、私が別で開発しているOSSの設定を見ると良いでしょう。

コードメトリクスツールから、汎用メトリクスツールへ

今回、octocovの想定利用ケースを広げることができました。

当初の構想としては未実装はあと1つで、v1になる日も近そうです。

是非使ってみてください。

テストでしか使わないパッケージをgo.modに含めない方法

元エントリは↓で、これが全てです。

dev.to

Goでツールではなくライブラリ(パッケージ)を作っているとき、テストにしか使わないパッケージがgo.modに入って依存関係ができてしまうのが、いつも気になっていました。

ただ、「まあ別にいいかな」と思っていたのですが、ちょうど標準パッケージだけで機能が実現できるパッケージができたので、どうしてもgo.modを綺麗にしたくなりました。

そして、えいっと調べてみたらすぐに解決方法が見つかったのでした。

テストでしか使わないパッケージをgo.modに含めない方法

方法は次の通りです*1

  • テスト用に go_test.mod ファイルを用意して -modfile オプションを使って go_test.mod ファイルを指定して go test を実行する
  • (例えば)go.modの更新に go mod tidy を使わない

テスト用に go_test.mod ファイルを用意して -modfile オプションを使って go_test.mod ファイルを指定して go test を実行する

-modfile でgo.modを指定できるので(知らなかった!)、テスト用にgo_test.modファイルを指定するようにすることでパッケージとしての依存関係とテスト実行時の依存関係を別に管理します。

(例えば)go.modの更新に go mod tidy を使わない

公式の go mod tidy の説明によると

go mod tidy works by loading all of the packages in the main module and all of the packages they import, recursively.

とのことで、これにはテストコードも含まれます。

なので、 go mod tidy をそのまま使用することをやめるか、

Note that go mod tidy will not consider packages in the main module in directories named testdata or with names that start with . or _ unless those packages are explicitly imported by other packages.

とのことなので、テストコードを testdata ディレクトリ以下に押し込んでしまえば、テストでしか使わないパッケージが go.mod に入ることを回避できそうです。

ただ、 go mod tidy が使えないのは面倒...

具体的にどうしているか

https://github.com/k1LoW/rp/blob/aa26b46a7d17cdeb16370c1b704ee741c5e7b5b4/Makefile#L8-L10

$ cp go.mod testdata/go_test.mod
$ go mod tidy -modfile=testdata/go_test.mod
$ go test ./... -modfile=testdata/go_test.mod -coverprofile=coverage.out -covermode=count

のような感じで go_test.mod を生成してテストを実行しています。

もっといい方法がありそう

「私はこうやって解決しているよ」というのがあれば是非教えてください!

*1:エントリ公開当初「テストコードを別パッケージにする」と書いていましたが特に必要ありませんでした

PHPカンファレンス福岡2023に参加した

PHPカンファレンス福岡2023に参加してきました。

phpcon.fukuoka.jp

ホームでのカンファレンス開催と参加

私は福岡に住んでいます。その福岡のカンファレンスが、復活しました。実に4年ぶりとのことです。

4年で様々なことが変わりました。その変化の中、あの頃と変わらずにカンファレンスという場を提供してれたことに本当に感謝します。

これは推測でしかありませんが、4年の間、誰かの、誰か「達」の思いの火がずっと消えなかったからこその開催なのではないかなと思います。 一度(毎年開催という意味で)途絶えたカンファレンスを再度立ち上げるのは本当に「気持ち」が必要だっただろうなと思います。

私はというと、4年前といえば、私の子供はまだ生まれていませんでした。 そう考えると私のライフステージっぽいものも変わっていまして、今回は全然野菜や前夜祭などの参加は見送り、本編とAfter Hackの2つに絞って楽しみました。

fusic.connpass.com

あっという間の2日間でしたが、本当に楽しむことができました。

5年分の集大成としての発表

かなりの競争率の中無事採択され、発表をさせてもらうことができました。

今回、私が開発している tbls というツールについて話をさせてもらいました。

github.com

tblsと、tblsを開発したことがきっかけで考えるようになった "Documentation as Code" について、5年分のまとめ、そしてその先についての発表になります。

発表冒頭でも話しましたが、Documentation as Codeについて、もう話せることはありません。小さなアイデアすらないです。全部出し切りました。今後はインプットのフェーズだと思います。

5年分をまとめたお得スライドになっていると思いますので、よろしければ結論だけでも見てください。

かつて、私は福岡Ruby会議02でawspecというツールについて集大成的な発表をしました。

k1low.hatenablog.com

今回も福岡で、tblsについて同じように集大成的な発表ができて感無量です。

#runn開発者会議

PHPerKaigi2023に続いてrunn開発者会議を開催することができました。奇跡!

github.com

前回の去り際に「PHPカンファレンス福岡2023にプロポーザル出して福岡に行きますねー」といっていた @katzchum さん、有言実行してすごい。

今回もいくつかの機能について議論ができました。

  • 各ランブックから読みこむファイルのパス解決の機能拡張
    • まずパス解決の機構を統一する
    • 便利なパス名解釈( ../ など)やワイルドカード***/*)はセキュリティの観点からオプトインの仕組みを作る
  • Cookieを取り扱う機能の導入
  • ランブック単位、ステップ単位で一意性を持つIDの導入
    • 目的はエラー時の発生箇所特定や再実行に使う
    • コミットハッシュのように一意であれば短く指定することできるように
  • 大量のランブックを読み込む時の速度向上(もしくはUIで解決)

方針だけは決めたので、あとはGitHub上でコミュニケーション取りながらぼちぼち解決していこうと思います。

PHPカンファレンス福岡だった

PHPカンファレンス福岡の会場に入って受付をしたとき、「あーPHPカンファレンス福岡だ」と思いました。

スタッフに見知った熟練者がいるのを見かけるのも「あーPHPカンファレンス福岡だ」と思いました。

@nojimage さんがニコニコしているのも「あーPHPカンファレンス福岡だ」と思いました。

いろいろアップデートされていてさらに過ごしやすく楽しくなっているのはその通りなのですが、なんか「あーPHPカンファレンス福岡だ」と思ったのです。

いやー良いPHPカンファレンス福岡でしたね。

みなさんお疲れ様でした。