问题

普通应用Gradle配置Jacoco,运行createDebugAndroidTestCoverageReport,能够正常输出覆盖率报告,报告路径为:
build/reports/coverage/debug/index.html。查看build/outputs/code-coverage/connected/*-coverage.ec,存在运行覆盖数据。

android {    compileSdkVersion 27    defaultConfig {        applicationId "example.api.com.myapplication2"        minSdkVersion 24        targetSdkVersion 27        versionCode 1        versionName "1.0"        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"    }    buildTypes {        debug{            testCoverageEnabled true        }    } }

在AndroidManifest中增加android:sharedUserId=”android.uid.system”,输出报告覆盖率一直为0。查看build/outputs/code-coverage/connected/*-coverage.ec 一直为空。

Jacoco原理

参考:https://blog.csdn.net/allan_shore_ma/article/details/80053340
简单来说:
debug模式下增加testCoverageEnabled true 后Gradle自动利用 jacoco插件在生成最终的目标文件之前,对源app Class文件进行插桩,生成最终的目标文件,执行目标文件以后得到覆盖执行数据。通过执行createDebugAndroidTestCoverageReport,在测试程序结束后会通过反射调用org.jacoco.agent.rt.RT.getAgent().getExecutionData(false) 获取到二进制数据并将此文件数据同步到uild/outputs/code-coverage/connected/${deviceName}-coverage.ec中。然后Gralde将此文件与build/intermediates/classes文件进行比较标记出类和行号,同时与src对比,输出报告到build/reports/coverage/debug/index.html

解决办法

因为代码插桩原理都是一样的,运行后也会有jacoco的覆盖数据信息。但实际系统应用获取到的coverage.ec为空,可以猜测以为权限问题,此数据无法直接导入到build/outputs/code-coverage/connected/*-coverage.ec。系统应用以及它的测试应用已经有写sd卡权限,可以通过sd卡作为中转。在测试用例执行完成的恰当时机,刷新sd卡的coverage.ec。测试结束后导出数据到build/outputs/code-coverage/connected/coverage.ec。最后执行jacoco的生成报告任务。
在测试用例执行完的恰当时机刷新coverage.ec,主要代码:

public class PushTestReport {    public static String getSDPath(){        File sdDir = null;        boolean sdCardExist = Environment.getExternalStorageState()                .equals(android.os.Environment.MEDIA_MOUNTED);        if(sdCardExist)        {            sdDir = Environment.getExternalStorageDirectory();        }        return sdDir.toString();    }    public static void createReport() {        String dir = getSDPath();        String path = dir + "/coverage.ec";        Log.d("jacoco","createReport:"+path);        File file = new File(path);        if (!file.exists()) {            try {                file.createNewFile();            } catch (IOException e) {                e.printStackTrace();                Log.e("jacoco","pushReport,error:"+e.getMessage());            }        }        OutputStream out = null;        try {            out = new FileOutputStream(path, false);            Object agent = Class.forName("org.jacoco.agent.rt.RT")                    .getMethod("getAgent")                    .invoke(null);            byte[] bytes = (byte[])agent.getClass().getMethod("getExecutionData", boolean.class)                    .invoke(agent, false);            Log.d("jacoco","pushReport,bytes:"+bytes.length);            out.write(bytes);        } catch (Exception e) {            e.printStackTrace();            Log.e("jacoco","pushReport,error:"+e.getMessage());        } finally {            if (out != null) {                try {                    out.close();                } catch (IOException e) {                    e.printStackTrace();                }            }        }    }}

调用时机tearDown():

 @After    public void tearDown() throws Exception {        PushTestReport.createReport();    }

以及TestRule运行完用例处:

@Override    public Statement apply(final Statement base, final Description description) {        return new Statement() {            @Override            public void evaluate() throws Throwable {                base.evaluate();                PushTestReport.createReport();            }        };    }

build.gradle如下:

task preJacocoTestReport(type: Exec) {    executable 'sh'    args "pre-jacoco-report.sh"}task jacocoTestReport(type: JacocoReport,dependsOn: preJacocoTestReport) {    group = "Reporting"    description = "Generate Jacoco coverage reports after running tests."    reports {        xml.enabled = true        html.enabled = true    }    def coverageSourceDirs = [            '../app/src/main/java'    ]    def excludeClasses = ['**/R*.class',                          '**/*$InjectAdapter.class',                          '**/*$ModuleAdapter.class',                          '**/*$ViewInjector*.class',                          '**/BuildConfig.class'    ]    classDirectories = fileTree(dir: '../app/build/intermediates/classes/debug',excludes: excludeClasses)        sourceDirectories = files(coverageSourceDirs)    executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")    doFirst {        new File("$buildDir/intermediates/classes/").eachFileRecurse { file ->            if (file.name.contains('$$')) {                file.renameTo(file.path.replace('$$', '$'))            }        }    }}

