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 有效) :
- junit : mvnrepository
- org.jacoco.ant : mvnrepository
- sonarqube-ant-task : mvnrepository
如果專案中,尚有前人留下的禮物 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 上的連結,讓你可以去看看未來是否有得忙了 (怕)。