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 try { 144 if (mConfiguration.getCoverageOptions().isCoverageFlushEnabled()) { 145 getCoverageFlusher(device).forceCoverageFlush(); 146 } 147 148 // Pull and log the test coverage file. 149 if (testCoveragePath != null) { 150 if (!new File(testCoveragePath).isAbsolute()) { 151 testCoveragePath = 152 "/sdcard/googletest/internal_use/" + testCoveragePath; 153 } 154 testCoverage = device.pullFile(testCoveragePath); 155 if (testCoverage == null) { 156 // Log a warning only, since multi-device tests will not have this file 157 // on 158 // all devices. 159 CLog.w( 160 "Failed to pull test coverage file %s from the device.", 161 testCoveragePath); 162 } else { 163 saveCoverageMeasurement(testCoverage); 164 } 165 } 166 167 // Stream compressed coverage measurements from /data/misc/trace to the host. 168 coverageTarGz = FileUtil.createTempFile("java_coverage", ".tar.gz"); 169 try (OutputStream out = 170 new BufferedOutputStream(new FileOutputStream(coverageTarGz))) { 171 CommandResult result = 172 device.executeShellV2Command( 173 COMPRESS_COVERAGE_FILES, 174 null, 175 out, 176 mTimeoutMilli, 177 TimeUnit.MILLISECONDS, 178 1); 179 if (!CommandStatus.SUCCESS.equals(result.getStatus())) { 180 CLog.e( 181 "Failed to stream coverage data from the device: %s", 182 result.toString()); 183 } 184 } 185 186 // Decompress the files and log the measurements. 187 untarDir = TarUtil.extractTarGzipToTemp(coverageTarGz, "java_coverage"); 188 for (String coveragePath : FileUtil.findFiles(untarDir, ".*\\.ec")) { 189 saveCoverageMeasurement(new File(coveragePath)); 190 } 191 } catch (IOException e) { 192 throw new RuntimeException(e); 193 } finally { 194 // Clean up local coverage files. 195 FileUtil.deleteFile(testCoverage); 196 FileUtil.deleteFile(coverageTarGz); 197 FileUtil.recursiveDelete(untarDir); 198 199 // Clean up device coverage files. 200 cleanUpDeviceCoverageFiles(device); 201 } 202 } 203 } 204 205 // Log the merged coverage data file if the flag is set. 206 if (shouldMergeCoverage() && (mExecFileLoader != null)) { 207 File mergedCoverage = null; 208 try { 209 mergedCoverage = FileUtil.createTempFile("merged_java_coverage", ".ec"); 210 mExecFileLoader.save(mergedCoverage, false); 211 logCoverageMeasurement(mergedCoverage); 212 } catch (IOException e) { 213 throw new RuntimeException(e); 214 } finally { 215 mExecFileLoader = null; 216 FileUtil.deleteFile(mergedCoverage); 217 } 218 } 219 } 220 221 /** Saves Java coverage file data. */ saveCoverageMeasurement(File coverageFile)222 private void saveCoverageMeasurement(File coverageFile) throws IOException { 223 if (shouldMergeCoverage()) { 224 if (mExecFileLoader == null) { 225 mExecFileLoader = new ExecFileLoader(); 226 } 227 mExecFileLoader.load(coverageFile); 228 } else { 229 logCoverageMeasurement(coverageFile); 230 } 231 } 232 233 /** Logs files as Java coverage measurements. */ logCoverageMeasurement(File coverageFile)234 private void logCoverageMeasurement(File coverageFile) { 235 try (FileInputStreamSource source = new FileInputStreamSource(coverageFile, true)) { 236 testLog(generateMeasurementFileName(coverageFile), LogDataType.COVERAGE, source); 237 } 238 } 239 240 /** Generate the .ec file prefix in format "$moduleName_MODULE_$runName". */ generateMeasurementFileName(File coverageFile)241 private String generateMeasurementFileName(File coverageFile) { 242 String moduleName = Strings.nullToEmpty(getModuleName()); 243 if (moduleName.length() > 0) { 244 moduleName += "_MODULE_"; 245 } 246 return moduleName 247 + getRunName() 248 + "_" 249 + getNameWithoutExtension(coverageFile.getName()) 250 + "_runtime_coverage"; 251 } 252 253 /** Cleans up .ec files in /data/misc/trace. */ cleanUpDeviceCoverageFiles(ITestDevice device)254 private void cleanUpDeviceCoverageFiles(ITestDevice device) throws DeviceNotAvailableException { 255 List<Integer> activePids = getRunningProcessIds(device); 256 257 String fileList = device.executeShellCommand(FIND_COVERAGE_FILES); 258 for (String devicePath : Splitter.on('\n').omitEmptyStrings().split(fileList)) { 259 if (devicePath.endsWith(".mm.ec")) { 260 // Check if the process was still running. The file will have the format 261 // /data/misc/trace/jacoco-XXXXX.mm.ec where XXXXX is the process id. 262 int start = devicePath.indexOf('-') + 1; 263 int end = devicePath.indexOf('.'); 264 int pid = Integer.parseInt(devicePath.substring(start, end)); 265 if (!activePids.contains(pid)) { 266 device.deleteFile(devicePath); 267 } 268 } else { 269 device.deleteFile(devicePath); 270 } 271 } 272 } 273 274 /** Parses the output of `ps -e` to get a list of running process ids. */ getRunningProcessIds(ITestDevice device)275 private List<Integer> getRunningProcessIds(ITestDevice device) 276 throws DeviceNotAvailableException { 277 List<ProcessInfo> processes = PsParser.getProcesses(device.executeShellCommand("ps -e")); 278 List<Integer> pids = new ArrayList<>(); 279 280 for (ProcessInfo process : processes) { 281 pids.add(process.getPid()); 282 } 283 return pids; 284 } 285 isJavaCoverageEnabled()286 private boolean isJavaCoverageEnabled() { 287 return mConfiguration != null 288 && mConfiguration.getCoverageOptions().isCoverageEnabled() 289 && mConfiguration 290 .getCoverageOptions() 291 .getCoverageToolchains() 292 .contains(CoverageOptions.Toolchain.JACOCO); 293 } 294 shouldMergeCoverage()295 private boolean shouldMergeCoverage() { 296 return mConfiguration != null && mConfiguration.getCoverageOptions().shouldMergeCoverage(); 297 } 298 setCoverageOptions(CoverageOptions coverageOptions)299 private void setCoverageOptions(CoverageOptions coverageOptions) { 300 mTimeoutMilli = coverageOptions.getPullTimeout(); 301 } 302 } 303