きり丸の技術日記

技術検証したり、資格等をここに残していきます。

JavaをSonarCloudでカバレッジ取りながら静的分析する(Gradle, 2021年)

2021年になってGitHub Actions等のCIを使ったSonarCloud連携がしやすくなったので、手順を纏めます。

今回の記事では、JaCoCoでカバレッジを取得して、コードをSonarCloudへ連携できることをゴールとします。

環境

  • Java
    • 11
  • JaCoCo
  • SonarQube
    • 3.3
  • Gradle
    • 7.0.2
  • SonarCloud

書かないこと

  • SonarCloudについて
  • SonarCloudの設定
  • sonar-scannerでの連携
    • SonarQubeの構築記事で紹介予定です
  • GitHub Actionsのsecretの使用方法

ゴール

  • GitHub Actionsでコミットされると自動でSonarCloudに解析される
  • カバレッジも連携する

f:id:nainaistar:20210616193630p:plain

手順

解析対象のソースがあるpublicリポジトリを用意する

SonarCloudが解析できるように、publicリポジトリを作成します。特にこだわりがなければ、GitHubで良いでしょう。また、解析対象のソースが無いとSonarCloudに反映されないので、最低限TDD等で複数サイクル回してコミットされている状態がオススメです。

SonarCloudのプロジェクトを作成する

SonarCloudのページから、作成したリポジトリのアカウントでログインします。

ログイン後、画面右上の+ボタンから「Analyze new project」ボタンで新しいプロジェクトを作成します。

f:id:nainaistar:20210616193648p:plain

個人、または組織を選択して、解析したいリポジトリにチェックを入れます。チェック後、「Set Up」ボタンをクリックします。

f:id:nainaistar:20210616193702p:plain

CIで連携するか、手動で連携するかを選ぶことができます。今回は「With GitHub Actions」を選択します。

f:id:nainaistar:20210616193720p:plain

その後、ログイン用のトークンが発行されます。後で使用しますので、メモしてください。メモ後に「Continue」ボタンをクリックします。

f:id:nainaistar:20210616193738p:plain

Gradleの設定とGitHub Actionsの設定が表示されます。ある程度は参考になりますが、手を加える必要があるので、その点は後述いたします。

f:id:nainaistar:20210616193755p:plain

build.gradleに必要なライブラリとタスクを設定する

build.gradleに必要なライブラリとタスクを設定します。本来であれば、ログイン用のトークンはGitHub Secretsで管理したほうが良いですが、手順を簡略化するために直接書き込みます。

plugins {
    id 'org.sonarqube' version '3.3' // SonarCloudへの転送用
    id "jacoco" // カバレッジを取得します
}

sonarqube {
    properties {
    // プロジェクト名
        property "sonar.projectKey", "hirotoKirimaru_sonar-cloud-practice" 
    // 分析対象の組織名
        property "sonar.organization", "hirotokirimaru-github" 
    // SonarCloudのURL
        property "sonar.host.url", "https://sonarcloud.io" 
    // ログイン用トークン
        property "sonar.login", "62fbe0ec5e29acce113b1edb376c800f6c57096e" 
    // カバレッジレポートの格納先
        property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/test/jacocoTestReport.xml"
    }
}

// テストレポートの出力
jacocoTestReport {
    reports {
        xml.enabled true
        html.enabled true
        csv.enabled false
    }
}

この時点で、SonarCloudへの連携が可能です。次のコマンドで、テストして、カバレッジレポートを出力し、カバレッジとソースコードをSonarCloudに連携することができます。GitHub Actionsで動かす前に、ここで機能確認することがオススメです。

./gradlew test 
./gradlew jacocoTestReport
./gradlew sonarqube

GitHub Actionsの設定

SonarCloudの画面との修正点は以下の通りです。設定値はbuild.gradle側に寄せているので、次のYAMLをコピペするだけで動くはずです。

  • gradlewのパーミッション付与
  • テスト実行
  • カバレッジレポート出力
  • SonarCloudへの連携
  • その他、不要な項目の削除
name: Build
on:
  push:
    branches:
      - master
  pull_request:
    types: [opened, synchronize, reopened]
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0  # Shallow clones should be disabled for a better relevancy of analysis
      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 11
      - name: Cache SonarCloud packages
        uses: actions/cache@v1
        with:
          path: ~/.sonar/cache
          key: ${{ runner.os }}-sonar
          restore-keys: ${{ runner.os }}-sonar
      - name: Cache Gradle packages
        uses: actions/cache@v1
        with:
          path: ~/.gradle/caches
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
          restore-keys: ${{ runner.os }}-gradle
      - name: chmod
        run: chmod 777 gradlew
      - name: test
        run: ./gradlew test
      - name: testReport
        run: ./gradlew jacocoTestReport
      - name: analyze
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # Needed to get PR information, if any
        run: ./gradlew sonarqube

