在多项目工程中统计子工程的覆盖率

背景介绍

阅读此文请优先确保已读懂Gradle构建系统简介及在Gradle中集成覆盖率工具Jacoco并使用

在前文中我们对如何在gradle编译体系的工程中加入Jacoco代码覆盖率统计的方法做了介绍,但是前文的方法仅能统计到主工程的代码覆盖率,而无法统计到库工程,其具体原因可以参考此文Issue 76373: Code Coverage does not work for library project,简单总结一下就是目前google提供的android plugin有bug(或者是设计如此):所有的库工程都会使用Release版本来进行编译,即使你声明了TestCompile,而Release版本是无法用于统计代码覆盖率的,因此我们需要一些手段让编译系统能够编译出Debug版本的库工程,参考资料中提供了一个不错的思路,我们在向Dolphin中植入时会遇到一些问题,在此将整个过程详细记录下来,以供参考。

实践与经验

先介绍一下整体思路再分步详述。

要完成植入我们大概需要做如下的几件事:

  • 在所有库工程都引入了的DolphinBuild/common.gradle内增加配置项,打开debug模式下的覆盖率统计,同时让所有工程都能产生debug版本的编译结果
  • 处理主工程及所有子工程(包括嵌套的子工程)中的依赖关系,使用debug的configuration来进行编译
  • 处理插桩时可能存在的jacocoangent.jar重复的问题,删除重复的jar包
  • 处理jacocoReport任务,扩展需要分析的文件的范围

配置共用脚本common.gradle

观察Dolphin所有子工程的build.gradle脚本,发现都会有这样一行
apply from: "$project.rootDir/DolphinBuild/common.gradle"
意味着所有的脚本都会引用这个文件,通过修改这个文件我们可以直接修改到所有工程的build行为,而不用一一去更改每个build.gradle文件

为了能在所有工程中都能跑代码覆盖率,我们需要添加在debug版本下对覆盖率的支持:

android {
    buildTypes {
        debug {
            testCoverageEnabled true
        }
    }
}

为了让库工程能够编译输出Debug版本,我们需要增加如下的部分,可以参考:http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Referencing-a-Library|Referencing-a-Library:

android {
    publishNonDefault true
}

至此,这个共用文件的修改就完成了

处理依赖,使用debug编译库工程

如引文中所述,我们需要将所有的依赖项从:
compile project(':DolphinCoreLibrary')
的形式改变为
debugCompile project(path:':DolphinCoreLibrary', configuration: 'debug')
的形式

如果是少数几个文件,慢慢替换即可,可是像Dolphin这样拥有茫茫多库工程和相互依赖的项目来说,通过脚本一次性完成替换是必须的。可以在工程根目录(即shel_en_agile下)运行命令,内容如下:

find ./ -name build.gradle | xargs sed -i "s/^\(.*\)compile project(':\(.*\))/\1debugCompile project(path:':\2, configuration: 'debug')/g"

不要小看这小小的一行命令,里面有着多个重要的知识点,让我们娓娓道来(明白的同学和不关心细节可以自行跳过啦,这些知识和本文无关):

  • find命令是linux下的查找命令,-name的参数声明我们要在当前目录(./)下查找所有名字为build.gradle的文件,如果仅执行find命令,输出的是该目录下所有满足条件的文件的路径
  • |是linux下的重定向符,将前面命令的结果作为参数传递给后面的命令
  • xargs命令的作用是将参数列表转换成小块分段传递给其他命令(这里传递的目标是sed命令)
  • sed命令是一个简单的对文件逐行处理的程序,支持正则表达式,-i的参数表示操作会直接在文件中生效而不是显示在控制台上,其后的参数中s表示这次执行替换操作,/是分割符,分开了需要被替换的部分和替换的目标,g表示全局替换,会替换全部的匹配项
  • 正则表达式分成了两个部分,匹配部分为<color red>^(.)compile project(':(.))</color>,匹配包含compile project且结尾为")"的行,两组括号(已转义)表示需要提取的group在替换时使用。替换部分为<color red>\1debugCompile project(path:':\2, configuration: 'debug')</color>,表示替换的目标其中的\1和\2表示之前匹配的两个group,其余部分用定义的文字替换

处理插桩时可能存在的jacocoangent.jar重复的问题,删除重复的jar包

我们尝试使用处理过的包进行编译是发现报错了:

Execution failed for task ':DolphinBrowserEN:proguardDebug'.
> java.io.IOException: Can't write [/home/pgao/dolphin/src/shell_en_agile/DolphinBrowserEN/build/intermediates/classes-proguard/debug/classes.jar] (Can't read [/home/pgao/dolphin/src/shell_en_agile/DolphinBrowserEN/build/intermediates/exploded-aar/shell_en_agile.common_library/ui_utils/unspecified/debug/libs/jacocoagent.jar(;;;;;;!META-INF/MANIFEST.MF)] (Duplicate zip entry [jacocoagent.jar:com/vladium/emma/rt/RT.class]))

原因是主工程已经集成了jacoco,库工程又集成会导致同时存在了多个jacocoagent.jar文件,我们需要在执行proguardDebug任务前删除多余的jacocoagent.jar,编译就可以继续进行了,需要在主工程的build.gradle中增加如下的脚本(先贴总脚本,再讲解过程):

