测试开发之路 持续集成的开源方案攻略 (五) 多分支 pipeline 与可视化

孙高飞 · March 11, 2020 · 1132 hits

什么是多分支pipeline

在jenkins中存在两种类型的pipeline, 之前我们讲述的都是普通类型的pipeline。 而多分支pipeline作为一个非常重要的类型重要为我们完成持续集成的第一个步骤—打通gitlab的通信。 这样研发再push代码后通知jenkins运行我们预先定义的pipeline完成整个持续集成流程。 要做到这样的效果需要分别在jenkins和gitlab中做如下准备工作。

jenkins的配置

配置jenkins(这里已经在咱们的jenkins中配置好,凭据名称为gaofeigitlab账号)
  • 安装gitlab插件
  • 在安全设置中添加jenkins 凭据
  • 类型选择:Gitlab API Token (获取方式:在gitlab中使用自己的账户登录,在User settings中找到Access Tokens。 在这里创建一个token)
  • 复制这个token保存到上面说的jenkins 凭据中。
创建多分支pipeline

在jenkins中创建一个job,选择类型为多分支pipeline。 在git中填写研发的repo地址,jenkins 凭据以及要监控的分支。 如下:

gitlab的配置

到研发的repo中,添加一个跟jenkins通信的webhook。 需要进入settings->integration→添加webhook。 中间要填写jenkins job的url 以及 勾选push event和merge event。如下:

注意:jenkins job 的url的格式是:http://JENKINS_URL/project/PROJECT_NAME
通过上面的配置,我们就打通了jenkins 与 gitlab的通信。 一旦有研发在提交代码和提交merge的时候就会触发这个多分支pipeline运行。

准备jenkinsfile

多分支pipeline的规则是打通了研发repo中所有分支的事件。 可以理解为它监控了repo中的所有分支的代码变动。 所以它不准在job的脚本框中编写pipeline,我们需要在研发的分支中添加jenkinsfile来保存我们的pipeline。

注意: 多分支pipeline在创建后就会扫描研发repo中所有的分支并寻找jenkinsfile文件。 所以我们要在所有需要执行pipeline的分支中都要编写一份pipeline。如果jenkins找不到jenkinsfile边不会监控此分支。 jenkinsfile中的pipeline脚本与普通的pipeline语法一致,没有区别。

效果

根据上面的配置, 多分支pipeline扫描了研发的分支后决定跟踪哪些分支的事件, 这里有两个规则, 一个是它只会监控有jenkinsfile的分支,如果没有就不跟踪, 这跟上面说的一样, 第二个规则是在job可以根据正则表达式来配置都跟踪哪些分支。 当配置生效后, 多分支pipeline会定期的去扫描研发的repo去获取最新的要跟踪的分支信息。 然后会根据分支名字作为job名字。 当跟踪了多个分支后就会在多分支pipeline下创建多个pipeline job出来, 就像上面的图, 只不过因为我们这个项目走的是主线开发模型, 所以我配置的只跟踪master分支。

可视化

在第四范式我们提倡测试的透明化和可视化。 我们希望每个项目都有一个质量看板,能够实时的反应出当前的bug,测试用例,覆盖率等相关信息。 举个例子, 我们最近开展的sdk项目中,我制作了如下的看板。

为了达到上面的效果, 我们采取了如下的工具链:

  • allure: 不论是java,python还是js语言来做测试, 选取的测试报告框架一律都是allure,这个框架在兼容各个主流语言的同时,同时会暴露出http接口可以供我们抓取测试结果。 而详尽的用例分类展示功能,也可以让我们统计每个模块的测试用例数据。 这也是上面我能制作出模块级别的测试用例变化趋势图的原因。
  • jenkins shared library: 利用之前介绍的这个功能,我们编写了统一的库。 可以让各个项目对接, 只要用了allure作为report的,都可以无缝对接。
  • metabase:这是一个开源的BI软件,特点是能对接各种数据库, 可以在UI上很方便的配置出各种图标, 上述的效果都是用metabase制作的。
jenkins上抓取allure report结果的库

/**
* Created by sungaofei on 19/3/1.
*/


@Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7')
@Grab(group = 'org.jsoup', module = 'jsoup', version = '1.10.3')
import org.jsoup.Jsoup
import groovyx.net.http.HTTPBuilder


import static groovyx.net.http.ContentType.*
import static groovyx.net.http.Method.*
import groovy.transform.Field

//可以指定maven仓库
//@GrabResolver(name = 'aliyun', root = 'http://maven.aliyun.com/nexus/content/groups/public/')
//加载数据库连接驱动包
//@Grab('mysql:mysql-connector-java:5.1.25')
//@GrabConfig(systemClassLoader=true)

//global variable
@Field jenkinsURL = "http://auto.4paradigm.com"

@Field int passed
@Field int failed
@Field int skipped
@Field int broken
@Field int unknown
@Field int total
@Field Map<String, Map<String, Integer>> map = new HashMap<>()