注意点としては、SonarCloudのメインブランチは確認して下さい。私はメインブランチをmasterとしていますが、SonarCloudのメインブランチ名がmainとなっています。トップページはメインブランチの解析結果となりますので、ブランチごとの解析結果を確認したい場合は明示的に変更する必要があります。

「なぜかGitHub Actionsでは正常終了しているのに、結果が連携されない…」と、気になっている方はブランチ名を再確認してください。

f:id:nainaistar:20210616193814p:plain

f:id:nainaistar:20210616193827p:plain

ソースコード

SonarCloudはだいぶ連携が楽にはなりましたが、ブランチ名のデフォルトがmasterからmainに変更されたことでハマっていました。

GitHubのデフォルトブランチ名は未だにmasterを指定しているので、SonarCloud側のデフォルトブランチ名がmainになっていることは気付けませんでした。

Gitと連携できるサービス側でのデフォルトブランチ名は今後も気を付けないといけませんね。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

類似

f:id:nainaistar:20210616193942p:plain

Google スプレッドシートでフィルタした結果を元に操作する(SUBTOTALとFILTER)

Google スプレッドシートで管理していた障害管理表を元に、フィルタ機能で自分が担当した障害をフィルタしていました。そのまま原因分析をしようとしたところ、自分の担当した件数以上にデータを取得してしまいました。

画面のフィルタを活かす、またはフィルタと同等の機能を使えるようにするのが、この記事の目的です。

環境

  • Google スプレッドシート

仕様

2021年10月1日から2021年10月31日までの東京の平均気温が載っているCSVを元にする。土日の平均気温を調べたい。


次の画像はゴールを指定しています。画像では曜日のB列で「値でフィルタ」をして「土」・「日」をフィルタした結果を表示しているため、土日しか表示されていませんが、平日のデータも存在します。土日の平均気温は18.07℃、10月平均気温は18.17℃です。18.07℃を求められることがゴールです。

f:id:nainaistar:20211118225558p:plain

フィルタした結果を元に計算したい

SUBTOTAL関数を使用することで、画面上でフィルタしている状態の平均や合計を簡単に計算できます。

第一引数を変更することで、最小値、最大値等を求めることもできます。

# 平均を求めるには1を第一引数とする。
# 第二引数には範囲を指定します。
=SUBTOTAL(1,$C$4:$C$34)

f:id:nainaistar:20211118225558p:plain

SUBTOTAL関数の詳細は公式ページを参照してください。

  • 1 - AVERAGE
  • 2 - COUNT
  • 3 - COUNTA
  • 4 - MAX
  • 5 - MIN
  • 6 - PRODUCT
  • 7 - STDEV
  • 8 - STDEVP
  • 9 - SUM
  • 10 - VAR
  • 11 - VARP

また、1桁のコードの先頭に10を追加するか、2桁のコードの先頭に1を追加すると非表示の値を無視できるそうです。101であれば非表示の値を無視したうえでAVERAGE, 110であれば非表示の値を無視したうえでVARの関数で処理します。

フィルタした結果を元に処理したい

集合関数(AVERAGE、SUM、MAX等々)を使用したい場合はSUBTOTAL関数を使用すれば、画面上でフィルタした結果を使用できます。

それ以外の操作をしたい場合、FILTER関数を使用するとフィルタした結果を元に処理できます。ただし、FILTER関数では画面上でフィルタした結果を使用することはできません。画面上で使用しているフィルタと同等の条件をFILTER関数ですべて指定する必要があります。

FILTER関数の第一引数に、フィルタする前の範囲を指定します。第二引数以降にフィルタする条件を指定します。第二引数、第三引数以降はそれぞれの条件をANDで結合してフィルタします。もしOR条件でフィルタしたい場合は、条件を+で結合するとOR条件でフィルタできます。

# AND条件でフィルタしたい場合、第二引数以降にパラメータを指定する
=FILTER(範囲, A条件, B条件)

# OR条件でフィルタしたい場合、+で結合します
=FILTER(範囲, (A条件)+(B条件))

また、列数は一致している必要はありませんが、行数は一致している必要がありますので注意してください。

今回のユースケースの場合、B列が「土」・「日」のレコードのC列の平均気温を取得したいので、FILTER関数でフィルタした後に集合関数のAVERAGEを設定しています。

=AVERAGE(FILTER($B$4:$C$34, ($B$4:$B$34="土")+($B$4:$B$34="日")))

f:id:nainaistar:20211118225558p:plain

