1 /* 2 * Copyright (C) 2025 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.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.CLANG; 20 21 import static com.google.common.base.Verify.verifyNotNull; 22 import static com.google.common.io.Files.getNameWithoutExtension; 23 24 import com.android.tradefed.build.IBuildInfo; 25 import com.android.tradefed.config.IConfiguration; 26 import com.android.tradefed.config.IConfigurationReceiver; 27 import com.android.tradefed.device.DeviceNotAvailableException; 28 import com.android.tradefed.device.ITestDevice; 29 import com.android.tradefed.invoker.IInvocationContext; 30 import com.android.tradefed.log.LogUtil.CLog; 31 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 32 import com.android.tradefed.result.FileInputStreamSource; 33 import com.android.tradefed.result.ITestInvocationListener; 34 import com.android.tradefed.result.LogDataType; 35 import com.android.tradefed.testtype.coverage.CoverageOptions; 36 import com.android.tradefed.util.AdbRootElevator; 37 import com.android.tradefed.util.ClangProfileIndexer; 38 import com.android.tradefed.util.CommandResult; 39 import com.android.tradefed.util.CommandStatus; 40 import com.android.tradefed.util.FileUtil; 41 import com.android.tradefed.util.IRunUtil; 42 import com.android.tradefed.util.JavaCodeCoverageFlusher; 43 import com.android.tradefed.util.NativeCodeCoverageFlusher; 44 import com.android.tradefed.util.ProcessInfo; 45 import com.android.tradefed.util.PsParser; 46 import com.android.tradefed.util.RunUtil; 47 import com.android.tradefed.util.TarUtil; 48 import com.android.tradefed.util.ZipUtil; 49 50 import com.google.common.annotations.VisibleForTesting; 51 import com.google.common.base.Splitter; 52 import com.google.common.base.Strings; 53 54 import org.jacoco.core.tools.ExecFileLoader; 55 56 import java.io.BufferedOutputStream; 57 import java.io.File; 58 import java.io.FileOutputStream; 59 import java.io.IOException; 60 import java.io.OutputStream; 61 import java.util.ArrayList; 62 import java.util.HashMap; 63 import java.util.HashSet; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Set; 67 import java.util.concurrent.TimeUnit; 68 69 /** 70 * A {@link com.android.tradefed.device.metric.BaseDeviceMetricCollector} that will pull Java and 71 * native coverage measurements off of the device and log them as test artifacts. 72 */ 73 public final class CodeCoverageCollector extends BaseDeviceMetricCollector 74 implements IConfigurationReceiver { 75 76 public static final String COVERAGE_MEASUREMENT_KEY = "coverageFilePath"; 77 public static final String COVERAGE_DIRECTORY = "/data/misc/trace"; 78 public static final String FIND_COVERAGE_FILES = 79 String.format("find %s -name '*.ec'", COVERAGE_DIRECTORY); 80 public static final String COMPRESS_COVERAGE_FILES = 81 String.format("%s | tar -czf - -T - 2>/dev/null", FIND_COVERAGE_FILES); 82 83 // Finds .profraw files and compresses those files only. Stores the full 84 // path of the file on the device. 85 private static final String ZIP_CLANG_FILES_COMMAND_FORMAT = 86 "find %s -name '*.profraw' | tar -czf - -T - 2>/dev/null"; 87 88 // Deletes .profraw files in the directory. 89 private static final String DELETE_COVERAGE_FILES_COMMAND_FORMAT = 90 "find %s -name '*.profraw' -delete"; 91 92 private ExecFileLoader mExecFileLoader; 93 94 private JavaCodeCoverageFlusher mJavaFlusher; 95 96 private IRunUtil mRunUtil = RunUtil.getDefault(); 97 private NativeCodeCoverageFlusher mClangFlusher; 98 private File mLlvmProfileTool; 99 100 private IConfiguration mConfiguration; 101 // Timeout for pulling cross-process coverage files from the device, in milliseconds. 102 private long mTimeoutMilli = 20 * 60 * 1000; 103 104 @Override extraInit(IInvocationContext context, ITestInvocationListener listener)105 public void extraInit(IInvocationContext context, ITestInvocationListener listener) 106 throws DeviceNotAvailableException { 107 super.extraInit(context, listener); 108 109 verifyNotNull(mConfiguration); 110 setCoverageOptions(mConfiguration.getCoverageOptions()); 111 112 boolean initJavaCoverage = isJavaCoverageEnabled(); 113 boolean initClangCoverage = isClangCoverageEnabled(); 114 115 if (!initJavaCoverage && !initClangCoverage) { 116 return; 117 } 118 119 if (mConfiguration.getCoverageOptions().shouldResetCoverageBeforeTest()) { 120 for (ITestDevice device : getRealDevices()) { 121 try (AdbRootElevator adbRoot = new AdbRootElevator(device)) { 122 if (initJavaCoverage) { 123 getJavaCoverageFlusher(device).resetCoverage(); 124 } 125 if (initClangCoverage) { 126 getNativeCoverageFlusher(device).deleteCoverageMeasurements(); 127 } 128 } 129 } 130 } 131 } 132 133 @Override setConfiguration(IConfiguration configuration)134 public void setConfiguration(IConfiguration configuration) { 135 mConfiguration = configuration; 136 } 137 138 @Override rebootEnded(ITestDevice device)139 public void rebootEnded(ITestDevice device) throws DeviceNotAvailableException { 140 if (isClangCoverageEnabled() 141 && mConfiguration.getCoverageOptions().shouldResetCoverageBeforeTest()) { 142 getNativeCoverageFlusher(device).deleteCoverageMeasurements(); 143 } 144 } 145 146 @Override onTestRunEnd(DeviceMetricData runData, final Map<String, Metric> runMetrics)147 public void onTestRunEnd(DeviceMetricData runData, final Map<String, Metric> runMetrics) 148 throws DeviceNotAvailableException { 149 if (!isJavaCoverageEnabled() && !isClangCoverageEnabled()) { 150 return; 151 } 152 153 String testCoveragePath = null; 154 155 if (isJavaCoverageEnabled()) { 156 // Get the path of the coverage measurement on the device. 157 Metric devicePathMetric = runMetrics.get(COVERAGE_MEASUREMENT_KEY); 158 if (devicePathMetric == null) { 159 CLog.d("No Java code coverage measurement."); 160 } else { 161 testCoveragePath = devicePathMetric.getMeasurements().getSingleString(); 162 if (testCoveragePath == null) { 163 CLog.d("No Java code coverage measurement."); 164 } 165 } 166 } 167 168 for (ITestDevice device : getRealDevices()) { 169 File testCoverage = null; 170 File coverageTarGz = null; 171 File untarDir = null; 172 173 try (AdbRootElevator adbRoot = new AdbRootElevator(device)) { 174 try { 175 if (mConfiguration.getCoverageOptions().isCoverageFlushEnabled()) { 176 if (isJavaCoverageEnabled()) { 177 getJavaCoverageFlusher(device).forceCoverageFlush(); 178 } 179 if (isClangCoverageEnabled()) { 180 getNativeCoverageFlusher(device).forceCoverageFlush(); 181 } 182 } 183 184 if (isJavaCoverageEnabled()) { 185 // Pull and log the test coverage file. 186 if (testCoveragePath != null) { 187 if (!new File(testCoveragePath).isAbsolute()) { 188 testCoveragePath = 189 "/sdcard/googletest/internal_use/" + testCoveragePath; 190 } 191 testCoverage = device.pullFile(testCoveragePath); 192 if (testCoverage == null) { 193 // Log a warning only, since multi-device tests will not have this 194 // file on all devices. 195 CLog.w( 196 "Failed to pull test coverage file %s from the device.", 197 testCoveragePath); 198 } else { 199 saveJavaCoverageMeasurement(testCoverage); 200 } 201 } 202 203 // Stream compressed coverage measurements from /data/misc/trace to the 204 // host. 205 coverageTarGz = FileUtil.createTempFile("java_coverage", ".tar.gz"); 206 try (OutputStream out = 207 new BufferedOutputStream(new FileOutputStream(coverageTarGz))) { 208 CommandResult result = 209 device.executeShellV2Command( 210 COMPRESS_COVERAGE_FILES, 211 null, 212 out, 213 mTimeoutMilli, 214 TimeUnit.MILLISECONDS, 215 1); 216 if (!CommandStatus.SUCCESS.equals(result.getStatus())) { 217 CLog.e( 218 "Failed to stream coverage data from the device: %s", 219 result.toString()); 220 } 221 } 222 223 // Decompress the files and log the measurements. 224 untarDir = TarUtil.extractTarGzipToTemp(coverageTarGz, "java_coverage"); 225 for (String coveragePath : FileUtil.findFiles(untarDir, ".*\\.ec")) { 226 saveJavaCoverageMeasurement(new File(coveragePath)); 227 } 228 } 229 if (isClangCoverageEnabled()) { 230 logNativeCoverageMeasurement(device, generateNativeMeasurementFileName()); 231 } 232 } catch (IOException e) { 233 throw new RuntimeException(e); 234 } finally { 235 // Clean up local coverage files. 236 FileUtil.deleteFile(testCoverage); 237 FileUtil.deleteFile(coverageTarGz); 238 FileUtil.recursiveDelete(untarDir); 239 240 // Clean up device coverage files. 241 cleanUpDeviceCoverageFiles(device); 242 } 243 } 244 } 245 246 // Log the merged coverage data file if the flag is set. 247 if (shouldMergeCoverage() && (mExecFileLoader != null)) { 248 File mergedCoverage = null; 249 try { 250 mergedCoverage = FileUtil.createTempFile("merged_java_coverage", ".ec"); 251 mExecFileLoader.save(mergedCoverage, false); 252 logJavaCoverageMeasurement(mergedCoverage); 253 } catch (IOException e) { 254 throw new RuntimeException(e); 255 } finally { 256 mExecFileLoader = null; 257 FileUtil.deleteFile(mergedCoverage); 258 } 259 } 260 } 261 262 @VisibleForTesting setJavaCoverageFlusher(JavaCodeCoverageFlusher flusher)263 void setJavaCoverageFlusher(JavaCodeCoverageFlusher flusher) { 264 mJavaFlusher = flusher; 265 } 266 267 @VisibleForTesting setClangFlusherRunUtil(IRunUtil runUtil)268 void setClangFlusherRunUtil(IRunUtil runUtil) { 269 mRunUtil = runUtil; 270 if (mClangFlusher != null) { 271 mClangFlusher.setRunUtil(runUtil); 272 } 273 } 274 getJavaCoverageFlusher(ITestDevice device)275 private JavaCodeCoverageFlusher getJavaCoverageFlusher(ITestDevice device) { 276 if (mJavaFlusher == null) { 277 mJavaFlusher = 278 new JavaCodeCoverageFlusher( 279 device, mConfiguration.getCoverageOptions().getCoverageProcesses()); 280 } 281 return mJavaFlusher; 282 } 283 284 /** Saves Java coverage file data. */ saveJavaCoverageMeasurement(File coverageFile)285 private void saveJavaCoverageMeasurement(File coverageFile) throws IOException { 286 if (shouldMergeCoverage()) { 287 if (mExecFileLoader == null) { 288 mExecFileLoader = new ExecFileLoader(); 289 } 290 mExecFileLoader.load(coverageFile); 291 } else { 292 logJavaCoverageMeasurement(coverageFile); 293 } 294 } 295 296 /** Logs files as Java coverage measurements. */ logJavaCoverageMeasurement(File coverageFile)297 private void logJavaCoverageMeasurement(File coverageFile) { 298 try (FileInputStreamSource source = new FileInputStreamSource(coverageFile, true)) { 299 testLog(generateJavaMeasurementFileName(coverageFile), LogDataType.COVERAGE, source); 300 } 301 } 302 303 /** Generate the .ec file prefix in format "$moduleName_MODULE_$runName". */ generateJavaMeasurementFileName(File coverageFile)304 private String generateJavaMeasurementFileName(File coverageFile) { 305 String moduleName = Strings.nullToEmpty(getModuleName()); 306 if (moduleName.length() > 0) { 307 moduleName += "_MODULE_"; 308 } 309 return moduleName 310 + getRunName() 311 + "_" 312 + getNameWithoutExtension(coverageFile.getName()) 313 + "_runtime_coverage"; 314 } 315 316 /** Cleans up .ec files in /data/misc/trace. */ cleanUpDeviceCoverageFiles(ITestDevice device)317 private void cleanUpDeviceCoverageFiles(ITestDevice device) throws DeviceNotAvailableException { 318 List<Integer> activePids = getRunningProcessIds(device); 319 320 String fileList = device.executeShellCommand(FIND_COVERAGE_FILES); 321 for (String devicePath : Splitter.on('\n').omitEmptyStrings().split(fileList)) { 322 if (devicePath.endsWith(".mm.ec")) { 323 // Check if the process was still running. The file will have the format 324 // /data/misc/trace/jacoco-XXXXX.mm.ec where XXXXX is the process id. 325 int start = devicePath.indexOf('-') + 1; 326 int end = devicePath.indexOf('.'); 327 int pid = Integer.parseInt(devicePath.substring(start, end)); 328 if (!activePids.contains(pid)) { 329 device.deleteFile(devicePath); 330 } 331 } else { 332 device.deleteFile(devicePath); 333 } 334 } 335 } 336 337 /** Parses the output of `ps -e` to get a list of running process ids. */ getRunningProcessIds(ITestDevice device)338 private List<Integer> getRunningProcessIds(ITestDevice device) 339 throws DeviceNotAvailableException { 340 List<ProcessInfo> processes = PsParser.getProcesses(device.executeShellCommand("ps -e")); 341 List<Integer> pids = new ArrayList<>(); 342 343 for (ProcessInfo process : processes) { 344 pids.add(process.getPid()); 345 } 346 return pids; 347 } 348 isJavaCoverageEnabled()349 private boolean isJavaCoverageEnabled() { 350 return mConfiguration != null 351 && mConfiguration.getCoverageOptions().isCoverageEnabled() 352 && mConfiguration 353 .getCoverageOptions() 354 .getCoverageToolchains() 355 .contains(CoverageOptions.Toolchain.JACOCO); 356 } 357 isClangCoverageEnabled()358 private boolean isClangCoverageEnabled() { 359 return mConfiguration != null 360 && mConfiguration.getCoverageOptions().isCoverageEnabled() 361 && mConfiguration.getCoverageOptions().getCoverageToolchains().contains(CLANG); 362 } 363 364 /** 365 * Creates a {@link NativeCodeCoverageFlusher} if one does not already exist. 366 * 367 * @return a NativeCodeCoverageFlusher 368 */ getNativeCoverageFlusher(ITestDevice device)369 private NativeCodeCoverageFlusher getNativeCoverageFlusher(ITestDevice device) { 370 if (mClangFlusher == null) { 371 verifyNotNull(mConfiguration); 372 mClangFlusher = 373 new NativeCodeCoverageFlusher(device, mConfiguration.getCoverageOptions()); 374 mClangFlusher.setRunUtil(mRunUtil); 375 } 376 return mClangFlusher; 377 } 378 379 /** Generate the .profdata file prefix in format "$moduleName_MODULE_$runName". */ generateNativeMeasurementFileName()380 private String generateNativeMeasurementFileName() { 381 String moduleName = Strings.nullToEmpty(getModuleName()); 382 if (moduleName.length() > 0) { 383 moduleName += "_MODULE_"; 384 } 385 return moduleName + getRunName().replace(' ', '_'); 386 } 387 388 /** 389 * Logs Clang coverage measurements from the device. 390 * 391 * @param runName name used in the log file 392 * @throws DeviceNotAvailableException 393 * @throws IOException 394 */ logNativeCoverageMeasurement(ITestDevice device, String runName)395 private void logNativeCoverageMeasurement(ITestDevice device, String runName) 396 throws DeviceNotAvailableException, IOException { 397 Map<String, File> untarDirs = new HashMap<>(); 398 File profileTool = null; 399 File indexedProfileFile = null; 400 try { 401 Set<String> rawProfileFiles = new HashSet<>(); 402 for (String devicePath : mConfiguration.getCoverageOptions().getDeviceCoveragePaths()) { 403 File coverageTarGz = FileUtil.createTempFile("clang_coverage", ".tar.gz"); 404 405 try { 406 // Compress coverage measurements on the device before streaming to the host. 407 try (OutputStream out = 408 new BufferedOutputStream(new FileOutputStream(coverageTarGz))) { 409 device.executeShellV2Command( 410 String.format( 411 ZIP_CLANG_FILES_COMMAND_FORMAT, devicePath), // Command 412 null, // File pipe as input 413 out, // OutputStream to write to 414 mTimeoutMilli, // Timeout in milliseconds 415 TimeUnit.MILLISECONDS, // Timeout units 416 1); // Retry count 417 } 418 419 File untarDir = TarUtil.extractTarGzipToTemp(coverageTarGz, "clang_coverage"); 420 untarDirs.put(devicePath, untarDir); 421 rawProfileFiles.addAll( 422 FileUtil.findFiles( 423 untarDir, 424 mConfiguration.getCoverageOptions().getProfrawFilter())); 425 } catch (IOException e) { 426 CLog.e("Failed to pull Clang coverage data from %s", devicePath); 427 CLog.e(e); 428 } finally { 429 FileUtil.deleteFile(coverageTarGz); 430 } 431 } 432 433 if (rawProfileFiles.isEmpty()) { 434 CLog.i("No Clang code coverage measurements found."); 435 return; 436 } 437 438 CLog.i("Received %d Clang code coverage measurements.", rawProfileFiles.size()); 439 440 ClangProfileIndexer indexer = new ClangProfileIndexer(getProfileTool(), mRunUtil); 441 442 // Create the output file. 443 indexedProfileFile = 444 FileUtil.createTempFile(runName + "_clang_runtime_coverage", ".profdata"); 445 indexer.index(rawProfileFiles, indexedProfileFile); 446 447 try (FileInputStreamSource source = 448 new FileInputStreamSource(indexedProfileFile, true)) { 449 testLog(runName + "_clang_runtime_coverage", LogDataType.CLANG_COVERAGE, source); 450 } 451 } finally { 452 // Delete coverage files on the device. 453 for (String devicePath : mConfiguration.getCoverageOptions().getDeviceCoveragePaths()) { 454 device.executeShellCommand( 455 String.format(DELETE_COVERAGE_FILES_COMMAND_FORMAT, devicePath)); 456 } 457 for (File untarDir : untarDirs.values()) { 458 FileUtil.recursiveDelete(untarDir); 459 } 460 FileUtil.recursiveDelete(mLlvmProfileTool); 461 FileUtil.deleteFile(indexedProfileFile); 462 } 463 } 464 465 /** 466 * Retrieves the profile tool and dependencies from the build, and extracts them. 467 * 468 * @return the directory containing the profile tool and dependencies 469 */ getProfileTool()470 private File getProfileTool() throws IOException { 471 // If llvm-profdata-path was set in the Configuration, pass it through. Don't save the path 472 // locally since the parent process is responsible for cleaning it up. 473 File configurationTool = mConfiguration.getCoverageOptions().getLlvmProfdataPath(); 474 if (configurationTool != null) { 475 return configurationTool; 476 } 477 if (mLlvmProfileTool != null && mLlvmProfileTool.exists()) { 478 return mLlvmProfileTool; 479 } 480 481 // Otherwise, try to download llvm-profdata.zip from the build and cache it. 482 File profileToolZip = null; 483 for (IBuildInfo info : getBuildInfos()) { 484 if (info.getFile("llvm-profdata.zip") != null) { 485 profileToolZip = info.getFile("llvm-profdata.zip"); 486 mLlvmProfileTool = ZipUtil.extractZipToTemp(profileToolZip, "llvm-profdata"); 487 return mLlvmProfileTool; 488 } 489 } 490 return mLlvmProfileTool; 491 } 492 shouldMergeCoverage()493 private boolean shouldMergeCoverage() { 494 return mConfiguration != null && mConfiguration.getCoverageOptions().shouldMergeCoverage(); 495 } 496 setCoverageOptions(CoverageOptions coverageOptions)497 private void setCoverageOptions(CoverageOptions coverageOptions) { 498 mTimeoutMilli = coverageOptions.getPullTimeout(); 499 } 500 } 501