@NonCPS
def getResultFromAllure() {
def reportURL = ""
if (env.BRANCH_NAME != "" && env.BRANCH_NAME != null) {
reportURL = "/view/API/job/${jobName}/job/${env.BRANCH_NAME}/${BUILD_NUMBER}/allure/"
} else {
reportURL = "/view/API/job/${JOB_NAME}/${BUILD_NUMBER}/allure/"
}

// reportURL = "/view/API/job/sage-sdk-test/185/allure/"

HTTPBuilder http = new HTTPBuilder(jenkinsURL)
//根据responsedata中的Content-Type header,调用json解析器处理responsedata
http.get(path: "${reportURL}widgets/summary.json") { resp, json ->
println resp.status
passed = Integer.parseInt((String) json.statistic.passed)
failed = Integer.parseInt((String) json.statistic.failed)
skipped = Integer.parseInt((String) json.statistic.skipped)
broken = Integer.parseInt((String) json.statistic.broken)
unknown = Integer.parseInt((String) json.statistic.unknown)
total = Integer.parseInt((String) json.statistic.total)
}

http.get(path: "${reportURL}data/behaviors.json") { resp, json ->
List featureJson = json.children

for (int i = 0; i < featureJson.size(); i++) {
String featureName = featureJson.get(i).name
Map<String, Integer> results = new HashMap<>()
results['passed'] = 0
results['failed'] = 0
results['skipped'] = 0
results['broken'] = 0
results['unknown'] = 0


List storyJson = featureJson.get(i).children
for (int j = 0; j < storyJson.size(); j++) {

List caseJson = storyJson.get(j).children
for (int k = 0; k < caseJson.size(); k++) {
def caseInfo = caseJson.get(k)
String status = caseInfo.status
int num = results.get(status) + 1
results[status] = num

}
}
int total = 0
results.each { key, value ->
total = total + value
}
results['total'] = total
map.put(featureName, results)
}


}
}

def int getLineCov(){
def htmlurl = "${jenkinsURL}/view/API/job/${JOB_NAME}/${BUILD_NUMBER}/_e4bba3_e7a081_e8a686_e79b96_e78e87_e68aa5_e5918a/index.html"
String doc = Jsoup.connect(htmlurl).get().getElementsByClass("pc_cov").text();
int cov = Integer.parseInt(doc.replace("%", ""))
println("当前行覆盖率为 ${cov}")
return cov
}

def int getBranchCov(){
def htmlurl = "${jenkinsURL}/view/API/job/${JOB_NAME}/${BUILD_NUMBER}/_e4bba3_e7a081_e8a686_e79b96_e78e87_e68aa5_e5918a/index.html"
String branchAll = Jsoup.connect(htmlurl).get().select(".total > :nth-child(5)").text();
String branchPartial = Jsoup.connect(htmlurl).get().select(".total > :nth-child(6)").text();

println("all branch number: ${branchAll}")
println("cover branch number: ${branchPartial}")

def cov = Integer.parseInt(branchPartial)/Integer.parseInt(branchAll)
println("the branch cov is ${cov}")



return cov

}

def call() {
def version = "release/3.8.2"
getResultFromAllure()

getDatabaseConnection(type: 'GLOBAL') {
map.each { feature, valueMap ->
def sqlString = "INSERT INTO func_test (name, build_id, feature, version, total, passed, unknown, skipped, failed, broken, create_time) VALUES ('${JOB_NAME}', '${BUILD_ID}', '${feature}', '${version}', " +
"${valueMap['total']}, ${valueMap['passed']}, ${valueMap['unknown']}, ${valueMap['skipped']}, ${valueMap['failed']}, ${valueMap['broken']}, NOW())"
println(sqlString)

sql sql: sqlString
}

def lineCov = getLineCov()
def branchCov = getBranchCov()
def sqlString = "INSERT INTO func_test_summary (name, build_id, version, total, passed, unknown, skipped, failed, broken, line_cov, branch_cov, create_time) VALUES ('${JOB_NAME}', '${BUILD_ID}', '${version}', " +
"${total}, ${passed}, ${unknown}, ${skipped}, ${failed}, ${broken}, ${lineCov}, ${branchCov}, NOW())"

sql sql: sqlString
}
}

上面的注意点:

  • 使用grab 来下载http builder 和 jsoup的依赖, 分别用来请求allure的http接口, 以及使用jsoup来解析覆盖率这种纯html页面。
  • allure的分为allure1和allure2, 不同版本对外暴露的接口不一样, 这一点要注意,我这里统一使用allure2. 具体的接口细节可以在chrome上去抓
  • allure的接口用到的两个: summary用来获取整体的测试用例情况, 而behaviors 则能获取每一个模块的测试用例细节。
  • 最后入库的sql指令使用的是database和mysql-database的插件,可以在jenkins上的插件管理中下载。 然后在全局配置中,按如下进行配置。

PS: 我曾经想要使用groovy的jdbc 包来与mysql通信, 但是在jenkins中的groovy毕竟跟常规的不一样,导致依赖包无法加载, 一直没有办法解决这个问题。 所以只能用jenkins的插件来曲线救国了。

至于Metabase这个BI软件的教程就不写了, 非常简单,大家随便去搜一下吧, 我当时都没看文档, 直接启动起来以后玩两下子就行了。 上面统计bug的信息是做了个定时任务,定时的去抓jira的接口然后入库搞的, 也就不详细讲了。

结尾

这个系列写到这,关于jenkins pipeline以及相关工具的介绍就结束了, 下一期开始写docker&k8s。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
No Reply at the moment.
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up