GitHub上で構築するコードメトリクス計測基盤

TL;DR

長いので要約すると

  • octocovは計測したコードメトリクスをGitHub Actions Artifactsに保存すると、単体リポジトリでメトリクスの比較レポートなどもしてくれて便利
  • さらに octocovs-template を使って、各リポジトリのArtifactsに保存されているメトリクスを収集する専用リポジトリを作るとバッジも生成・配布できて便利

となります。


GitHub」というキーワードでお声かけいただき、「TECH STAND #9 GitHub」にて、本エントリと同名タイトルで発表させてもらいました。

standfm.connpass.com

GitHubの中の開発の仕方を知ることができたり、GitHubを使った様々な生産性向上のアプローチを知ることができたり、何よりいろんな方に octocov やペパボでの取り組みを知ってもらえてよかったです。

今回は、個人的にずっと模索していたことがTECH STANDでの発表やアフタートーク、その後フィードバックを通じて気づきを得て解決することができたのでそれを紹介したいと思います。

GitHub "のみで" 構築するコードメトリクス計測基盤

octocovはPull Requestへのコードメトリクスレポート機能やCLIによるカバレッジ確認機能などがあります。

なお、CLIによるカバレッジ機能については次のエントリをぜひ。

k1low.hatenablog.com

バッジ生成機能

その中でも面白い(しかし本人はいたって真面目に作った)機能として、バッジ生成機能があります。

せっかくCIで計測しているコードカバレッジやCode To Test Ratioやテスト実行時間をバッジにしてREADME.mdに貼りたいわけです。

↑こういうのです

そのためにパッケージも書いたりしています。

octocov/pkg/badge at main · k1LoW/octocov · GitHub

ただ、これには1つ個人的なこだわりで課題を持っていました。

CIで計測したコードメトリクスのバッジを生成したとして、そのバッジを誰が配布するのか

CIで計測した結果、その場でバッジの生成はできます。

しかしREADME.mdにそれを更新される状態で貼るには、バッジを常に更新されても固定のURLで公開する必要があるわけです。

その実現方法について満足していませんでした。

CI上で生成したバッジをそのままリポジトリのコミットしてしまう

実現方法の1つは、CI上で生成したバッジをそのままリポジトリのコミットしてしまう方法です。

コミットさえしてしまえばそれをREADME.mdから読むことは可能です。

# octocov

![coverage](docs/coverage.svg) ![ratio](docs/ratio.svg) ![time](docs/time.svg)

最近まで、リポジトリとしてのoctocovもこの方法をとっていました。

https://github.com/k1LoW/octocov/blob/v0.41.0/README.md?plain=1#L5

ただ、これには欠点があって「バッジのコミットが積まれてしまう」というものです。これがうざい。テスト実行時間も取得していることから結構な頻度でコミットが積まれていまいます。

Central modeを使う

もう1つはCentral modeを使う方法です。

octocovにはCentral modeというコードメトリクスを複数のリポジトリから収集するモードがあります。

現在は収集したコードメトリクスと、生成したバッジの一覧を生成する機能があります。Central modeを使えばバッジの配布が可能です。

このCentral modeを使う方法にも(今までは)「GitHubのみで構築するコードメトリクス基盤としては、Central modeのリポジトリ(Central repo)に各リポジトリのメトリクスデータを集約する構成の決定打がない」という課題がありました。

従来の実現方法としてはCentral repoに対して各リポジトリGitHub Actionsからコミットをするというものでした。

以下のような感じです。

図をみるとわかるようにCentral repoへコミットするためのPermissionが必要になってしまいます。

できれば、リポジトリにある secrets.GITHUB_TOKEN のみでいい感じに他リポジトリにコミットできる仕組みが欲しいところですが、今のところはありません*1。Parsonal Access Tokenが必要になってしまいます。

なんとかできないものかと思っていたのですが、現状は解決を諦めていました。

GitHub Actions Artifacts

ところで、

octocovに最近追加された機能として、「計測したコードメトリクスのデータをArtifactsに保存する」というものがありました。

これによってリポジトリ単体の機能の範囲で過去メトリクスを保存できるようになり、Pull Requestへのレポート時に「前回メトリクスとの比較」もレポートできるようになりました*2

作者である私自身のoctocovに対する機能の理解としてもここまでだったのですが、先日のイベント登壇からの流れでふと

リポジトリのArtifactsにあるコードメトリクスレポートをCentral repoから収集すればいいのでは?

と思い付いたのです。

そして、実際に設定してみると出来たのでした。

既に機能としては実現していたのに作者が気づいていなかったという...*3

実現イメージとしては以下のような感じです。

