Ant with SonarQube|Piece of DevOps

雖然說現在使用到 Ant 的專案不多,但在公司裡遇到長壽型 or 化石級專案也是在所難免。如果你正在一個希望將 Code 品質提昇的團隊中,主管肯定會為這類型的專案爭取權利,請你將這些專案也一併送上 SonarQube。所以這次要介紹的是 : 如何透過 Ant 將專案推至 SonarQube

為了將 Ant 的專案送往 SonarQube,需要做許多的前置處理,這些處理的項目大致可以分為 :
  • 一個專案 (一切的起源)
  • 相關 Dependency (Jar)
  • 產生 Report :
    • Junit Report (for unit testing) 
    • Jacoco Report (for code coverage)
  • 設定 SonarScanner 的 Buildfile (sonar.xml)
以下將會依序介紹。

一個專案

開始都是最難的部分,像是怎麼樣才是正規專案結構呢?就像玩遊戲要取名字一樣艱辛。這部分我也苦惱了很久,最後還是不爭氣地直接參考 Spring-boot 範例專案的結構,建了一個專案,其目錄結構如下 :
  • lib -> Dependencies from Ivy
  • reports -> Reports from Junit and Jacoco
  • src
    • main -> Resource Root
      • helloAnt -> Package
        • Main
    • test -> Test Resource Root
      • helloAnt -> Package
        • MainTest
  • out -> output (class, jar ...)  
  • ivy.xml
  • testing.xml
  • sonar.xml
其中,將 main 跟 test 分為兩個同階層的目錄。在這樣的目錄結構下,我們可以得到一些好處,分別是 :
  • 明確將主程式跟測試用的程式區分開來。
  • 主程式跟測試程式皆屬於同一個 Package,所以在寫測試的時候,不需要特別引入主程式,即可執行。

相關 Dependency

在這裡是我們需要準備的 Dependency 及下載連結 (2019-12-27 有效) :
如果專案中,尚有前人留下的禮物 Ivy。那恭喜你,取得這些 Dependency 的路會輕鬆一點,以下是 ivy.xml 的設定方式 :

ivy.xml

<ivy-module version="2.0" xmlns:e="http://ant.apache.org/ivy/extra">
    <info organisation="org.pieceofdevops" module="TestwithAnt"/>
    <dependencies>
        <dependency org="junit" name="junit" rev="4.13-rc-2" conf="default->test">
            <artifact name="junit" type="jar" />
        </dependency>
        <dependency org="org.jacoco" name="org.jacoco.ant" rev="0.8.5" conf="default->master">
            <artifact name="org.jacoco.ant" e:classifier="nodeps"/>
        </dependency>
        <dependency org="org.sonarsource.scanner.ant" name="sonarqube-ant-task" rev="2.7.0.1612" conf="default->master">
            <artifact name="sonarqube-ant-task" type="jar" />
        </dependency>
    </dependencies>
</ivy-module>

產生 Report

現在我們離 SonarQube 越來越近了。其實我們不產生任何額外的 Report,SonarQube 仍是可以分析我們專案中的 Code,但關於單元測試及覆蓋率相關的資訊,SonarQube 就無法幫忙一併做總結。所以 SonarQube 提供一些參數,讓我們將相關 Report 餵進去。為了讓我們在 SonarQube 上的結果完整些,以下是相關 Report 用 Ant 產生的方式 :

testing.xml

