rcmdnk's blog
Last update

20230122_coverage_report_200_200

GitHub Actionsを使ってコードのテストを行う際、テスト結果を見るだけなら ログを見るだけで十分なことが場合が多いですが、coverageの結果を見ようと思うと テキスト情報だけだとちょっと見づらいこともあります。

なのでcoverageの結果をHTMLで出してくれるツールなどもあるので、 そのcoverageの結果を別のbranchへpushして見れるようにしてみました。

ここではPythonのpytestを使った話です。

pytestのcoverageの出力

pytest を使ったテストの結果のcoverageを出します。

coverageを出すには別途 pytest-covというプラグインが必要です。

両方ともpipで入れられます。

$ pip install pytest pytest-cov

これで、pytest--covというオプションが使えるようになり、--cov=srcのようにディレクトリを指定するとその中の コードをどれだけカバー出来たかを調べてくれるようになります。

pytest-covでは--cov-report=htmlとするとhtmlcovというディレクトリの中にHTMLのレポートを作ってくれます。 ただ、このレポートはcssやjsを含むリッチなHTMLになっていて、これ全体をサイトとして公開するようなことをしないと見れません。

やるのであればGitHub Pagesを使ってこのレポートを表示できるようにする、という方法もありますが、今回はもうちょっとライトにやる方法を使います。

Pytest Coverage Comment

pytestのcoverage結果をいい感じの1 page HTMLに直してくれるActionが公開されています。

Pytest Coverage Comment · Actions · GitHub Marketplace

20230122_coverage_report.png

こんな感じのレポートを作ってくれます。

Missing(カバーできてない部分)は行数を表していて、それぞれGitHub上のデフォルトブランチのコードの該当部分へのリンクとなっています。

これを使うために、workflow内でのpytestを行うための部分のstepは

  - name: Run test
    run: pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=src tests/ | tee pytest-coverage.txt

のような形で--junitxmlオプションを追加し、また--cov-reportterm-missing:skip-coveredにしておきます。 また、出力をpytest-coverage.txtというファイルにも書き出すようにします(teeを使ってログにも出すように)。 最後のtestsはテスト用のファイルが入っているディレクトリの指定です。

このあとに以下のようなstepを実行します。

  - name: Pytest coverage comment
    uses: MishaKav/pytest-coverage-comment@main
    id: coverageComment
    with:
      hide-comment: true
      pytest-coverage-path: ./pytest-coverage.txt
      junitxml-path: ./pytest.xml

このstepを実行すると、stepのoutputsとしていくつか出力が得られて、その中のsummaryReportというのがMarkdown用の出力になっています。

Pytest Coverage CommentのREADMEの説明だと、

  • coverageHtml: Html with links to files of missing lines. See the output-example
  • summaryReport: Markdown with summaryof: Tests/Skipped/Failures/Errors/Time

となっていますが、実際にはsummaryReportの方はcoverageHtml + Markdown (table)のようになっているので、 summaryReportの方だけを出力するようにすればOKです。

多分以下がこれに関する話。

Duplicated coverage badge · Issue #82 · MishaKav/pytest-coverage-comment

上ではcoverageHtmlsummaryReportを両方書き出すと重複する部分がある、というレポートに対してcoverageHtmlだけにしてください、となってますが、 summaryReportの方がテーブルが追加されてるのでそちらを使った方が良いかと思います。

coverageブランチにsummaryReportを載せる

通常のmainブランチにpushした際にテストを実行し、そのcoverageの結果をcoverageというブランチに出力することにします。

このcoverageは開発などでも使わないようにします。

全体的な例としてはこんな感じ。

test.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
name: test

on:
  push:
    branches-ignore:
      - "coverage"
  pull_request:

jobs:
  test:
    strategy:
      matrix:
        os: [macos-12, ubuntu-latest]
        python-version: ['3.10', '3.9']
    runs-on: ${{ matrix.os }}
    permissions:
      contents: write
    steps:
      - name: Check is main
        id: is_main
        run: |
          if [ "${{ github.ref }}" = "refs/heads/main" ] && [ "${{ matrix.os }}" = "ubuntu-latest" ] && [ "${{ matrix.python-version }}" = "3.10" ];then
            echo "flag=1" >> $GITHUB_OUTPUT
          else
            echo "flag=0" >> $GITHUB_OUTPUT
          fi
      - uses: actions/checkout@v3
        with:
          persist-credentials: false
          fetch-depth: 0
      - uses: actions/setup-python@v4
        with:
          python-version: ${{matrix.python-version}}
      - name: Install poetry
        run: pip install poetry
      - name: Poetry setup
        run: poetry install
      - name: Run test
        id: pytest
        continue-on-error: true
        run: poetry run pytest --durations=0 --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=src tests/ | tee pytest-coverage.txt
      - name: Pytest coverage comment
        if: ${{ steps.is_main.outputs.flag == '1' }}
        id: coverageComment
        uses: MishaKav/pytest-coverage-comment@main
        with:
          hide-comment: true
          pytest-coverage-path: ./pytest-coverage.txt
          junitxml-path: ./pytest.xml
      - name: Update Readme in coverage branch
        if: ${{ steps.is_main.outputs.flag == '1' }}
        run: |
          coverage=$(git branch -a|grep "remotes/origin/coverage$") || :
          if [ -z "$coverage" ];then
            git checkout --orphan coverage
            git rm -rf .
          else
            git checkout coverage
          fi
          echo "[![test](https://github.com/$GITHUB_REPOSITORY/actions/workflows/test.yml/badge.svg)](https://github.com/$GITHUB_REPOSITORY/actions/workflows/test.yml)" > ./README.md
          echo -e ${{ steps.coverageComment.outputs.summaryReport }} >> ./README.md
      - name: Commit
        if: ${{ steps.is_main.outputs.flag == '1' }}
        run: |
          git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add README.md
          git commit -m "Update coverage"
      - name: Push
        if: ${{ steps.is_main.outputs.flag == '1' }}
        uses: ad-m/github-push-action@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          branch: coverage