GitHub のみで コードメトリクスバッジ基盤を構築する方法

さて、具体的な設定方法です。前提として既にoctocovを利用してGitHub Actions Artifactsにコードメトリクスを保存しているPublicなリポジトリを持っている必要があります。

次にCentral repoを作ります。こちらはテンプレートリポジトリを用意しました。

github.com

このリポジトリの "Use this template" ボタンを押して完成です。終わり。

あとは .octocov.yml の central.reports.datastores: にコードメトリクスを収集したいリポジトリを追加していけば良いです。サンプルとして octocov のメトリクスを収集するようになっています。

central:
  reports:
    datastores:
      - artifact://k1LoW/octocov # delete this line
      # - artifact://owner/repo
      # - artifact://owner/other-repo
      # - artifact://owner/another-repo
  badges:
    datastores:
      - local://badges
  push:
    enable: true

実際にバッジを生成している私のリポジトリこちらです。

まとめ

やっと「GitHub上で構築するコードメトリクス計測基盤」を真の意味で実現できる道筋ができました。

Central modeは、現状は最新のコードメトリクスとバッジの一覧の配布のみに対応していますが、将来的にはメトリクスを蓄積して便利そうなダッシュボードなどをGitHubの機能のみでつくれるところまで実現したいとは思っています。今のところキーワードとなっているGitHubの機能は「FlatData」と「ActionsからPagesへの直接デプロイ」です。

ただ、まあ、、、

ぜひ皆さんも使ってみてください。「(個人や会社で)使っているよ」というフィードバックが一番嬉しいです。機能リクエストやPull Requestもお待ちしています。

*1: @kyanny さんにブログエントリ経由でリポジトリ間のアクセスについてまとまっている https://docs.github.com/ja/actions/security-guides/security-hardening-for-github-actions#considering-cross-repository-access を教えてもらいました

*2:データストアを使えば今までもできていましたが、リポジトリ単体で実現できるようになったのがポイントです

*3:なぜ既に機能として実現していたかというと、octocovがサポートするデータストアはREADは全て io/fs.FS インターフェースで揃えていたからです。詳しくは Go Conference 2021 Autumn の発表資料をご覧ください https://speakerdeck.com/k1low/go-conference-2021-autumn-online

経過時間を内訳も含めて計測するためのストップウォッチライブラリ stopw を作った

runn を使ってシナリオテストを順調に積み上げていった結果、順調にテスト実行時間が長くなってきました。

そこでテストのどの部分に時間がかかっているかを計測したくなったのですが、runnにその時間計測機能はなかったのでそれを作ろうと考えました。

ところが、実現したい時間計測機能を満たすちょうど良いライブラリがなかったのでそれを作ることから始めました。

実現したい時間計測機能

時間計測というと Start()Stop() の間の開始時刻と終了時刻と経過時間がわかれば良さそうです。

しかし、runnに組み込むにあたって実現したかった時間計測は、「runnで実行している『どのシナリオ』の『どのステップ』の『どの処理』に時間がかかっているか?」を特定するためのもので、

上記の図でいうと横軸が時系列だとして、実現したい時間計測機能は

  • 濃いめの青のライン全ての開始時刻と終了時刻と経過時間を
  • 入れ子状態を維持した(表現した)状態で
  • 1つの結果として

時間計測したいというものです。

stopw

というわけで作成したのが stopw です。

github.com

stopwは Start()Stop() したあと、計測結果を Result() で取得します。

上記だけだとただの時刻計測です。

stopwは入れ子を表現するためにキーを複数渡すことができます。

キーを渡して時刻計測することで、次のように入れ子の計測結果を得ることができます。

package main

import (
    "encoding/json"
    "fmt"

    "github.com/k1LoW/stopw"
)

func main() {
    stopw.Start()
    stopw.Start("sub span A")
    // do something for `sub span A`
    stopw.Start("sub span B")
    // do something for `sub span A` or `sub span B`
    stopw.Start("sub span A", "sub sub span a")
    // do something for `sub span A` or `sub span B` or `sub sub span a`
    stopw.Stop("sub span A", "sub sub span a")
    // do something for `sub span A` or `sub span B`
    stopw.Stop("sub span span A")
    // do something for `sub span B`
    stopw.Stop()

    r := stopw.Result()
    b, _ := json.MarshalIndent(r, "", "  ")
    fmt.Println(string(b))

    // Output:
    // {
    //   "id": "cbt1386v9mc80ofooblg",
    //   "started_at": "2009-11-10T23:00:00.436022+09:00",
    //   "stopped_at": "2009-11-10T23:00:00.436883+09:00",
    //   "elapsed": 860375,
    //   "breakdown": [
    //     {
    //       "id": "sub span A",
    //       "started_at": "2009-11-10T23:00:00.436153+09:00",
    //       "stopped_at": "2009-11-10T23:00:00.436594+09:00",
    //       "elapsed": 441292,
    //       "breakdown": [
    //         {
    //           "id": "sub sub span a",
    //           "started_at": "2009-11-10T23:00:00.436449+09:00",
    //           "stopped_at": "2009-11-10T23:00:00.436594+09:00",
    //           "elapsed": 145500
    //         }
    //       ]
    //     },
    //     {
    //       "id": "sub span B",
    //       "started_at": "2009-11-10T23:00:00.436303+09:00",
    //       "stopped_at": "2009-11-10T23:00:00.436883+09:00",
    //       "elapsed": 580083
    //     }
    //   ]
    // }
}