<project basedir="." default="test" xmlns:jacoco="antlib:org.jacoco.ant">
    <property name="junit.dir" value="./reports/junit"/>
    <property name="jacoco.dir" value="./reports/jacoco"/>
    <property name="jacoco.exec" value="./jacoco.exec"/>

    <property name="src.dir" location="src" />
    <property name="lib.dir" location="lib" />
    <property name="build.dir" location="build" />
    <property name="dist.dir" location="dist" />
    <!-- init -->
    <target name="init" depends="clean">
        <mkdir dir="${build.dir}" />
        <mkdir dir="${dist.dir}" />
    </target>
    <!-- compile -->
    <target name="compile" depends="init" description="compile the source">
        <path id="lib.path">
            <fileset dir="${lib.dir}/">
                <include name="*.jar"/>
            </fileset>
        </path>
        <javac srcdir="${src.dir}" destdir="${build.dir}" includeAntRuntime="false">
            <classpath>
                <path refid="lib.path"/>
            </classpath>
        </javac>
    </target>
    <!-- generate jar of this project -->
    <target name="jar" depends="compile" description="package, output to JAR">
        <jar destfile="${dist.dir}/TestwithAnt.jar" filesetmanifest="skip" basedir="${build.dir}">
            <zipgroupfileset dir="lib" includes="*.jar" excludes=""/>
            <manifest>
                <attribute name="Main-Class" value="HelloAnt.Main"/>
                <attribute name="Class-Path" value=""/>
            </manifest>
        </jar>
    </target>
    <!-- generate reports for SonarQube -->
    <target name="test" depends="jar">

        <mkdir dir="${junit.dir}"></mkdir>
        <mkdir dir="${jacoco.dir}"></mkdir>
        <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
            <classpath path="lib/org.jacoco.ant-0.8.5.jar"/>
        </taskdef>

        <jacoco:coverage destfile="${jacoco.exec}">
            <junit fork="true" forkmode="once" showoutput="true">
                <formatter type="xml"/>
                <test name="HelloAnt.MainTest" methods="echoCountTest" todir="${junit.dir}"/>
                <classpath>
                    <path location="${dist.dir}/TestwithAnt.jar"/>
                </classpath>
            </junit>
        </jacoco:coverage>
        <jacoco:report>
            <executiondata>
                <file file="${jacoco.exec}"/>
            </executiondata>
            <structure name="Example Project">
                <classfiles>
                    <fileset dir="out/production"/>
                </classfiles>
                <sourcefiles encoding="UTF-8">
                    <fileset dir="src/main"/>
                </sourcefiles>
            </structure>
            <xml destfile="${jacoco.dir}/jacoco.xml"/>
            <html destdir="${jacoco.dir}"/>
        </jacoco:report>
    </target>
    <!-- junit report in html -->
    <target name="junit-html-report" depends="test">
        <delete dir="${junit.dir}"></delete>
        <mkdir dir="${junit.dir}"></mkdir>
        <junitreport todir="${junit.dir}">
            <fileset dir="${junit.dir}">
                <include name="${junit.dir}"/>
            </fileset>
            <report format="frames" todir="${junit.dir}/html"/>
        </junitreport>
    </target>
    <!-- clean up -->
    <target name="clean" description="clean up">
        <delete dir="${build.dir}" />
        <delete dir="${dist.dir}" />
        <delete dir="${junit.dir}" />
        <delete dir="${jacoco.dir}"/>
    </target>
</project>
由於 Jacoco 產生 Coverage Report 的時候是透過 Junit 完成,所以 Unit testing 跟 Coverage 是可以一併產生的。關於 Unit testing 的部分 (需設定為 xml 格式),我們直接使用 todir 將 Report 導出至固定的目錄,以便 SonarScanner 取得。Jacoco 產生 Report 則需要經過兩個階段,分別為 :
  • jacoco:coverage : 解析 Junit 所產生的結果,並產生一個執行檔 jacoco.exec
  • jacoco:report : 解析 jacoco.exec,並依 structure 中的 classfiles 設定決定要顯示哪些 Class 的覆蓋率,產生對應的 Report。其中,sourcefiles 並不一定需要設定,也就是一個 Option。如果不指定,只是在 Report 上無法直接看到對應的程式碼。另外,需要注意的是,Jacoco 產出的 Report 也是需要是 xml 版的,SonarQube(8.2)才會買單