task deleteJacocoagentJar {
    doLast {
        getTransitiveProjectDependencies(this, 'debugCompile').each { project ->
//            println "**********" + "build/intermediates/exploded-aar/${rootProject.name}.services/${project.name}/unspecified/debug/libs/jacocoagent.jar" + "***********"
            delete "build/intermediates/exploded-aar/${rootProject.name}.services/${project.name}/unspecified/debug/libs/jacocoagent.jar"
            delete "build/intermediates/exploded-aar/${rootProject.name}.common_library/${project.name}/unspecified/debug/libs/jacocoagent.jar"
            delete "build/intermediates/exploded-aar/${rootProject.name}.third_party/${project.name}/unspecified/debug/libs/jacocoagent.jar"
            delete "build/intermediates/exploded-aar/${rootProject.name}/${project.name}/unspecified/debug/libs/jacocoagent.jar"
            delete "build/intermediates/exploded-aar/shell_en_agile.third_party.animator/library/unspecified/debug/libs/jacocoagent.jar"
            delete "build/intermediates/exploded-aar/shell_en_agile.third_party.svg-android/svgandroid/unspecified/debug/libs/jacocoagent.jar"
            delete "build/intermediates/exploded-aar/shell_en_agile.services.promotion_service/promotion_link/unspecified/debug/libs/jacocoagent.jar"
            //delete "/home/pgao/dolphin/src/topstory/DolphinNewsClient/build/intermediates/exploded-aar/topstory.services/news_service/unspecified/debug/libs/jacocoagent.jar"
        }
    }
}

def getTransitiveProjectDependencies(project, configuration) {
    def projectDependencies = project.configurations."$configuration".getAllDependencies().withType(ProjectDependency)
    def dependencyProjects = projectDependencies*.dependencyProject
    dependencyProjects.each {
        dependencyProjects += getTransitiveProjectDependencies(it, configuration)
    }
    return dependencyProjects.unique()
}

android {
    applicationVariants.all { variant ->
        variant.dex.dependsOn deleteJacocoagentJar
        deleteJacocoagentJar.mustRunAfter variant.javaCompile
    }

    testVariants.all { variant ->
        variant.dex.dependsOn deleteJacocoagentJar
        deleteJacocoagentJar.mustRunAfter variant.javaCompile
    }
}

我们定义了一个名字为:deleteJacocoagentJar的任务,和一个getTransitiveProjectDependencies的方法,并将这个任务绑定到了javaCompile任务之后强制执行。

getTransitiveProjectDependencies

这是我们自定义的一个方法,作用是获取到所有依赖项(包括递归依赖项)的工程,后续会把这些工程对应在主工程内生成的jacocoagent.jar删除,获取这个工程列表分成了4步

  • 获取当前工程全部的依赖项
  • 针对每一个依赖项获取其工程,并加入列表
  • 递归处理每一个库工程,将所有的依赖工程加入列表
  • 针对列表做去重处理并返回

deleteJacocoagentJar任务

在这个任务中我们针对之前获取的全部依赖项做删除处理,由于暂时没有弄清楚build/intermediates/exploded-aar下不同的工程编译文件生成的不同路径的原理,因此我暂时也没有找到一条通用的规则适配到全部的工程上,只能根据编译结果修改不同的模式,在浏览器工程下我总结了7条规则,如果要针对其他的项目做移植,需要自己判断是否需要对规则进行修改,甚至直接列出每一个工程的直接路径。后续在调查清楚这些文件生成的逻辑后可能会更新此块的内容。

将这个任务绑定到了javaCompile任务之后强制执行

我们定义的任务是需要被自动执行的,否则编译还是会报错,因此我们需要在android块中声明,这个任务必须在javaCompile后强制执行

variants是android plugin提供的操纵tasks的接口,为了让测试工程也能编译通过,我们还增加了在测试编译时也删除这些多余jar包的配置

至此,插桩的任务就完成了,测试包也可以编译通过并通过引导程序生成代码覆盖率文件了,接下来需要将文件和源码链接起来,即扩大生成报告使用的文件范围到整个项目中

处理jacocoReport任务,扩展需要分析的文件的范围

同只统计主工程的方法一致,我们需要对jacocoReort任务做修改,扩大sourceDirectories和classDirectories的范围,成品如下:

task jacocoReportNew(type: JacocoReport) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."

    reports {
        xml.enabled true
        html.enabled true
    }

    def excludesFilter = ['**/R.class',
                          '**/R$*.class',
                          '**/*$ViewInjector*.*',
                          '**/BuildConfig.*',
                          '**/Manifest*.*',]

    sourceDirectories = files("src")
    classDirectories = fileTree(dir: "./build/intermediates/classes/debug", excludes: excludesFilter)
    project.rootProject.allprojects.each { project ->
        if (project.name != "shell_en_agile" && project.name != "DolphinRecordTest" && project.name != "DolphinBrowserEN"){
            sourceDirectories += files((project.projectDir).toString() + "/src")
            classDirectories += fileTree(dir:(project.projectDir).toString() + "/build/intermediates/classes/debug", excludes: excludesFilter)
        }
    }

    executionData = fileTree(dir: "/home/pgao/code-coverage/shell")
}

我们除了将主工程的相关文件加入以外,还通过遍历根工程下所有子工程的方法增加了其他工程的相关文件。

在实际操作工程中,sourceDirectories虽然在官方文档中给出的类型是FileCollection,按理说FileTree是他的子类,应该满足需求,但是使用FileTree死活无法链接到源文件,改为Files类型后即正常了,留做后续调研吧。

另外我尝试使用前面我们自定义的方法来递归获取所有以来子工程也是持续报错,暂时放弃这个智能的方法改用目前的手动剔除不需要的工程,同样留给后续调研吧,这两项都不影响我们的集成和使用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,050评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,538评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,673评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,622评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,047评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,974评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,129评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,893评论 0 209
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,654评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,828评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,297评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,619评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,326评论 3 243
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,176评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,975评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,118评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,909评论 2 278

推荐阅读更多精彩内容