1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tradefed.device.metric; 18 19 import static com.google.common.base.Verify.verifyNotNull; 20 import static com.google.common.io.Files.getNameWithoutExtension; 21 22 import com.android.tradefed.config.IConfiguration; 23 import com.android.tradefed.config.IConfigurationReceiver; 24 import com.android.tradefed.device.DeviceNotAvailableException; 25 import com.android.tradefed.device.ITestDevice; 26 import com.android.tradefed.invoker.IInvocationContext; 27 import com.android.tradefed.log.LogUtil.CLog; 28 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 29 import com.android.tradefed.result.FileInputStreamSource; 30 import com.android.tradefed.result.ITestInvocationListener; 31 import com.android.tradefed.result.LogDataType; 32 import com.android.tradefed.testtype.coverage.CoverageOptions; 33 import com.android.tradefed.util.AdbRootElevator; 34 import com.android.tradefed.util.CommandResult; 35 import com.android.tradefed.util.CommandStatus; 36 import com.android.tradefed.util.FileUtil; 37 import com.android.tradefed.util.JavaCodeCoverageFlusher; 38 import com.android.tradefed.util.ProcessInfo; 39 import com.android.tradefed.util.PsParser; 40 import com.android.tradefed.util.TarUtil; 41 42 import com.google.common.annotations.VisibleForTesting; 43 import com.google.common.base.Splitter; 44 import com.google.common.base.Strings; 45 46 import org.jacoco.core.tools.ExecFileLoader; 47 48 import java.io.BufferedOutputStream; 49 import java.io.File; 50 import java.io.FileOutputStream; 51 import java.io.IOException; 52 import java.io.OutputStream; 53 import java.util.ArrayList; 54 import java.util.List; 55 import java.util.Map; 56 import java.util.concurrent.TimeUnit; 57 58 /** 59 * A {@link com.android.tradefed.device.metric.BaseDeviceMetricCollector} that will pull Java 60 * coverage measurements off of the device and log them as test artifacts. 61 */ 62 public final class JavaCodeCoverageCollector extends BaseDeviceMetricCollector 63 implements IConfigurationReceiver { 64 65 public static final String MERGE_COVERAGE_MEASUREMENTS_TEST_NAME = "mergeCoverageMeasurements"; 66 public static final String COVERAGE_MEASUREMENT_KEY = "coverageFilePath"; 67 public static final String COVERAGE_DIRECTORY = "/data/misc/trace"; 68 public static final String FIND_COVERAGE_FILES = 69 String.format("find %s -name '*.ec'", COVERAGE_DIRECTORY); 70 public static final String COMPRESS_COVERAGE_FILES = 71 String.format("%s | tar -czf - -T - 2>/dev/null", FIND_COVERAGE_FILES); 72 73 private ExecFileLoader mExecFileLoader; 74 75 private JavaCodeCoverageFlusher mFlusher; 76 private IConfiguration mConfiguration; 77 // Timeout for pulling coverage files from the device, in milliseconds. 78 private long mTimeoutMilli = 20 * 60 * 1000; 79 80 @Override extraInit(IInvocationContext context, ITestInvocationListener listener)81 public void extraInit(IInvocationContext context, ITestInvocationListener listener) 82 throws DeviceNotAvailableException { 83 super.extraInit(context, listener); 84 85 verifyNotNull(mConfiguration); 86 setCoverageOptions(mConfiguration.getCoverageOptions()); 87 88 if (isJavaCoverageEnabled() 89 && mConfiguration.getCoverageOptions().shouldResetCoverageBeforeTest()) { 90 for (ITestDevice device : getRealDevices()) { 91 try (AdbRootElevator adbRoot = new AdbRootElevator(device)) { 92 getCoverageFlusher(device).resetCoverage(); 93 } 94 } 95 } 96 } 97 98 @Override setConfiguration(IConfiguration configuration)99 public void setConfiguration(IConfiguration configuration) { 100 mConfiguration = configuration; 101 } 102 getCoverageFlusher(ITestDevice device)103 private JavaCodeCoverageFlusher getCoverageFlusher(ITestDevice device) { 104 if (mFlusher == null) { 105 mFlusher = 106 new JavaCodeCoverageFlusher( 107 device, mConfiguration.getCoverageOptions().getCoverageProcesses()); 108 } 109 return mFlusher; 110 } 111 112 @VisibleForTesting setCoverageFlusher(JavaCodeCoverageFlusher flusher)113 void setCoverageFlusher(JavaCodeCoverageFlusher flusher) { 114 mFlusher = flusher; 115 } 116 117 @Override onTestRunEnd(DeviceMetricData runData, final Map<String, Metric> runMetrics)118 public void onTestRunEnd(DeviceMetricData runData, final Map<String, Metric> runMetrics) 119 throws DeviceNotAvailableException { 120 if (!isJavaCoverageEnabled()) { 121 return; 122 } 123 124 String testCoveragePath = null; 125 126 // Get the path of the coverage measurement on the device. 127 Metric devicePathMetric = runMetrics.get(COVERAGE_MEASUREMENT_KEY); 128 if (devicePathMetric == null) { 129 CLog.d("No Java code coverage measurement."); 130 } else { 131 testCoveragePath = devicePathMetric.getMeasurements().getSingleString(); 132 if (testCoveragePath == null) { 133 CLog.d("No Java code coverage measurement."); 134 } 135 } 136 137 for (ITestDevice device : getRealDevices()) { 138 File testCoverage = null; 139 File coverageTarGz = null; 140 File untarDir = null; 141 142 try (AdbRootElevator adbRoot = new AdbRootElevator(device)) { 143 if (mConfiguration.getCoverageOptions().isCoverageFlushEnabled()) { 144 getCoverageFlusher(device).forceCoverageFlush(); 145 } 146 147 // Pull and log the test coverage file. 148 if (testCoveragePath != null) { 149 if (!new File(testCoveragePath).isAbsolute()) { 150 testCoveragePath = 151 "/sdcard/googletest/internal_use/" + testCoveragePath; 152 } 153 testCoverage = device.pullFile(testCoveragePath); 154 if (testCoverage == null) { 155 // Log a warning only, since multi-device tests will not have this file on 156 // all devices. 157 CLog.w( 158 "Failed to pull test coverage file %s from the device.", 159 testCoveragePath); 160 } else { 161 saveCoverageMeasurement(testCoverage); 162 } 163 } 164 165 // Stream compressed coverage measurements from /data/misc/trace to the host. 166 coverageTarGz = FileUtil.createTempFile("java_coverage", ".tar.gz"); 167 try (OutputStream out = 168 new BufferedOutputStream(new FileOutputStream(coverageTarGz))) { 169 CommandResult result = 170 device.executeShellV2Command( 171 COMPRESS_COVERAGE_FILES, 172 null, 173 out, 174 mTimeoutMilli, 175 TimeUnit.MILLISECONDS, 176 1); 177 if (!CommandStatus.SUCCESS.equals(result.getStatus())) { 178 CLog.e( 179 "Failed to stream coverage data from the device: %s", 180 result.toString()); 181 } 182 } 183 184 // Decompress the files and log the measurements. 185 untarDir = TarUtil.extractTarGzipToTemp(coverageTarGz, "java_coverage"); 186 for (String coveragePath : FileUtil.findFiles(untarDir, ".*\\.ec")) { 187 saveCoverageMeasurement(new File(coveragePath)); 188 } 189 } catch (IOException e) { 190 throw new RuntimeException(e); 191 } finally { 192 // Clean up local coverage files. 193 FileUtil.deleteFile(testCoverage); 194 FileUtil.deleteFile(coverageTarGz); 195 FileUtil.recursiveDelete(untarDir); 196 197 // Clean up device coverage files. 198 cleanUpDeviceCoverageFiles(device); 199 } 200 } 201 202 // Log the merged coverage data file if the flag is set. 203 if (shouldMergeCoverage() && (mExecFileLoader != null)) { 204 File mergedCoverage = null; 205 try { 206 mergedCoverage = FileUtil.createTempFile("merged_java_coverage", ".ec"); 207 mExecFileLoader.save(mergedCoverage, false); 208 logCoverageMeasurement(mergedCoverage); 209 } catch (IOException e) { 210 throw new RuntimeException(e); 211 } finally { 212 mExecFileLoader = null; 213 FileUtil.deleteFile(mergedCoverage); 214 } 215 } 216 } 217 218 /** Saves Java coverage file data. */ saveCoverageMeasurement(File coverageFile)219 private void saveCoverageMeasurement(File coverageFile) throws IOException { 220 if (shouldMergeCoverage()) { 221 if (mExecFileLoader == null) { 222 mExecFileLoader = new ExecFileLoader(); 223 } 224 mExecFileLoader.load(coverageFile); 225 } else { 226 logCoverageMeasurement(coverageFile); 227 } 228 } 229 230 /** Logs files as Java coverage measurements. */ logCoverageMeasurement(File coverageFile)231 private void logCoverageMeasurement(File coverageFile) { 232 try (FileInputStreamSource source = new FileInputStreamSource(coverageFile, true)) { 233 testLog(generateMeasurementFileName(coverageFile), LogDataType.COVERAGE, source); 234 } 235 } 236 237 /** Generate the .ec file prefix in format "$moduleName_MODULE_$runName". */ generateMeasurementFileName(File coverageFile)238 private String generateMeasurementFileName(File coverageFile) { 239 String moduleName = Strings.nullToEmpty(getModuleName()); 240 if (moduleName.length() > 0) { 241 moduleName += "_MODULE_"; 242 } 243 return moduleName 244 + getRunName() 245 + "_" 246 + getNameWithoutExtension(coverageFile.getName()) 247 + "_runtime_coverage"; 248 } 249 250 /** Cleans up .ec files in /data/misc/trace. */ cleanUpDeviceCoverageFiles(ITestDevice device)251 private void cleanUpDeviceCoverageFiles(ITestDevice device) throws DeviceNotAvailableException { 252 try (AdbRootElevator root = new AdbRootElevator(device)) { 253 List<Integer> activePids = getRunningProcessIds(device); 254 255 String fileList = device.executeShellCommand(FIND_COVERAGE_FILES); 256 for (String devicePath : Splitter.on('\n').omitEmptyStrings().split(fileList)) { 257 if (devicePath.endsWith(".mm.ec")) { 258 // Check if the process was still running. The file will have the format 259 // /data/misc/trace/jacoco-XXXXX.mm.ec where XXXXX is the process id. 260 int start = devicePath.indexOf('-') + 1; 261 int end = devicePath.indexOf('.'); 262 int pid = Integer.parseInt(devicePath.substring(start, end)); 263 if (!activePids.contains(pid)) { 264 device.deleteFile(devicePath); 265 } 266 } else { 267 device.deleteFile(devicePath); 268 } 269 } 270 } 271 } 272 273 /** Parses the output of `ps -e` to get a list of running process ids. */ getRunningProcessIds(ITestDevice device)274 private List<Integer> getRunningProcessIds(ITestDevice device) 275 throws DeviceNotAvailableException { 276 List<ProcessInfo> processes = PsParser.getProcesses(device.executeShellCommand("ps -e")); 277 List<Integer> pids = new ArrayList<>(); 278 279 for (ProcessInfo process : processes) { 280 pids.add(process.getPid()); 281 } 282 return pids; 283 } 284 isJavaCoverageEnabled()285 private boolean isJavaCoverageEnabled() { 286 return mConfiguration != null 287 && mConfiguration.getCoverageOptions().isCoverageEnabled() 288 && mConfiguration 289 .getCoverageOptions() 290 .getCoverageToolchains() 291 .contains(CoverageOptions.Toolchain.JACOCO); 292 } 293 shouldMergeCoverage()294 private boolean shouldMergeCoverage() { 295 return mConfiguration != null && mConfiguration.getCoverageOptions().shouldMergeCoverage(); 296 } 297 setCoverageOptions(CoverageOptions coverageOptions)298 private void setCoverageOptions(CoverageOptions coverageOptions) { 299 mTimeoutMilli = coverageOptions.getPullTimeout(); 300 } 301 } 302