上記のコードをみたらわかるように、渡したキーの数で入れ子の深さを表現するという単純なものです。

ただ、いくつか使いやすくするためのしくみを入れています。

いきなり stopw.Start() ができる

これの何が便利かというと、stopw.Start() のスコープと stopw.Stop() のスコープが同じである必要がないということです。

例えば、ある関数スコープ内で stopw.Start() して、別の関数スコープ内で stopw.Start("keyA") して、さらに別の関数スコープ内で stopw.Start("keyA", "keyB") して、さらに別の関数スコープ内で stopw.Stop しても、1つにまとまった結果を stopw.Result() から取得できます。

これは組み込みやすさにつながります。

math/rand などでも使われているテクニックですね。

https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/math/rand/rand.go;l=293

当然、別々の時刻計測をするためにインスタンスを分けることも可能です。

a := stopw.New()
b := stopw.New()

a.Start()
// do something for a
a.Stop()

b.Start()
// do something for b
b.Stop()

ra := a.Result()
rb := b.Result()

defer stopw.Start().Stop() で、関数ブロックの時間計測ができる

これも組み込みやすくするためのしくみです。1行で時間計測ができるのは便利です。

func () {
    defer stopw.Start().Stop()
    // do something
}()
r := stopw.Result()
[...]

このテクニックもどこかのライブラリで見た気がするのですが忘れてしまいました。

stopw.Disable() で、時間計測処理をスキップできる

一括してOFFにできるようにしていて、stopwを組み込んだ結果発生するオーバヘッドをできるだけ小さくできるようにしています。

キーに合わせてできる限り時刻を含め入れ子を保つように動く

例えば stopw.Start() を実行しなくても stopw.Start("key A") を実行すれば stopw.Start() で記録されるべき開始時刻が記録されますし、stopw.Stop("key B") を実行しなくても stopw.Stop() を実行すれば stopw.Stop("key B") で記録されるべき終了時刻が記録されます。

stopwはキーを元に入れ子をできるだけ保とうとします。

最後に

runnにはどう組み込まれているかというと、

runn.Profile(true) をオプションで渡して、最後に DumpProfile() を実行すると、計測結果をJSON形式で取得できるようになっています。

opts := []runn.Option{
    runn.T(t),
    runn.Book("testdata/books/login.yml"),
    runn.Profile(true)
}
o, err := runn.New(opts...)
if err != nil {
    t.Fatal(err)
}
if err := o.Run(ctx); err != nil {
    t.Fatal(err)
}
f, err := os.Open("profile.json")
if err != nil {
    t.Fatal(err)
}
if err := o.DumpProfile(f); err != nil {
    t.Fatal(err)
}

というわけで、時間計測ライブラリ stopw の紹介でした。

良かったら使ってみてください。

シナリオテスティングツール/パッケージであるrunnをgRPCに対応させた

なんとかgRPC対応ができました。

「そもそもrunnって何?」については会社のテックブログにエントリを書きましたのでそちらをご覧ください。

tech.pepabo.com

なお、runnを使ったgRPCのシナリオテストの実戦投入は私もまだなので、もし触ってくださる方がいましたら是非フィードバックをお待ちしています。

gRPC Runner

runnでシナリオの各ステップを実行するコンポーネントをRunnerと呼んでいるのですが、今回gRPC Runnerを追加しました。

runners: セクションで grpc:// というschemeでgRPC Runnerを作成できるようにしています。

次の例だと greq という名前をつけたgRPC Runnerを作成しています。

runners:
  greq: grpc://grpc.example.com:80

443ポートにするとTLSを使用するようになります。細かく設定する場合は以下のように書けます。

runners:
  greq:
    addr: grpc.example.com:8080
    tls: true
    cacert: path/to/cacert.pem
    cert: path/to/cert.pem
    key: path/to/key.pem
    # skipVerify: false

