• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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