blob: 1f784e95aa38b24d3adbd84c1b6e8fe2cca0eae2 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.device.metric;
import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.CLANG;
import static com.google.common.base.Verify.verifyNotNull;
import com.android.tradefed.build.BuildRetrievalError;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationReceiver;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.testtype.coverage.CoverageOptions;
import com.android.tradefed.util.AdbRootElevator;
import com.android.tradefed.util.ClangProfileIndexer;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.NativeCodeCoverageFlusher;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.TarUtil;
import com.android.tradefed.util.ZipUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* A {@link com.android.tradefed.device.metric.BaseDeviceMetricCollector} that will pull Clang
* coverage measurements off of the device and log them as test artifacts.
*/
public final class ClangCodeCoverageCollector extends BaseDeviceMetricCollector
implements IConfigurationReceiver {
// Finds .profraw files and compresses those files only. Stores the full
// path of the file on the device.
private static final String ZIP_CLANG_FILES_COMMAND_FORMAT =
"find %s -name '*.profraw' | tar -czf - -T - 2>/dev/null";
// Deletes .profraw files in the directory.
private static final String DELETE_COVERAGE_FILES_COMMAND_FORMAT =
"find %s -name '*.profraw' -delete";
private IConfiguration mConfiguration;
private IRunUtil mRunUtil = RunUtil.getDefault();
// Timeout for pulling coverage measurements from the device, in milliseconds.
private long mTimeoutMilli = 20 * 60 * 1000;
private File mLlvmProfileTool;
private NativeCodeCoverageFlusher mFlusher;
@Override
public void extraInit(IInvocationContext context, ITestInvocationListener listener)
throws DeviceNotAvailableException {
super.extraInit(context, listener);
setDisableReceiver(false);
verifyNotNull(mConfiguration);
setCoverageOptions(mConfiguration.getCoverageOptions());
if (isClangCoverageEnabled()
&& mConfiguration.getCoverageOptions().shouldResetCoverageBeforeTest()) {
for (ITestDevice device : getRealDevices()) {
// Clear coverage measurements on the device.
try (AdbRootElevator adbRoot = new AdbRootElevator(device)) {
getCoverageFlusher(device).deleteCoverageMeasurements();
}
}
}
}
@Override
public void setConfiguration(IConfiguration configuration) {
mConfiguration = configuration;
}
@Override
public void rebootEnded(ITestDevice device) throws DeviceNotAvailableException {
if (isClangCoverageEnabled()
&& mConfiguration.getCoverageOptions().shouldResetCoverageBeforeTest()) {
getCoverageFlusher(device).deleteCoverageMeasurements();
}
}
@VisibleForTesting
public void setRunUtil(IRunUtil runUtil) {
mRunUtil = runUtil;
if (mFlusher != null) {
mFlusher.setRunUtil(runUtil);
}
}
@Override
public void onTestRunEnd(DeviceMetricData runData, final Map<String, Metric> currentRunMetrics)
throws DeviceNotAvailableException {
if (!isClangCoverageEnabled()) {
return;
}
for (ITestDevice device : getRealDevices()) {
try (AdbRootElevator adbRoot = new AdbRootElevator(device)) {
if (mConfiguration.getCoverageOptions().isCoverageFlushEnabled()) {
getCoverageFlusher(device).forceCoverageFlush();
}
logCoverageMeasurement(device, generateMeasurementFileName());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/** Generate the .profdata file prefix in format "$moduleName_MODULE_$runName". */
private String generateMeasurementFileName() {
String moduleName = Strings.nullToEmpty(getModuleName());
if (moduleName.length() > 0) {
moduleName += "_MODULE_";
}
return moduleName + getRunName().replace(' ', '_');
}
/**
* Logs Clang coverage measurements from the device.
*
* @param runName name used in the log file
* @throws DeviceNotAvailableException
* @throws IOException
*/
private void logCoverageMeasurement(ITestDevice device, String runName)
throws DeviceNotAvailableException, IOException {
Map<String, File> untarDirs = new HashMap<>();
File profileTool = null;
File indexedProfileFile = null;
try {
Set<String> rawProfileFiles = new HashSet<>();
for (String devicePath : mConfiguration.getCoverageOptions().getDeviceCoveragePaths()) {
File coverageTarGz = FileUtil.createTempFile("clang_coverage", ".tar.gz");
try {
// Compress coverage measurements on the device before streaming to the host.
try (OutputStream out =
new BufferedOutputStream(new FileOutputStream(coverageTarGz))) {
device.executeShellV2Command(
String.format(
ZIP_CLANG_FILES_COMMAND_FORMAT, devicePath), // Command
null, // File pipe as input
out, // OutputStream to write to
mTimeoutMilli, // Timeout in milliseconds
TimeUnit.MILLISECONDS, // Timeout units
1); // Retry count
}
File untarDir = TarUtil.extractTarGzipToTemp(coverageTarGz, "clang_coverage");
untarDirs.put(devicePath, untarDir);
rawProfileFiles.addAll(
FileUtil.findFiles(
untarDir,
mConfiguration.getCoverageOptions().getProfrawFilter()));
} catch (IOException e) {
CLog.e("Failed to pull Clang coverage data from %s", devicePath);
CLog.e(e);
} finally {
FileUtil.deleteFile(coverageTarGz);
}
}
if (rawProfileFiles.isEmpty()) {
CLog.i("No Clang code coverage measurements found.");
return;
}
CLog.i("Received %d Clang code coverage measurements.", rawProfileFiles.size());
ClangProfileIndexer indexer = new ClangProfileIndexer(getProfileTool(), mRunUtil);
// Create the output file.
indexedProfileFile =
FileUtil.createTempFile(runName + "_clang_runtime_coverage", ".profdata");
indexer.index(rawProfileFiles, indexedProfileFile);
try (FileInputStreamSource source =
new FileInputStreamSource(indexedProfileFile, true)) {
testLog(runName + "_clang_runtime_coverage", LogDataType.CLANG_COVERAGE, source);
}
} finally {
// Delete coverage files on the device.
for (String devicePath : mConfiguration.getCoverageOptions().getDeviceCoveragePaths()) {
device.executeShellCommand(
String.format(DELETE_COVERAGE_FILES_COMMAND_FORMAT, devicePath));
}
for (File untarDir : untarDirs.values()) {
FileUtil.recursiveDelete(untarDir);
}
FileUtil.recursiveDelete(mLlvmProfileTool);
FileUtil.deleteFile(indexedProfileFile);
}
}
/**
* Creates a {@link NativeCodeCoverageFlusher} if one does not already exist.
*
* @return a NativeCodeCoverageFlusher
*/
private NativeCodeCoverageFlusher getCoverageFlusher(ITestDevice device) {
if (mFlusher == null) {
verifyNotNull(mConfiguration);
mFlusher = new NativeCodeCoverageFlusher(device, mConfiguration.getCoverageOptions());
mFlusher.setRunUtil(mRunUtil);
}
return mFlusher;
}
private boolean isClangCoverageEnabled() {
return mConfiguration != null
&& mConfiguration.getCoverageOptions().isCoverageEnabled()
&& mConfiguration.getCoverageOptions().getCoverageToolchains().contains(CLANG);
}
/**
* Retrieves the profile tool and dependencies from the build, and extracts them.
*
* @return the directory containing the profile tool and dependencies
*/
private File getProfileTool() throws IOException {
// If llvm-profdata-path was set in the Configuration, pass it through. Don't save the path
// locally since the parent process is responsible for cleaning it up.
File configurationTool = mConfiguration.getCoverageOptions().getLlvmProfdataPath();
if (configurationTool != null) {
return configurationTool;
}
if (mLlvmProfileTool != null && mLlvmProfileTool.exists()) {
return mLlvmProfileTool;
}
// Otherwise, try to download llvm-profdata.zip from the build and cache it.
File profileToolZip = null;
for (IBuildInfo info : getBuildInfos()) {
if (info.getFile("llvm-profdata.zip") != null) {
profileToolZip = info.getFile("llvm-profdata.zip");
mLlvmProfileTool = ZipUtil.extractZipToTemp(profileToolZip, "llvm-profdata");
return mLlvmProfileTool;
}
}
try {
// TODO: Delete this, we shouldn't have re-entry in the build
// provider this can cause quite a lot of overhead.
IBuildInfo buildInfo = mConfiguration.getBuildProvider().getBuild();
profileToolZip =
verifyNotNull(
buildInfo.getFile("llvm-profdata.zip"),
"Could not get llvm-profdata.zip from the build.");
mLlvmProfileTool = ZipUtil.extractZipToTemp(profileToolZip, "llvm-profdata");
return mLlvmProfileTool;
} catch (BuildRetrievalError e) {
throw new RuntimeException(e);
} finally {
FileUtil.deleteFile(profileToolZip);
}
}
private void setCoverageOptions(CoverageOptions coverageOptions) {
mTimeoutMilli = coverageOptions.getPullTimeout();
}
}