私が主に想定しているユースケースが「開発しているgRPCサーバのシナリオテスト」なので、大抵は次のようなコードで runners: セクションの設定を上書きします(次の例だと greq を runn.Runner() で上書きしています)。

func TestServer(t *testing.T) {
    addr := "127.0.0.1:8080"
    l, err := net.Listen("tcp", addr)
    if err != nil {
        t.Fatal(err)
    }
    ts := grpc.NewServer()
    myapppb.RegisterMyappServiceServer(s, NewMyappServer())
    reflection.Register(s)
    go func() {
        s.Serve(l)
    }()
    t.Cleanup(func() {
        ts.GracefulStop()
    })
    opts := []runn.Option{
        runn.T(t),
        runn.Runner("greq", fmt.Sprintf("grpc://%s", addr),
    }
    o, err := runn.Load("testdata/books/**/*.yml", opts...)
    if err != nil {
        t.Fatal(err)
    }
    if err := o.RunN(ctx); err != nil {
        t.Fatal(err)
    }
}

gRPCリクエストの書き方

4種類の通信方式はそれぞれ次のようにかけます。

なお、protoファイルは https://github.com/k1LoW/runn/blob/main/testdata/grpctest.proto を想定しています。

Unary RPC

headers: と message:` でリクエストを投げることができます。レスポンスはHTTP Runnerと同様に自動で記録され使用できます。

steps:
  hello_unary:
    desc: Request using Unary RPC
    greq:
      grpctest.GrpcTestService/Hello:
        headers:
          authentication: tokenhello
        message:
          name: alice
          num: 3
          request_time: 2022-06-25T05:24:43.861872Z
    test: |
      steps.hello_unary.res.status == 0 && steps.hello_unary.res.message.num == 3

Server streaming RPC

Server streaming RPCもUnary RPCと同様に headers:message: でリクエストを投げることができます。

レスポンスは複数のメッセージが返ってくることがあるため steps.<step_id>.res.messages に配列でレスポンスメッセージが記録されます。

steps:
  hello_server_streaming:
    desc: Request using Server streaming RPC
    greq:
      grpctest.GrpcTestService/ListHello:
        headers:
          authentication: tokenlisthello
        message:
          name: bob
          num: 4
          request_time: 2022-06-25T05:24:43.861872Z
    test: |
      steps.hello_server_streaming.res.status == 0 && len(steps.hello_server_streaming.res.messages) == 2 && steps.hello_server_streaming.res.messages[1].num == 34

Client streaming RPC

Client streaming RPCは複数のメッセージを送るので message: の代わりに messages: セクションで複数のリクエストを投げることができます。

steps:
  hello_client_streaming:
    desc: Request using Client streaming RPC
    greq:
      grpctest.GrpcTestService/MultiHello:
        headers:
          authentication: tokenmultihello
        messages:
          -
            name: alice
            num: 5
            request_time: 2022-06-25T05:24:43.861872Z
          -
            name: bob
            num: 6
            request_time: 2022-06-25T05:24:43.861872Z
    test: |
      steps.hello_client_streaming.res.status == 0 && steps.hello_client_streaming.res.message.num == 35

Bidirectional streaming RPC

同期的にではありますがBidirectional streaming RPCもサポートしています。 messages: セクションで receive キーワードでサーバからのメッセージを1つ受信し、close キーワードで通信を閉じます。

  hello_bidi_streaming:
    desc: Request using Bidirectional streaming RPC
    greq:
      grpctest.GrpcTestService/HelloChat:
        headers:
          authentication: tokenhellochat
        messages:
          -
            name: alice
            num: 7
            request_time: 2022-06-25T05:24:43.861872Z
          - recieve # recieve server message
          -
            name: bob
            num: 8
            request_time: 2022-06-25T05:24:43.861872Z
          -
            name: charlie
            num: 9
            request_time: 2022-06-25T05:24:43.861872Z
          - close # close connection
    test: |
      steps.hello_bidi_streaming.res.status == 0 && len(steps.hello_bidi_streaming.res.messages) == 1

gRPCサポートを終えて

まだ実戦投入していないのでアレですが、これでrunnで最低限欲しいと思っていた機能がひと通り揃いました。

あとはテックブログに書いたような機能を必要性やモチベーションに応じて追加したり、細かいところをちまちまと整備したりしようかと思います*1

そう言えば、gRPCのテストを書くために grpcstub を作ったりしたのですが、それはまたどこかで紹介したいです。

なお、runnを使ったgRPCのシナリオテストの実戦投入は私もまだなので、もし触ってくださる方がいましたら是非フィードバックをお待ちしています(大事なことなので2回(ry)。

*1:そういえば Flaky なテストが残っていて(TestShardという名前のテストでYAMLのパースが時々失敗しているように見える)、それも原因を特定したい。。

gomockで「呼ばれるべき」と指定した関数の非同期な実行を待ちたいのでテストヘルパーを書いてみた

gomockでデータベース操作などをモックすることはよくあると思います。

そのテスト関数内で「呼ばれるべき(EXPECT())」と指定した関数に非同期に実行される挙動がある場合、テスト関数の書き方によっては実行終了までにその関数が実行されないことがあり、結果としてテストがエラーになってしまうことがあります。 そして、エラーになったりエラーにならなかったりしてくると、Flakyなテストの出来上がりです*1

(「そもそもテストしやすいように〜すべきだ」というテスト容易性については一旦傍に置いておきます)

ものすごい雑コードですが、例えば以下のような感じのテストです*2

func TestAsyncExec(t *testing.T) {
    ctrl := gomock.NewController(t)
    t.Cleanup(func() {
        ctrl.Finish()
    })
    m := NewMockQuerier(ctrl)
    m.
        EXPECT().
        Exec("INSERT INTO users (name) VALUES ('alice');").
        Return(nil)

    go func() {
        time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
        m.Exec("INSERT INTO users (name) VALUES ('alice');")
    }()
}

実際に動くコードはこちらです。

実行すると以下のようなエラーが出ます。goroutine内の処理が実行しきる前にテスト関数の実行が完了するからですね。

=== RUN   TestAsyncExec
    controller.go:269: missing call(s) to *main.MockQuerier.Exec(is equal to INSERT INTO users (name) VALUES ('alice'); (string)) /tmp/sandbox509983514/prog.go:24
    controller.go:269: aborting test due to missing call(s)
--- FAIL: TestAsyncExec (0.00s)
FAIL

Program exited.

「テスト関数の終了をちょっとだけ待ってほしい」だけなんですが、なんとなくの待ち時間を決めて time.Sleep() を使うのもちょっとなあと思ってしまいます。

なので、gomockで「呼ばれるべき」と指定した関数が全て実行されるのを一定間隔で確認するテストヘルパーを書いてみました。

func waitMockCall(t *testing.T, ctrl *gomock.Controller, timeout time.Duration) {
    t.Helper()
    called := false
    done := make(chan interface{})

    go func() {
        time.Sleep(timeout)
        close(done)
    }()

    for {
        select {
        case <-done:
            break
        default:
        }

        var c reflect.Value = reflect.ValueOf(ctrl).Elem()
        var cs reflect.Value = c.FieldByName("expectedCalls").Elem()
        i := cs.FieldByName("expected").MapRange()
        called = true
        for i.Next() {
            calls := i.Value()
            if calls.Len() != 0 {
                called = false
            }
        }
        if called {
            break
        }
        time.Sleep(10 * time.Millisecond)
    }

    if !called {
        t.Log("missed call in time")
    }
}

waitMockCall(t *testing.T, ctrl *gomock.Controller, timeout time.Duration) を、テスト関数の末尾で呼び出しておけば、EXPECT()した関数が全て呼ばれるまで一定期間待つようになります。

実際に動くコードはこちらです。

実行すると以下のように成功するはずです。

=== RUN   TestAsyncExec
--- PASS: TestAsyncExec (0.09s)
PASS

Program exited.

feat: Add AllExpectedCallsSatisfied to Controller by FlorianLoch · Pull Request #627 · golang/mock · GitHub らへんのPull Requestが取り込まれていればよかったのですが、まだだったので reflect パッケージを使って使えそうな値を引っ張り出しています。

で、結局どうするのが正解?

みなさんはどうしていますか?是非知りたいです。

*1:「フレイキーテスト」。最近覚えた言葉です。https://www.publickey1.jp/blog/22/itjenkinsdevops_days_tokyo_2022.html

*2:「なんで突然の goroutine なんだよ!」というのは、簡単にするためにテスト関数内に書いているだけで本当は何か別の関数内で実行されるものとしてください

octocovはコードカバレッジを確認するCLIツールとしても使える

octocovは私が開発しているコードメトリクス計測のためのツールキットです。

主となる用途であるCI(GitHub Actions)に組み込む使い方は会社のテックブログで紹介させてもらいましたのでそちらをご覧ください。

tech.pepabo.com

私自身もドックフーディングをしていて、自分の主だったリポジトリはすでに octocov に移行済みです。

Fix handling for bind runner by k1LoW · Pull Request #47 · k1LoW/runn · GitHub

実は octocov にはもうひとつ便利な使い方があるので、そちらを紹介したいと思います。

コードカバレッジを確認する

octocov は実は単体のCLIツールとしても使うことができます。.octocov.yml といった設定ファイルも一切必要ありません。ただ、インストールするだけでOKです。

macOSだとHomebrew経由でインストール可能です。

$ brew install k1LoW/tap/octocov

あとは octocov が対応しているフォーマットカバレッジレポートを出力しているリポジトリでレポートファイルを指定して octocov コマンドを実行するだけです。

例えば、Goのプロジェクトだと以下のような感じです。

$ go test ./... -coverprofile=coverage.out -covermode=count
?       github.com/k1LoW/octocov        [no test files]
ok      github.com/k1LoW/octocov/central        3.682s  coverage: 73.1% of statements
?       github.com/k1LoW/octocov/cmd    [no test files]
ok      github.com/k1LoW/octocov/config 1.264s  coverage: 64.6% of statements
ok      github.com/k1LoW/octocov/datastore      1.690s  coverage: 43.0% of statements
?       github.com/k1LoW/octocov/datastore/artifact     [no test files]
?       github.com/k1LoW/octocov/datastore/bq   [no test files]
?       github.com/k1LoW/octocov/datastore/gcs  [no test files]
?       github.com/k1LoW/octocov/datastore/github       [no test files]
ok      github.com/k1LoW/octocov/datastore/local        0.796s  coverage: 80.0% of statements
?       github.com/k1LoW/octocov/datastore/s3   [no test files]
ok      github.com/k1LoW/octocov/gh     1.848s  coverage: 16.1% of statements
ok      github.com/k1LoW/octocov/internal       0.712s  coverage: 84.8% of statements
ok      github.com/k1LoW/octocov/pkg/badge      1.929s  coverage: 72.1% of statements
ok      github.com/k1LoW/octocov/pkg/coverage   2.606s  coverage: 88.4% of statements
ok      github.com/k1LoW/octocov/pkg/pplang     0.747s  coverage: 77.3% of statements
ok      github.com/k1LoW/octocov/pkg/ratio      1.062s  coverage: 83.2% of statements
ok      github.com/k1LoW/octocov/report 1.193s  coverage: 68.7% of statements
?       github.com/k1LoW/octocov/version        [no test files]
$ octocov -r coverage.out

                      main (9244fb7)
--------------------------------------
  Coverage                     66.8%

うまく Coverage が取得できていることが確認できたら octocov ls-files でファイルごとのカバレッジを見てみましょう。

$ octocov -r coverage.out ls-files
 73.1% [ 98/134] central/central.go
  0.0% [   0/29] config/build.go
 56.7% [ 89/157] config/config.go
 90.9% [  10/11] config/generate.go
 84.3% [ 91/108] config/ready.go
 77.8% [  42/54] config/yaml.go
 45.4% [ 49/108] datastore/datastore.go
  0.0% [    0/6] datastore/hint.go
 80.0% [  16/20] datastore/local/local.go
 16.1% [ 54/335] gh/gh.go
 83.9% [  52/62] internal/path.go
100.0% [    4/4] internal/value.go
 72.1% [  31/43] pkg/badge/badge.go
 91.5% [  43/47] pkg/coverage/clover.go
 91.7% [  44/48] pkg/coverage/cobertura.go
 84.7% [144/170] pkg/coverage/coverage.go
 96.7% [  29/30] pkg/coverage/diff.go
 90.9% [  40/44] pkg/coverage/gocover.go
 90.6% [  48/53] pkg/coverage/jacoco.go
 84.1% [  58/69] pkg/coverage/lcov.go
 90.6% [  29/32] pkg/coverage/merge.go
 89.9% [  62/69] pkg/coverage/printer.go
 87.8% [  65/74] pkg/coverage/simplecov.go
 77.3% [  34/44] pkg/pplang/pplang.go
 60.4% [  32/53] pkg/ratio/copy_from_gocloc.go
 92.9% [  26/28] pkg/ratio/merge.go
 94.2% [  81/86] pkg/ratio/ratio.go
 66.7% [132/198] report/diff_report.go
 70.2% [193/275] report/report.go

さらに octocov view でファイルを指定してテストがカバーしている行も確認できます。

$ octocov -r coverage.out view pkg/coverage/simplecov.go
[...]
112   3371              lcs := fcov.Blocks.ToLineCoverages()
113   3371              fcov.Total = lcs.Total()
114   3371              fcov.Covered = lcs.Covered()
115   3371              cov.Total += fcov.Total
116   3371              cov.Covered += fcov.Covered
117             }
118
119     11      return cov, rp, nil
120        }
121
122     16 func (s *Simplecov) detectReportPath(path string) (string, error) {
123     16      p, err := os.Stat(path)
124     16      if err != nil {
125                     return "", err
126             }
127     18      if p.IsDir() {
128      2              // path/to/coverage/.resultset.json
129      2              np := filepath.Join(path, SimplecovDefaultPath[0], SimplecovDefaultPath[1])
130      4              if _, err := os.Stat(np); err != nil {
131      2                      // path/to/.resultset.json
132      2                      np = filepath.Join(path, SimplecovDefaultPath[1])
133      2                      if _, err := os.Stat(np); err != nil {
134                                     return "", err
135                             }
136                     }
137      2              path = np
[...]

1列目が行数、2列目がテストにおいて通った回数、3列目が実際のコード行になります。

ターミナル上でカバレッジを確認できるのは意外に便利

私は今までファイル単位のコードカバレッジを見るためには、カバレッジレポートをHTMLで出力してブラウザで見たり、SaaSが提供している画面で見たりする方法しか持っていませんでした。

ターミナルでコードカバレッジをみるというのはターミナル内で開発をする自分のスタイルにもあっています。cat でファイルの中身をみるレベルでカバレッジもみることができるようになって便利です。

octocovは、Goのカバレッジフォーマットだけではなく、Rubyでよく使われるSimpleCovや、PHPUnitが出力可能なClover XMLや、Java/Kotlinでよく使われているらしいJaCoCoや、その他にもLCOVやCoverturaにも対応していますので、是非使ってみてください。

椅子と机が最終形態になった

椅子と机が最終形態となりました。

もう、壊れない限りは「他のものが欲しい」とならないと思います。

Mirra 2 Chairs

椅子です。

www.hermanmiller.com

2020年にリモートワークが増えることになったことをきっかけに購入しました。

会社ではとても良い椅子を提供してもらっていたので、それがなくなると身体的に大変なことになるかも?と思っていろいろ調べていた記憶があります。

当時、各所で椅子購入ブームだったので、得られる情報も多くとても迷いました。

決め手はハーマンミラーというブランドへの信頼と、調節要素の多さでした。

というのも椅子は奥さんと共有して使用する予定だったので*1、それぞれの体型や姿勢にあった調整ができたほうが良いだろうと考えました。

さらに、コロナ禍で店舗もあまり空いておらず、椅子ブームで在庫もなく、結局「座って確認」ができませんでした。そう言った意味でもブランドへの信頼や調節要素によるバッファが重要になりました。

結構な賭けでしたが購入を決断し、結果としてはとても満足して座れています。

PREDUCTS DESK - METRO / FlexiSpot E7

机です。

DESK - METRO / FlexiSpot E7preducts.jp

どの経路で知ったのかは覚えていないのですが、「PREDUCTSという会社ができてモジュール式のデスクを販売するよ」というのを知ってから、ずっとウォッチしていました。

note.com

「この机が欲しい」なんてことは今までの人生で思ったことがなかったのですが、これは欲しいと思ってしまい時々眺める日々。

実はそれまでのリモートワークでは奥さんが持ち込んだ机を共用させてもらっていました。

そろそろ「机もちゃんと別にあったほうがいいよね」と思いはじめたとても良いタイミングで昇降デスク版が出てしまい(?)、とても高い買い物なのでものすごく悩んだのですが、購入ボタンを押しました。

TwitterのTLを見ていると若干FORESTのほうが人気みたいなのですが、私はMETROを選択しました。

天板は艶消しブラックのメラミン化粧板らしいのですが、手触りもよく大満足です。

また、PREDUCTS代表のGo Andoさんの真似をしてキャスターをつけています。

私の部屋は広くないので「机の左右に人が立って机を持ち上げる」ということができないため、掃除や模様替えで苦労するかもしれないので念のためというのが理由です。

キャスターは重い昇降デスクを支えることになるので、何も考えずFlexiSpotが推奨しているものにしました。

flexispot.jp

「机にキャスターをつける」これが大成功で「気分によって机の位置を変える」こともできそうなくらい簡単に移動できるようになりました*2

そして今は「何でもかんでも机の下にぶら下げたい病」にかかっています。

電源やスマホ以外にもウェットティッシュボックスとハンディワイパーボックスは成功しました。山崎実業のマグネット付きのボックスがいい感じです。

残念ながらレールには直接マグネットでつけることはできませんでしたが、トレイやデスクの脚にはつけれるので貼り付けています。

ベーススライダーに適当な鉄製の小さめのボードをつけてもいけそうです。

今はゴミ箱をどうにかしていい感じにぶら下げられないかなあと思っています(今はフックスライダーにゴミ袋をかけているだけ)。

これからもDIYなしに拡張ができそうで楽しみです。

本当に良いものをずっと使うということ

私は「良いものをずっと使う」という考えを持っていなくて「リーズナブルで良さそうなものを使う」という感覚でした。

一方で奥さんは「良いものを大切にずっと使う」を地で行く人です。

彼女は時々(私にとっては)高い買い物をしていてびっくりするのですが、その一方で使っているものが実は10年以上(下手したら20年以上)使い続けているものだったりしてもっとびっくりします(そうは見えないくらい綺麗に使っているのですが事実そうなのです)。そういった長寿なものがたくさんあります。というかそういったものばかりな気がするな。。。

大切に長く使えるのであれば本当に良い物にお金を払うのはトータルで満足度が高いんだなあと思いました。

私はなかなか「大切に使う」が下手なのですが、「良いものを大切にずっと使う」を実践している彼女を見ると実践してみたい、見習いたいと思います*3

今回、椅子と机が本当に良い物(そしてお気に入り)になったので、大切に使っていこうと思います。

そういえば

他に最終形態になったものとして、バックパックがあるのですがこちらもずっと満足して使えています。こちらも良い物だった。

k1low.hatenablog.com

追記

「写真がない」というコメントをいただいたので貼っておきます*4

*1:今は別々になりました

*2:まだその機会は訪れていません

*3:PREDUCTS DESKの購入に悩んでいる時も「ずっと使うんでしょ?だったら月1万円で15ヶ月と考えたら余裕じゃないの?」と言われて購入に踏み切った気がします。よく考えたら5年も10年もずっと使う物だった。

*4:写真下手で椅子と机に申し訳ない...

PHPerKaigi 2022でGitHub Actionsについて発表した #phperkaigi

参加してきましたPHPerKaigi 2022

phperkaigi.jp

感想

一言で言うと「オフライン参加の人たちが羨ましい!!!」でした。

オンライン開催でも本当に参加してよかったと思えたのですが、時折垣間見えるオフライン参加の人たちのワイワイ感が楽しそうでした。

オンライン参加しなかったのは完全に自分の選択であって誰のせいでもないのです。ただ、「オンラインでも楽しいけど、やっぱりオフライン楽しいんだな」という感じです( ep 67 @fortkle @o0h_と、私の尊敬する若手スペシャル by Yokohama North AM でも楽しそうに語っていて羨ましかったです)。

逆にオンラインの場を設けてもらって、地方勢や家庭イベント勢には本当にありがたいと思いました*1

GitHub Actions Deep Dive using PHP

今回はGitHub Actionsについてちょっとだけ濃い話を発表してきました。

私は、自分のOSSのCI/CD環境だけでなく所属会社であるGMOペパボでもGitHub Actionsにどっぷりなので「多分普通よりもGitHub Actionsを使っているだろう」ということで発表題材にしました。

Upload Artifact

発表の中では余談として話した「Upload ArtifactはAPIドキュメントにない」というお話ですが、なぜこんなマニアックな挙動を調べたかというと、「Artifactを使った実装を自作Actionで完結させて提供したかったから」です。

具体的には何に使っているかと言うと octocov のコードメトリクスレポートの保存先です。Artifactを使うことでリポジトリ内で完結した状態で、外部サーバやストレージなどに頼らずにPRごとにカバレッジの差分表示などを出すことができるので便利なのです。

言語はGoですが、Upload Artifactの実装はパッケージに切り出していますので、「Artifactを使った実装を自作Actionで完結させて提供したい」という奇特な方はご利用ください。

github.com

New Action

発表のインパクトになればいいなーと、今回の発表のためだけに2つActionを作成しました。

github.com

github.com

とはいえサンプルなどではなく、一応ちゃんと作ったつもりですので使うタイミングがあったらぜひ使ってみてください(マーケットプレイスにちゃんと登録しないとだ)。

私がGitHub Actionsに抱いているイメージ

これは賛同する人は少ないかもなのですが、私がGitHub Actionsに抱いているイメージは「CI/CD」というよりも「Serverless」です。

なんというかそれ自体の使い勝手が良いのと、コンピューティングリソースの運用をしなくていいので*2、なんでもかんでもGitHub Actionsで完結させたくなります。

「ハンマーを持つと全てが釘に見える」というアレですね。

これからも「いち実行環境」として便利に使っていくんだろうなと思います。

PHPerKaigi 2022 お疲れさまでした!!

運営の皆さん、発表者の皆さん、参加者の皆さん、お疲れさまでしたー!

楽しかったです!

*1:ただ、ただ、モジュラモノリス発表x2の合同アフタートークがあったりしたらしく、それは聞きたかったなーと

*2:正確には他の誰かに丸投げしている