改进了几个点
不用借助 Instrumentation 启动,正常启动即可;
测试代码不用 push 到主分支,主分支代码拉到本地后用 git apply patch 方式合并覆盖率代码;
测试完成后,连按两次 back 键把 app 置于后台,并自动上报覆盖率文件到服务器;
src 下新建一个 test package,放入下面两个测试类
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* Created by sun on 17/7/4.
*/
public class JacocoUtils {
static String TAG = "JacocoUtils";
//ec文件的路径
private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec";
/**
* 生成ec文件
*
* @param isNew 是否重新创建ec文件
*/
public static void generateEcFile(boolean isNew) {
// String DEFAULT_COVERAGE_FILE_PATH = NLog.getContext().getFilesDir().getPath().toString() + "/coverage.ec";
Log.d(TAG, "生成覆盖率文件: " + DEFAULT_COVERAGE_FILE_PATH);
OutputStream out = null;
File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE_PATH);
try {
if (isNew && mCoverageFilePath.exists()) {
Log.d(TAG, "JacocoUtils_generateEcFile: 清除旧的ec文件");
mCoverageFilePath.delete();
}
if (!mCoverageFilePath.exists()) {
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));
// ec文件自动上报到服务器
UploadService uploadService = new UploadService(mCoverageFilePath);
uploadService.start();
} catch (Exception e) {
Log.e(TAG, "generateEcFile: " + e.getMessage());
} finally {
if (out == null)
return;
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
上传 ec 文件和设计信息到服务器
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import android.util.Log;
import com.x.x.x.LuojiLabApplication;
import com.x.x.x.DeviceUtils;
/**
* Created by sun on 17/7/4.
*/
public class UploadService extends Thread{
private File file;
public UploadService(File file) {
this.file = file;
}
public void run() {
Log.i("UploadService", "initCoverageInfo");
// 当前时间
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Calendar cal = Calendar.getInstance();
String create_time = format.format(cal.getTime()).substring(0,19);
// 系统版本
String os_version = DeviceUtils.getSystemVersion();
// 系统机型
String device_name = DeviceUtils.getDeviceType();
// 应用版本
String app_version = DeviceUtils.getAppVersionName(LuojiLabApplication.getInstance());
// 环境
String context = "";
Map<String, String> params = new HashMap<String, String>();
params.put("os_version", os_version);
params.put("device_name", device_name);
params.put("app_version", app_version);
params.put("create_time", create_time);
try {
post("http://x.x.x.x:8888/importCodeCoverage!upload", params, file);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 通过拼接的方式构造请求内容,实现参数传输以及文件传输
*
* @param url Service net address
* @param params text content
* @param files pictures
* @return String result of Service response
* @throws IOException
*/
public static String post(String url, Map<String, String> params, File files)
throws IOException {
String BOUNDARY = java.util.UUID.randomUUID().toString();
String PREFIX = "--", LINEND = "\r\n";
String MULTIPART_FROM_DATA = "multipart/form-data";
String CHARSET = "UTF-8";
Log.i("UploadService", url);
URL uri = new URL(url);
HttpURLConnection conn = (HttpURLConnection) uri.openConnection();
conn.setReadTimeout(10 * 1000); // 缓存的最长时间
conn.setDoInput(true);// 允许输入
conn.setDoOutput(true);// 允许输出
conn.setUseCaches(false); // 不允许使用缓存
conn.setRequestMethod("POST");
conn.setRequestProperty("connection", "keep-alive");
conn.setRequestProperty("Charsert", "UTF-8");
conn.setRequestProperty("Content-Type", MULTIPART_FROM_DATA + ";boundary=" + BOUNDARY);
// 首先组拼文本类型的参数
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
sb.append(PREFIX);
sb.append(BOUNDARY);
sb.append(LINEND);
sb.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"" + LINEND);
sb.append("Content-Type: text/plain; charset=" + CHARSET + LINEND);
sb.append("Content-Transfer-Encoding: 8bit" + LINEND);
sb.append(LINEND);
sb.append(entry.getValue());
sb.append(LINEND);
}
DataOutputStream outStream = new DataOutputStream(conn.getOutputStream());
outStream.write(sb.toString().getBytes());
// 发送文件数据
if (files != null) {
StringBuilder sb1 = new StringBuilder();
sb1.append(PREFIX);
sb1.append(BOUNDARY);
sb1.append(LINEND);
sb1.append("Content-Disposition: form-data; name=\"uploadfile\"; filename=\""
+ files.getName() + "\"" + LINEND);
sb1.append("Content-Type: application/octet-stream; charset=" + CHARSET + LINEND);
sb1.append(LINEND);
outStream.write(sb1.toString().getBytes());
InputStream is = new FileInputStream(files);
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}
is.close();
outStream.write(LINEND.getBytes());
}
// 请求结束标志
byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINEND).getBytes();
outStream.write(end_data);
outStream.flush();
// 得到响应码
int res = conn.getResponseCode();
Log.i("UploadService", String.valueOf(res));
InputStream in = conn.getInputStream();
StringBuilder sb2 = new StringBuilder();
if (res == 200) {
int ch;
while ((ch = in.read()) != -1) {
sb2.append((char) ch);
}
}
outStream.close();
conn.disconnect();
return sb2.toString();
}
}
在 build.gradle 新增
apply plugin: 'jacoco'
jacoco {
toolVersion = '0.7.9'
}
buildTypes {
release {
// 在release下统计覆盖率信息
testCoverageEnabled = true
}
}
最重要的一行代码,加在监听设备按键的地方,如果连续 2 次点击设备 back 键,app 已置于后台,则调用生成覆盖率方法。
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
....
JacocoUtils.generateEcFile(true);
}
}
为了不影响工程代码,我这里用 git apply patch 的方式应用的上面的覆盖率代码
首先 git commit 上面的覆盖率代码
然后 git log 查看 commit
我提交覆盖率代码的 commit 是最近的一次,然后拿到上一次的 commit,并生成.patch 文件,-o 是输出目录
git format-patch 0e4c................... -o ~/Documents/jk/script/
把.patch 文件拷贝到自动打包的机器,
使用 Jenkins 自动打包,拉取最新代码后,在编译前 Execute shell 自动执行下面的命令,把覆盖率文件应用到工程内
git apply --reject ~/Documents/jk/script/0001-patch.patch
执行成功后的输出:
然后就可以安装包含统计覆盖率代码的测试包了,测试完后后,连按两次 back 键生成覆盖率文件并上传到服务器。
在服务器我也拉了一个 Android 工程,专门用于生成报告
主要在 build.gradle 新增
def coverageSourceDirs = [
'../app/src/main/java'
]
task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories = fileTree(
dir: './build/intermediates/classes/debug',
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
])
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('$$', '$'))
}
}
}
}
然后设备上传 ec 文件到 Android 工程的 $buildDir/outputs/code-coverage/connected 目录下,并依次执行
gradle createDebugCoverageReport
gradle jacocoTestReport
最后把 $buildDir/reports/jacoco/目录下的覆盖率报告拷贝到展现的位置