この記事は、ソフトウェアテスト Advent Calendar 2018 の7日目の記事です。

ソフトウェアテスト Advent Calendar 2018
https://qiita.com/advent-calendar/2018/softwaretesting

CIツールで静的テストを実行し、
コードの品質を維持する取り組みを行っているプロジェクトは多いと思います。

静的テストのツールは、
checkstyle, PMD, JDepend, flake8, RuboCop, Brakeman, ESLintなど
いろいろとありますが。
IntelliJ IDEAのようなInspection機能があるIDEを導入していると、
IDEと静的テストツールの指摘に食い違いがあったり、
レビュアーが修正を手元のIDEに取り込んでチェックする手間が出てきたり、
など、面倒な事が出てきがちです。

そこで、このエントリでは、
IntelliJ IDEAのInspection機能をCircleCI上で実行する方法を説明します。

以下のように、
IntelliJ IDEAはInspection機能をコマンドラインで実行する事が出来るので、
この機能を活用します。

Running Inspections Offline | Intellij IDEA Help
https://www.jetbrains.com/help/idea/running-inspections-offline.html

適当なJavaのプロジェクトを用意する

まずは、適当なJavaのプロジェクトを用意します。

以下のようにディレクトリを準備します。

$ mkdir java-sample
$ cd java-sample
$ mkdir -p src/main/java

以下のファイルを作成します。

build.gradle

plugins {
    id 'java'
    id 'application'
}
mainClassName = 'Hello'

src/main/java/Hello.java

public class Hello {
  public static void main(String[] args) {
    System.out.println("Hello");
  }
}

gradleから実行出来ることを確認します。

$ gradle run

プロジェクトをIntelliJ IDEAにimportする

次に、作成したプロジェクトをIntelliJ IDEAにインポートします。

IntelliJ IDEA起動後、Import Projectを選んだ後。
先ほど作成したディレクトを指定、
Import project from external modelで、Gradleを選択します。

File -> Project Structure -> Projectとたどり、
Project SDKを指定します。
ここでは、Java 10を利用するので「10」を選択しておきます。

Inspectionsを設定してGitHubにpushする

Preferences -> Editor -> Inspectionsとたどります。

ProfileをStore in IDE - Defaultに切り替えた後、
右側の歯車メニューからCopy to Projectを選択し、
Profile名をDefaultに指定します。

intelliidea-ci_01

intelliidea-ci_02

この後、必要に応じてInspectionの設定は変更して下さい。

ここで、必要なファイルをGitHubにpushします。
gitignoreを以下のように、指定して置きます。

.gitignore

.gradle
build