onのトリガー部分ではcoverageだけ外すようにします。 coverageブランチでは.github/workflowsを作らないので無くても実行はされませんが一応。

jobs.test.permissionsの設定はsecrets.GITHUB_TOKENpushする権限を持たせるために必要です。 以前はすべてのwrite権限を持ってましたが、現在はデフォルトでread onlyになっているので contentsに対してのwriteを加えるようにするか、もしくはレポジトリの設定ですべてのwrite権限を加える必要があります 1

coverageブランチに載せるのはmainブランチだけにします。 また、テストが複数のOSやPythonの組み合わせで行われる場合にも1つだけを選んで載せるようにします。

その作業をis_main stepで行って、このflag1の時だけ後のcoverageブランチへの出力のstepを実行するようにしています。

pytestを行う前の部分でpoetryを実行していますがこれはpoetryを使ったレポジトリの例ということで。

そうでない場合は適当なセットアップを入れてください。また、Run testのところでもpoetryを使ってるのでpoetry runをつけているのでその部分も消してください。

その後はcoverageブランチへレポートを載せるための作業です。

  • Pytest coverage comment: レポート作り
  • Update Readme in coverage branch: coverageブランチへの切り替え(なければ作成)、README.mdにworkflowのstatus badgeとsummaryReportを書き込み。
  • Commit: coverageブランチへのcommit
  • Push: coverageブランチをpush

commitする際にはGitHub Actionsで行ったことがわかるようにユーザーをgithub-actions[bot]にしておきます。

これで以下の例のようにcoverageというブランチでレポートが見られます。

rcmdnk/homebrew-file at coverage

mainブランチのREADMEの方に

README.md
1
2
[![Test](https://github.com/rcmdnk/homebrew-file/actions/workflows/test.yml/badge.svg)](https://github.com/rcmdnk/homebrew-file/actions/workflows/test.yml)
[![Coverage Status](https://img.shields.io/badge/Coverage-check%20here-blue.svg)](https://github.com/rcmdnk/homebrew-file/tree/coverage)

みたいな感じで Shields.ioを使って他のbadgeと並べて リンクを張っておけば見やすいかと思います。

以下のような感じ。

rcmdnk/homebrew-file: Brewfile manager for Homebrew

ここにcoverageブランチにあるようなパーセンテージ付きのバッジがあった方がよりよいとは思うのですが、 mainのブランチをGitHub Actionsで変更してしまうとコード変更してpushするたびにcoverage用のcommitが入って汚れてしまいます。

その辺気にしなければ、summaryReport自体をmainのREADMEに載せてしまうのもありだとは思います。

coverageブランチにsummaryReportを載せる

もしmainブランチのcommitが汚れても問題ない、という場合には以下のようにして mainブランチに載せる事もできます。(Pytest Coverage CommentのREADMEで説明されているやり方。)

その場合はmainREADME.mdの先頭に

README.md
1
2
3
4
5
6
7
# My Repository
Repository explanation.

<!-- Pytest Coverage Comment:Begin -->
<!-- Pytest Coverage Comment:End -->

...

のようにPytest Coverage Comment用のタグを書いておいて、 Update Readme in coverage branchの以下の部分を以下のUpdate Readme with Coverage Htmlの以下のようなstepに書き換えます。

test.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
      - name: Update Readme with Coverage Html
        run: |
          sed -i "/<!-- Pytest Coverage Comment:Begin -->/,/<!-- Pytest Coverage Comment:End -->/c\<!-- Pytest Coverage Comment:Begin -->\n[![test](https://github.com/$GITHUB_REPOSITORY/actions/workflows/test.yml/badge.svg)](https://github.com/$GITHUB_REPOSITORY/actions/workflows/test.yml)\n$(echo ${{ steps.coverageComment.outputs.summaryReport }})\n<!-- Pytest Coverage Comment:End -->" ./README.md
      - name: Commit
        if: ${{ steps.is_main.outputs.flag == '1' }}
        run: |
          git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add README.md
          git commit -m "Update coverage"
      - name: Push
        if: ${{ steps.is_main.outputs.flag == '1' }}
        uses: ad-m/github-push-action@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          branch: main

最後のpushmainにするのを忘れずに。

追記: 2023/02/05

もっと気軽にジョブ概要欄に出す方法もあります。

追記ここまで

Sponsored Links
Sponsored Links

« Python 3.10で導入された構造的パターンマッチで正規表現を使ったmatchを行う GitHub Actionsを手動実行するworkflow_dispatch »

}