pre-jacoco-report.sh脚本:

#!/usr/bin/env bash#当前在环境为Project/app目录rm -f build/outputs/code-coverage/connected/coverage.ecadb pull /storage/emulated/0/coverage.ec build/outputs/code-coverage/connected/

运行以下任务输出报告:build/reports/jacoco/jacocoTestReport/html/index.html

gradle createDebugCoverageReport -p Project/appgradle jacocoTestReport -p Project/app

子模块覆盖率统计注意

如果想要子模块的覆盖率也统计进去

  1. 子模块的build.gradle中也增加testCoverageEnabled true

  2. src也要包含子模块的src,例如:

    def coverageSourceDirs = [
    ‘…/**/src/main/java’
    ]

  3. class也要包含子模块的class:

    classDirectories += fileTree(dir: ‘…/subMoudle1/build/intermediates/classes/debug’,excludes: excludeClasses)
    classDirectories += fileTree(dir: ‘…/subMoudle2/build/intermediates/classes/debug’,excludes: excludeClasses)

插曲

Jacoco一直报错:

08-20 13:47:35.971 25869-25869/com.gs.service W/System.err: java.io.FileNotFoundException: /jacoco.exec (Read-only file system)        at java.io.FileOutputStream.open(Native Method)        at java.io.FileOutputStream.(FileOutputStream.java:221)        at org.jacoco.agent.rt.internal_8ff85ea.output.FileOutput.openFile(FileOutput.java:67)        at org.jacoco.agent.rt.internal_8ff85ea.output.FileOutput.startup(FileOutput.java:49)08-20 13:47:35.972 25869-25869/com.gs.service W/System.err:     at org.jacoco.agent.rt.internal_8ff85ea.Agent.startup(Agent.java:122)        at org.jacoco.agent.rt.internal_8ff85ea.Agent.getInstance(Agent.java:50)        at org.jacoco.agent.rt.internal_8ff85ea.Offline.(Offline.java:31)        at org.jacoco.agent.rt.internal_8ff85ea.Offline.getProbes(Offline.java:51)

修改文件权限或者增加/jacoco-agent.properties 里面destfile=/mnt/sdcard/jacoco.exec并不能解决问题。然而此异常也并不影响覆盖率的统计。后续仍可深究。

更多相关文章

  1. 关于Android(安卓)studio混淆遇到的问题
  2. Android执行 shell command
  3. Android(安卓)调用系统相机 失败
  4. Android(安卓)程序执行Linux命令的解决方法及注意事项
  5. android Bitmap
  6. SharedPreferences之Android数据保存
  7. Android(安卓)对话框【Dialog】去除白色边框代码
  8. Android第五个功能:文件存储到SDCard上面
  9. NPM 和webpack 的基础使用

随机推荐

  1. 安装配置PhoneGap开发环境(一)
  2. Android 设备网络信息的获取(网络类型、网
  3. android Fragment 常用api方法
  4. linux配置android真机调试的步骤
  5. Android(安卓)端天气预报APP的实现(四)使用
  6. Android中获取控件宽高的4大方法
  7. Android:图解Activity启动流程源码(整体流
  8. 【Android】-- adb shell 命令探索
  9. Android(安卓)检查网络是否连接
  10. android 引导用户指示操作 高亮显示 可以