android 代码覆盖率
背景
项目使用的是small插件。一个app分为main和多个插件,为了统计插件的代码覆盖率。
1 修改插件
修改插件build.gradle
buildTypes { release { ... } debug{ minifyEnabled false testCoverageEnabled = true //打开debug版本的代码覆盖率开关 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }
因为工程原因插件生成的classes文件在下次生成的时候会变动。因此要讲classes文件拷贝到其他位置暂存。
tasks.whenTaskAdded { task -> if (task.name == 'assembleRelease' || task.name == 'assembleDebug') { task.doLast { println "copy classes to jacoco" def applicationId = android.defaultConfig.applicationId def artifactName = applicationId.substring(applicationId.lastIndexOf(".") + 1, applicationId.length()) project.copy { from "build/intermediates/classes/debug" into "${rootDir}/../jacoco/${artifactName}/classes/debug" } } } }
2 修改main
main作为插件的容器,我们的测试代码也在这里。所有的测试用例继承于BaseTest.
给BaseTest的finishZ增加覆盖率保存功能。
@Override protected void finished(Description description) { super.finished(description); ..... generateEcFile(true); } /** * 生成ec文件 * * @param isNew 是否重新创建ec文件 */ public static void generateEcFile(boolean isNew) { SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd_HHmmss"); final String DEFAULT_COVERAGE_FILE_PATH = "/sdcard/coverage/" + String.format("coverage_%s.ec", df.format(new Date())); LogUtils.i("生成覆盖率文件: " + DEFAULT_COVERAGE_FILE_PATH); OutputStream out = null; File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE_PATH); try { if (isNew && mCoverageFilePath.exists()) { LogUtils.i("JacocoUtils_generateEcFile: 清除旧的ec文件"); mCoverageFilePath.delete(); } if (!mCoverageFilePath.exists()) { File d = new File("/sdcard/coverage/"); d.mkdirs(); mCoverageFilePath.createNewFile(); } out = new FileOutputStream(mCoverageFilePath.getPath(), true); Object agent = Class.forName("org.jacoco.agent.rt.RT") .getMethod("getAgent") .invoke(null); out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class) .invoke(agent, false)); } catch (Exception e) { LogUtils.i("generateEcFile: " + e.getMessage()); } finally { if (out == null) return; try { out.close(); } catch (IOException e) { e.printStackTrace(); } } }
编译main,生成测试apk和debug版本的apk.
3.测试
使用测试系统进行测试
4.收集所有测试被测收集的测试数据。
import subprocess import os,sys o = subprocess.check_output(['adb','devices']) ls = o.split('\n') for line in ls: if line.endswith('device'): id_ = line.split('\t')[0] os.makedirs('./report/'+id_) c1 = ['adb','-s',id_,'pull','/sdcard/coverage','./report/'+id_] print ' '.join( c1 ) o1 = subprocess.check_output(c1) print o1
5.建立新的gradle工程,生成报告。
新建文件夹jacocReport。
进入文件夹运行gradle init。
修改build.gradle
apply plugin: 'jacoco' buildscript { repositories { mavenLocal() mavenCenter() maven { url 'plugins' } } dependencies { classpath 'net.researchgate:gradle-release:2.4.1' classpath 'com.android.tools.build:gradle:2.2.3' } } allprojects { repositories { mavenLocal() mavenCenter() flatDir { dirs 'libs' } } } //首先先删除旧的merge结果文件 task removeOldMergeEc(type: Delete) { delete "${rootDir}/../jacoco/coverageMerged/mergedcoverage.ec" } task mergeReport(type:JacocoMerge,dependsOn:removeOldMergeEc){ group = "Reporting" description = "merge jacoco report." destinationFile= file("${rootDir}/../jacoco/coverageMerged/mergedcoverage.ec") //这里的ec_dir是存储ec文件的文件夹 FileTree tree = fileTree("$projectDir/../jacoco/report") { include '**/*.ec' } def cnt =0 tree.each{ cnt++ } println "ec file conut:"+cnt // tree.each{f->println f} executionData = tree //executionData(files) } ["plugin1",'plugin2'].each { it1-> task "Report$it1"(type: JacocoReport,dependsOn: [mergeReport]) {it -> println "I'm task $it" group = "Reporting" description = "Generate Jacoco coverage reports after running tests." def pluginName1 = "$it1" println pluginName1 reports { xml.enabled = true html.enabled = true } classDirectories = fileTree( dir: "${rootDir}/../jacoco/${pluginName1}/classes/debug", excludes: ['**/R*.class', '**/*$InjectAdapter.class', '**/*$ModuleAdapter.class', '**/*$ViewInjector*.class' ]) def coverageSourceDirs =[ //不需要代码路径 ] sourceDirectories = files(coverageSourceDirs) File f = new File("${rootDir}/../jacoco/coverageMerged/mergedcoverage.ec") println ""+f.exists() println "" + f.length()+ " bytes" executionData = files("${rootDir}/../jacoco/coverageMerged/mergedcoverage.ec") doFirst { //修改claess 文件 println "${rootDir}/../jacoco/${pluginName1}/classes/debug" new File("${rootDir}/../jacoco/${pluginName1}/classes/debug").eachFileRecurse { file -> if (file.name.contains('$$')) { println "modify " + file.name file.renameTo(file.path.replace('$$', '$')) } } } } } task allReport(dependsOn: [Reportplugin1,Reportplugin2]){ doLast{ println "done!" } }
最后上目录
. ├── main #主apk代码的目录 ├── jacoco #数据目录-存放ec文件,classes文件 ├── jacocoReport #生成报告的工程目录 build.gradle在此 ├── plugin1 #插件1 ├── plugin2 #插件2