改進了幾個點
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 = "\r\n"; 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/目錄下的覆蓋率報告拷貝到展現的位置