終わりに

フィルタした結果を別シートにコピー&ペーストして、必要なレコードを抽出してもよかったのですが、毎回この手作業やりたくなかったので方法を知ることができてよかったです。

今後もSUBTOTALFILTER関数を使って、余計な条件を省いた結果を容易に分析したいです。


…まぁ、本格的に分析したければ、SpreadSheetではなくBIツールを使った方がいいとは理解しています。とりあえず、さくっと確認したいときには使える関数です。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考情報

f:id:nainaistar:20211118225731p:plain

PythonでEnumをfor文で回したら値をすべて出力しなかった

パラメータの値を元に、PythonのEnumで定義した値を取得するメソッドを作ろうとしました。しかし、PythonのEnumの知識が弱かったため、事前に定義していたEnumをすべて取得できない事象が発生しました。

この記事では、PythonのEnumの仕様を確認して、Enumで定義した値をすべて取得できなかった原因をメモします。

環境

  • Python
    • 3.8.6

for文でEnumの値が取得できなかった原因

Enumに定義していた値(_value_)が重複していたため。

まとめ

  1. 名前(name)の重複は許さない
  2. 値(value)の重複は許す。しかし、実行時に名前ごと欠落する
  3. Enumをタプルで定義している場合に、_value_を上書きすると怪しい挙動になる
  4. 値が重複した状態でも、全部の名前を出力する方法はある。しかし、期待した値を取得できないことがある

動作確認

名前の重複は許さない

名前が重複している場合、実行時にエラーが発生します。

from enum import Enum

class Color1(Enum):
    RED = 1 
    RED = 2 

# TypeError: Attempted to reuse key: 'RED

値の重複は許すが、実行時に欠落する

名前と違って値は重複しても、実行時エラーは発生しません。しかし、値が重複していると名前ごと欠落します。

from enum import Enum

class Color1(Enum):
    RED = 1 
    BLACK = 2
    WHITE = 2
def test_foo():
    for element in Color1:
        print(element)
# 出力される値
# Color1.RED
# Color1.BLACK
# 出力されない
# Color1.WHITE

Enumをタプルで持たせたケース

ちなみに、私がハマったのは亜種です。Enumをタプルで持たせていたのですが、値を上書きしていたために目的の名前を取得できませんでした。_value_を上書きしない場合は、タプルのまま_valueにセットされますので問題は発生しません。

class Color2(Enum):
    RED = (1, 1)
    BLACK = (2, 2)
    WHITE = (3, 2)

    def __init__(self, id, value):
        self.id = id      
        # この1行で_value_を上書きすると、値重複が発生する
        self._value_ = value 

値が重複していても、名前を取得する

値が重複していた状態でも、名前を取得する方法はあります。定義したEnumに対して__members__.items():を使用することで、すべての名前を取得できます。ただし、注意してください。名前は取得できますが、は取得できない可能性があります。

先ほどのケースであるタプルでEnumを指定しており、_value_を上書きしているケースの場合、期待しない挙動をします。次の例では、WHITE = (3, 2)と定義しているので、取得したい値は(3, 2)ですが、_value_を上書きしているためにBLACKである(2, 2)を取得してしまいます。

class Color2(Enum):
    RED = (1, 1)
    BLACK = (2, 2)
    WHITE = (3, 2)

    def __init__(self, id, value):
        self.id = id      
        self._value_ = value 
def test_bar():
    for name, element in Color2.__members__.items():  
        print(name)
        print(element)

# 出力される値
# RED
# Color2.RED
# BLACK
# Color2.BLACK
# WHITE
# Color2.BLACK

# WHITEの下のColor2.BLACKはtypoではありません。

値の重複を許容しないようにする

Enum標準ライブラリのuniqueアノテーションを使用すると、値が重複していた際にも実行時エラーを発生させられます。

from enum import Enum, unique

@unique
class Color3(Enum):
    RED = (1, 1)
    BLACK = (2, 2)
    WHITE = (3, 2)

    def __init__(self, id, value):
        self.id = id      
        self._value_ = value

# E   ValueError: duplicate values found in <enum 'Color3'>: WHITE -> BLACK

ソースコード

終わりに

Pythonはメインで使用していない言語ですので、たまにハマりますね。

私には@uniqueで値を重複を許容しないようにするのか、__members__.items():を使用して値の重複を許容するのか、どちらがよい設計かは判断できません。@uniqueを使いつつ、タプルで定義して一意なIDを持たせておく方法が良さそうな予感はしていますが。

どちらにしてもPythonらしくない面倒な書き方だと思うので、もっと直感的に書けるようにしてほしいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考情報

f:id:nainaistar:20211120115830p:plain