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