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