.idea/*
!.idea/gradle.xml
!.idea/misc.xml
!.idea/inspectionProfiles

commitするファイルは以下のようになります。

  • build.gradle
  • src/main/java/Hello.java
  • .idea/gradle.xml
  • .idea/misc.xml
  • .idea/inspectionProfiles
  • .gitignore

これらのファイルを追加したら、GitHubにpushします。

DockerでInspectionを実行する

ここまでの流れで、
プロジェクトとInspectionルールの設定がGitHub上で共有されました。

ここでは、
InspectionをCircleCIで実行するためのDockerコンテナを用意します。

私が作成したDockerイメージをDockerHubに置いているので参考にして下さい。
元になったDockerfileもGitHubに上げています。

takemikami/ideaic-inspect | DockerHub
https://hub.docker.com/r/takemikami/ideaic-inspect

takemikami/docker-ideaic-inspect | GitHub
https://github.com/takemikami/docker-ideaic-inspect

※Java 10用のDockerfileは「10」ブランチにあります。
https://github.com/takemikami/docker-ideaic-inspect/tree/10

このイメージ(あるいは参考にして自分で作ったイメージ)を使って、
以下のように、コマンドラインからinspectionが実行出来ることを確認します。

docker run -i -v `pwd`:/project -v $HOME/.gradle:/root/.gradle -t takemikami/ideaic-inspect:10 /bin/bash
# cd /project
# mkdir -p report
# /opt/idea-IC/bin/inspect.sh `pwd`/build.gradle `pwd`/.idea/inspectionProfiles/Default.xml `pwd`/report -v2

結果は、report配下にxml形式で格納されます。

CircleCIでInspectionを実行する

CircleCIのサイトで、
ADD PROJECTS から対象プロジェクトを選び Set Up Project でCIを有効にします。

以下のような、CircleCIの設定ファイルを作成してpushします。

.circleci/config.yml

version: 2
jobs:
  build:
    docker:
      - image: takemikami/ideaic-inspect:10
    steps:
      - checkout
      - run:
          name: inspect
          command: |
            mkdir report
            /opt/idea-IC/bin/inspect.sh `pwd`/build.gradle `pwd`/.idea/inspectionProfiles/Default.xml `pwd`/report -v2            

この状態では、CiecleCIの画面からinspection結果を確認出来ないので、
inspectionの結果を標準出力に表示するシェルスクリプトを追加します。

.circleci/print-inspect.sh

#!/bin/bash

PROJECT_ROOT=$(cd $(dirname $0)/..; pwd)
ISSUE_CNT=0

for target_xml in `find $PROJECT_ROOT/report | grep xml | grep -v '/\.'`; do
  echo $target_xml
  problem_count=1
  while :
  do
    problem_xml=`echo "cat /problems/problem[${problem_count}]" | xmllint --shell $target_xml`
    if [ "${problem_xml}" = "/ > / > " ]; then
      break
    fi
    filename=`echo ${problem_xml} | sed -e "s/^.*<file>\([^<]*\).*$/\1/"`
    line=`echo ${problem_xml} | sed -e "s/^.*<line>\([^<]*\).*$/\1/"`
    description=`echo ${problem_xml} | sed -e "s/^.*<description>\([^<]*\).*$/\1/"`
    severity=`echo ${problem_xml} | sed -e "s/^.*severity=\"\([^\"]*\)\".*$/\1/"`
    source=`echo ${problem_xml} | sed -e "s/^.*<problem_class.*\">\([^<]*\).*$/\1/"`

    filename_regex="^file://"
    if [[ $filename =~ $filename_regex ]]; then
      filename=${filename:21} # ファイル名先頭の「file://$PROJECT_DIR$」を除く
    fi
    line_regex="^[0-9]*$"
    if [[ ! $line =~ $line_regex ]]; then
      line=""
    fi

    echo "$filename:$line [$severity] $description ($source)"
    if [ "$severity" != "TYPO" ]; then
      ISSUE_CNT=$((ISSUE_CNT + 1))
    fi
    problem_count=$((problem_count + 1))
  done
done

echo "$ISSUE_CNT issues."
if [ $ISSUE_CNT -gt 0 ]; then
  exit 1
fi
exit 0

実行権限も忘れずに。

$ chmod +x .circleci/print-inspect.sh

CircleCIの設定もシェルスクリプトを呼び出すように変更します。

.circleci/config.yml

version: 2
jobs:
  build:
    docker:
      - image: takemikami/ideaic-inspect:10
    steps:
      - run:
          name: install xmllint
          command: apt-get update && apt-get install -y libxml2-utils
      - checkout
      - run:
          name: inspect
          command: |
            mkdir report
            /opt/idea-IC/bin/inspect.sh `pwd`/build.gradle `pwd`/.idea/inspectionProfiles/Default.xml `pwd`/report -v2
            .circleci/print-inspect.sh            

以下のファイルをGiHubにpushしてPullRequstを上げます。

  • .circleci/config.yml
  • .circleci/print-inspect.sh

以下のように、
CircleCIでのCIに失敗し、PullRequestでも状態が確認出来ます。

intelliidea-ci_03

intelliidea-ci_04

ルールが厳しすぎる場合は、IDE上からInspectionルールの設定を変更して、 .idea/inspectionProfiles 以下をGitHubにpushします。

inspection結果の表示を、
PullRequestで修正した箇所のみに絞り込みたい場合は、
以下のエントリの方法を真似れば、対応することが出来ます。

RubocopでPullRequestの変更箇所で問題があった場合のみCIを失敗させる http://takemikami.com/2018/01/30/RubocopPullRequestCI.html

このようなプロセスを整備して、
機械に出来るテストは、どんどん機械にやらせていきましょう。

今後、このようなCIで使うと便利なシェルスクリプト群をまとめていこうと思っています。
現在作業中ですが、以下のリポジトリにまとめていく予定です。
https://github.com/takemikami/ci-utils