改进了几个点
1. 不用借助Instrumentation启动,正常启动即可;
2. 测试代码不用push到主分支,主分支代码拉到本地后用git apply patch方式合并覆盖率代码;
3. 测试完成后,连按两次back键把app置于后台,并自动上报覆盖率文件到服务器;
1. 新增覆盖率代码
src下新建一个test package,放入下面两个测试类
1 import android.util.Log; 2 3 import java.io.File; 4 import java.io.FileOutputStream; 5 import java.io.IOException; 6 import java.io.OutputStream; 7 8 /** 9 * Created by sun on 17/7/4. 10 */ 11 12 public class JacocoUtils { 13 static String TAG = "JacocoUtils"; 14 15 //ec文件的路径 16 private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec"; 17 18 /** 19 * 生成ec文件 20 * 21 * @param isNew 是否重新创建ec文件 22 */ 23 public static void generateEcFile(boolean isNew) { 24 // String DEFAULT_COVERAGE_FILE_PATH = NLog.getContext().getFilesDir().getPath().toString() + "/coverage.ec"; 25 Log.d(TAG, "生成覆盖率文件: " + DEFAULT_COVERAGE_FILE_PATH); 26 OutputStream out = null; 27 File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE_PATH); 28 try { 29 if (isNew && mCoverageFilePath.exists()) { 30 Log.d(TAG, "JacocoUtils_generateEcFile: 清除旧的ec文件"); 31 mCoverageFilePath.delete(); 32 } 33 if (!mCoverageFilePath.exists()) { 34 mCoverageFilePath.createNewFile(); 35 } 36 out = new FileOutputStream(mCoverageFilePath.getPath(), true); 37 38 Object agent = Class.forName("org.jacoco.agent.rt.RT") 39 .getMethod("getAgent") 40 .invoke(null); 41 42 out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class) 43 .invoke(agent, false)); 44 45 // ec文件自动上报到服务器 46 UploadService uploadService = new UploadService(mCoverageFilePath); 47 uploadService.start(); 48 } catch (Exception e) { 49 Log.e(TAG, "generateEcFile: " + e.getMessage()); 50 } finally { 51 if (out == null) 52 return; 53 try { 54 out.close(); 55 } catch (IOException e) { 56 e.printStackTrace(); 57 } 58 } 59 } 60 }
上传ec文件和设计信息到服务器
1 import java.io.DataOutputStream; 2 import java.io.File; 3 import java.io.FileInputStream; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.net.HttpURLConnection; 7 import java.net.URL; 8 import java.text.SimpleDateFormat; 9 import java.util.Calendar; 10 import java.util.HashMap; 11 import java.util.Map; 12 13 import android.util.Log; 14 15 import com.x.x.x.LuojiLabApplication; 16 import com.x.x.x.DeviceUtils; 17 18 /** 19 * Created by sun on 17/7/4. 20 */ 21 22 public class UploadService extends Thread{ 23 24 private File file; 25 public UploadService(File file) { 26 this.file = file; 27 } 28 29 public void run() { 30 Log.i("UploadService", "initCoverageInfo"); 31 // 当前时间 32 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 33 Calendar cal = Calendar.getInstance(); 34 String create_time = format.format(cal.getTime()).substring(0,19); 35 36 // 系统版本 37 String os_version = DeviceUtils.getSystemVersion(); 38 39 // 系统机型 40 String device_name = DeviceUtils.getDeviceType(); 41 42 // 应用版本 43 String app_version = DeviceUtils.getAppVersionName(LuojiLabApplication.getInstance()); 44 45 // 环境 46 String context = ""; 47 48 Map<String, String> params = new HashMap<String, String>(); 49 params.put("os_version", os_version); 50 params.put("device_name", device_name); 51 params.put("app_version", app_version); 52 params.put("create_time", create_time); 53 54 try { 55 post("http://x.x.x.x:8888/importCodeCoverage!upload", params, file); 56 } catch (IOException e) { 57 e.printStackTrace(); 58 } 59 60 } 61 62 /** 63 * 通过拼接的方式构造请求内容,实现参数传输以及文件传输 64 * 65 * @param url Service net address 66 * @param params text content 67 * @param files pictures 68 * @return String result of Service response 69 * @throws IOException 70 */ 71 public static String post(String url, Map<String, String> params, File files) 72 throws IOException { 73 String BOUNDARY = java.util.UUID.randomUUID().toString(); 74 String PREFIX = "--", LINEND = " "; 75 String MULTIPART_FROM_DATA = "multipart/form-data"; 76 String CHARSET = "UTF-8"; 77 78 79 Log.i("UploadService", url); 80 URL uri = new URL(url); 81 HttpURLConnection conn = (HttpURLConnection) uri.openConnection(); 82 conn.setReadTimeout(10 * 1000); // 缓存的最长时间 83 conn.setDoInput(true);// 允许输入 84 conn.setDoOutput(true);// 允许输出 85 conn.setUseCaches(false); // 不允许使用缓存 86 conn.setRequestMethod("POST"); 87 conn.setRequestProperty("connection", "keep-alive"); 88 conn.setRequestProperty("Charsert", "UTF-8"); 89 conn.setRequestProperty("Content-Type", MULTIPART_FROM_DATA + ";boundary=" + BOUNDARY); 90 91 // 首先组拼文本类型的参数 92 StringBuilder sb = new StringBuilder(); 93 for (Map.Entry<String, String> entry : params.entrySet()) { 94 sb.append(PREFIX); 95 sb.append(BOUNDARY); 96 sb.append(LINEND); 97 sb.append("Content-Disposition: form-data; name="" + entry.getKey() + """ + LINEND); 98 sb.append("Content-Type: text/plain; charset=" + CHARSET + LINEND); 99 sb.append("Content-Transfer-Encoding: 8bit" + LINEND); 100 sb.append(LINEND); 101 sb.append(entry.getValue()); 102 sb.append(LINEND); 103 } 104 105 106 DataOutputStream outStream = new DataOutputStream(conn.getOutputStream()); 107 outStream.write(sb.toString().getBytes()); 108 // 发送文件数据 109 if (files != null) { 110 StringBuilder sb1 = new StringBuilder(); 111 sb1.append(PREFIX); 112 sb1.append(BOUNDARY); 113 sb1.append(LINEND); 114 sb1.append("Content-Disposition: form-data; name="uploadfile"; filename="" 115 + files.getName() + """ + LINEND); 116 sb1.append("Content-Type: application/octet-stream; charset=" + CHARSET + LINEND); 117 sb1.append(LINEND); 118 outStream.write(sb1.toString().getBytes()); 119 120 121 InputStream is = new FileInputStream(files); 122 byte[] buffer = new byte[1024]; 123 int len = 0; 124 while ((len = is.read(buffer)) != -1) { 125 outStream.write(buffer, 0, len); 126 } 127 128 is.close(); 129 outStream.write(LINEND.getBytes()); 130 } 131 132 133 // 请求结束标志 134 byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINEND).getBytes(); 135 outStream.write(end_data); 136 outStream.flush(); 137 // 得到响应码 138 int res = conn.getResponseCode(); 139 Log.i("UploadService", String.valueOf(res)); 140 InputStream in = conn.getInputStream(); 141 StringBuilder sb2 = new StringBuilder(); 142 if (res == 200) { 143 int ch; 144 while ((ch = in.read()) != -1) { 145 sb2.append((char) ch); 146 } 147 } 148 outStream.close(); 149 conn.disconnect(); 150 return sb2.toString(); 151 } 152 }
在build.gradle新增
apply plugin: 'jacoco' jacoco { toolVersion = '0.7.9' }
buildTypes { release {
// 在release下统计覆盖率信息 testCoverageEnabled = true } }
最重要的一行代码,加在监听设备按键的地方,如果连续2次点击设备back键,app已置于后台,则调用生成覆盖率方法。
1 @Override 2 public boolean onKeyDown(int keyCode, KeyEvent event) { 3 if (keyCode == KeyEvent.KEYCODE_BACK) { 4 .... 5 6 JacocoUtils.generateEcFile(true); 7 } 8 9 }
2. git apply patch
为了不影响工程代码,我这里用git apply patch的方式应用的上面的覆盖率代码
首先git commit上面的覆盖率代码
然后git log查看commit
我提交覆盖率代码的commit是最近的一次,然后拿到上一次的commit,并生成patch文件,-o是输出目录
git format-patch 0e4c................... -o ~/Documents/jk/script/
然后使用Jenkins自动打包,拉取最新代码后,在编译前Execute shell自动执行下面的命令,把覆盖率文件应用到工程内
git apply --reject ~/Documents/jk/script/0001-patch.patch
执行成功后的输出:
3. 服务器生成jacoco覆盖率报告
在服务器我也拉了一个Android工程,专门用于生成报告
主要在build.gradle新增
1 def coverageSourceDirs = [ 2 '../app/src/main/java' 3 ] 4 5 task jacocoTestReport(type: JacocoReport) { 6 group = "Reporting" 7 description = "Generate Jacoco coverage reports after running tests." 8 reports { 9 xml.enabled = true 10 html.enabled = true 11 } 12 classDirectories = fileTree( 13 dir: './build/intermediates/classes/debug', 14 excludes: ['**/R*.class', 15 '**/*$InjectAdapter.class', 16 '**/*$ModuleAdapter.class', 17 '**/*$ViewInjector*.class' 18 ]) 19 sourceDirectories = files(coverageSourceDirs) 20 executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec") 21 22 doFirst { 23 new File("$buildDir/intermediates/classes/").eachFileRecurse { file -> 24 if (file.name.contains('$$')) { 25 file.renameTo(file.path.replace('$$', '$')) 26 } 27 } 28 } 29 }
然后设备上传ec文件到Android工程的$buildDir/outputs/code-coverage/connected目录下,并依次执行
gradle createDebugCoverageReport
gradle jacocoTestReport
最后把$buildDir/reports/jacoco/目录下的覆盖率报告拷贝到展现的位置