另外是 Junit 中的 Classpath,在此直接把整個專案(包括 Junit)包成一個 Jar,讓 Junit 可以順利值ㄒ。Report 方面到此告一個段落。接下來,就是來設定 SonarScanner 需要的 Buildfile。

設定 SonarScanner 的 Buildfile

來到最關鍵的一段,也就是跑 SonarScanner 用的 Buildfile。設定方式如下 :

sonar.xml

 <project name="Sonar" default="sonar" basedir="." xmlns:sonar="antlib:org.sonar.ant">
    <property name="junit.dir" value="./reports/junit"/>
    <property name="jacoco.dir" value="./reports/jacoco"/>

    <property name="sonar.host.url" value="http://localhost:9090" />
    <property name="sonar.projectKey" value="sonarqube-scanner-ant:test" />
    <property name="sonar.projectName" value="sonarqube-scanner-ant:test" />
    <!--<property name="sonar.login" value="<token>" />-->
    <property name="sonar.projectVersion" value="1.0" />
    <property name="sonar.sourceEncoding" value="UTF-8" />
    <property name="sonar.java.source" value="1.8" />

    <property name="sonar.sources" value="src/main" />
    <property name="sonar.tests" value="src/test" />

    <property name="sonar.java.binaries" value="out/production" />
    <property name="sonar.java.libraries" value="lib/*.jar" />

    <property name="sonar.java.test.binaries" value="out/test" />
    <property name="sonar.java.test.libraries" value="lib/*.jar" />

    <property name="sonar.coverage.jacoco.xmlReportPaths" value="${jacoco.dir}/jacoco.xml" />
    <property name="sonar.junit.reportPaths" value="${junit.dir}" />

    <target name="sonar">
        <taskdef uri="antlib:org.sonar.ant" resource="org/sonar/ant/antlib.xml">
            <classpath path="./lib/sonarqube-ant-task-2.7.0.1612.jar" />
        </taskdef>
        <sonar:sonar />
    </target>
</project>
從上面的設定來看,其實需要做的只有把 SonarScanner 需要的參數壓上即可。以下為上述參數的介紹(請注意,以下參數會依使用到的 SonarQube 或 Plugin 本版不同,而可能有所差異):
  • sonar.host.url : SonarQube 對應的網址。
  • sonar.projectKey & sonar.projectName : 如果 SonarQube 上已經建專案了,直接填入對應的值即可。如果還沒,SonarQube 就會依填入的值,創建專案。需要注意的是,一般 SonarQube 預設任何人都可以直接在上面創專案,如果你的 SonarQube 只有特定帳號才有權限創專案,可以透過 sonar.login 填入對應帳號的 token,SonarQube 就會認定是 token 的主人上傳專案,也就可以成功上傳。
  • sonar.projectVersion : 非必要參數,依需求填入即可。
  • sonar.sourceEncoding : 非必要參數,依需求填入即可。
  • sonar.java.source : 非必要參數,依需求填入即可。
  • sonar.sources : 主程式的目錄。
  • sonar.tests : 測試程式的目錄。
  • sonar.java.binaries : 必要參數,主程式的 Class 所在目錄。
  • sonar.java.libraries : 主程式的 Dependency 所在目錄。
  • sonar.java.test.binaries : 測試程式的 Class 所在目錄。
  • sonar.java..test.libraries : 測試程式的 Dependency 所在目錄。
  • sonar.coverage.jacoco.xmlReportPaths : Jacoco Report 檔案路徑。
  • sonar.junit.reportPaths : Junit Report 所在目錄。

送上 SonarQube

Dependency 、Report 及 sonar.xml 都準備後,就可以把專案送上 SonarQube 審查囉!
$ ant -f sonar.xml
接下來你會看到 SonarScanner 開始做事,最後它也會顯示專案在 SonarQube 上的連結,讓你可以去看看未來是否有得忙了 (怕)。

熱門文章