blob: 3b93111677abcd4cfdd9b315c69af8b1dbbf137c [file] [log] [blame]
/*
* Copyright (C) 2022 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.GCOV_KERNEL;
import static com.google.common.base.Verify.verifyNotNull;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationReceiver;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceRuntimeException;
import com.android.tradefed.device.INativeDevice;
import com.android.tradefed.device.NativeDevice;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.util.AdbRootElevator;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;
import com.google.common.base.Strings;
import java.io.File;
import java.util.Map;
/**
* A {@link com.android.tradefed.device.metric.BaseDeviceMetricCollector} that will pull gcov kernel
* coverage measurements out of debugfs and off of the device and then finally logs them as test
* artifacts.
*/
public final class GcovKernelCodeCoverageCollector extends BaseDeviceMetricCollector
implements IConfigurationReceiver {
public static final String RESET_GCOV_COUNTS_COMMAND =
String.format("echo 1 > %s/gcov/reset", NativeDevice.DEBUGFS_PATH);
public static final String MAKE_TEMP_DIR_COMMAND = "mktemp -d -p /data/local/tmp/";
public static final String MAKE_GCDA_TEMP_DIR_COMMAND_FMT = "mkdir -p %s";
public static final String COPY_GCOV_DATA_COMMAND_FMT = "cp -rf %s/* %s";
public static final String TAR_GCOV_DATA_COMMAND_FMT = "tar -czf %s -C %s %s";
private IConfiguration mConfiguration;
private boolean mTestRunStartFail;
private int mTestCount;
public GcovKernelCodeCoverageCollector() {
setDisableReceiver(false);
}
@Override
public void setConfiguration(IConfiguration config) {
mConfiguration = config;
}
private boolean isGcovKernelCoverageEnabled() {
return mConfiguration != null
&& mConfiguration.getCoverageOptions().isCoverageEnabled()
&& mConfiguration
.getCoverageOptions()
.getCoverageToolchains()
.contains(GCOV_KERNEL);
}
@Override
public void onTestRunStart(DeviceMetricData runData, int testCount)
throws DeviceNotAvailableException {
mTestCount = testCount;
if (!isGcovKernelCoverageEnabled()) {
return;
}
if (mTestCount == 0) {
CLog.i("No tests in test run, not collecting coverage for %s.", getTarBasename());
return;
}
try {
for (ITestDevice device : getRealDevices()) {
try (AdbRootElevator adbRoot = new AdbRootElevator(device)) {
device.mountDebugfs();
resetGcovCounts(device);
}
}
} catch (Throwable t) {
mTestRunStartFail = true;
throw t;
}
mTestRunStartFail = false;
}
@Override
public void onTestRunEnd(DeviceMetricData runData, final Map<String, Metric> currentRunMetrics)
throws DeviceNotAvailableException {
if (!isGcovKernelCoverageEnabled() || mTestCount == 0) {
return;
}
if (mTestRunStartFail) {
CLog.e("onTestRunStart failed, not collecting coverage for %s.", getTarBasename());
return;
}
for (ITestDevice device : getRealDevices()) {
try (AdbRootElevator adbRoot = new AdbRootElevator(device)) {
collectGcovDebugfsCoverage(device, getTarBasename());
device.unmountDebugfs();
}
}
}
@Override
public void rebootStarted(ITestDevice device) throws DeviceNotAvailableException {
super.rebootStarted(device);
try (AdbRootElevator adbRoot = new AdbRootElevator(device)) {
collectGcovDebugfsCoverage(device, getTarBasename());
}
}
@Override
public void rebootEnded(ITestDevice device) throws DeviceNotAvailableException {
super.rebootEnded(device);
try (AdbRootElevator adbRoot = new AdbRootElevator(device)) {
device.mountDebugfs();
}
}
/* Gets the name to be used for the collected coverage tar file.
* Prefer the module name if available otherwise use the run name.
*/
private String getTarBasename() {
String collectionFilename = getModuleName();
return Strings.isNullOrEmpty(collectionFilename) ? getRunName() : collectionFilename;
}
/** Reset gcov counts by writing to the gcov debugfs reset node. */
private void resetGcovCounts(ITestDevice device) throws DeviceNotAvailableException {
CommandResult result = device.executeShellV2Command(RESET_GCOV_COUNTS_COMMAND);
if (result.getStatus() != CommandStatus.SUCCESS) {
CLog.e("Failed to reset gcov counts for %s. %s", getTarBasename(), result);
throw new DeviceRuntimeException(
"'" + RESET_GCOV_COUNTS_COMMAND + "' has failed: " + result,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
}
/**
* Gather overage data files off of the device. This logic is was originally taken directly from
* the `gather_on_test.sh` script detailed here:
* https://www.kernel.org/doc/html/v4.15/dev-tools/gcov.html#appendix-b-gather-on-test-sh
* However, in practice the `find` + `cat` approach ended up taking a lot of time. The reasoning
* given for this approach was because of issues with the `seq_file` interface. It turns out
* this issue no longer applies to the `cp` command (it still applies to the `tar`). Discussion
* on this can b e found here:
* https://github.com/linux-test-project/lcov/discussions/199#discussion-4895422
*
* <p>TODO: Revert this summary back to the original text, once upstream patch lands that
* updates `gather_on_test.sh` `cp` instead of `find` + `cat`.
*/
private void collectGcovDebugfsCoverage(INativeDevice device, String name)
throws DeviceNotAvailableException {
if (!device.isDebugfsMounted()) {
String errorMessage =
String.format("debugfs not mounted, unable to collect for %s.", name);
CLog.e(errorMessage);
throw new DeviceRuntimeException(
errorMessage, DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
CommandResult result = device.executeShellV2Command(MAKE_TEMP_DIR_COMMAND);
if (result.getStatus() != CommandStatus.SUCCESS) {
CLog.e("Failed to create temp dir for %s. %s", name, result);
throw new DeviceRuntimeException(
"'" + MAKE_TEMP_DIR_COMMAND + "' has failed: " + result,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
String tempDir = result.getStdout().strip();
String gcda = "/d/gcov";
String gcdaTempDir = tempDir + gcda;
String makeGcdaTempDirCommand =
String.format(MAKE_GCDA_TEMP_DIR_COMMAND_FMT, gcdaTempDir);
result = device.executeShellV2Command(makeGcdaTempDirCommand);
if (result.getStatus() != CommandStatus.SUCCESS) {
CLog.e("Failed to create gcda temp directory %s. %s", gcdaTempDir, result);
throw new DeviceRuntimeException(
"'" + makeGcdaTempDirCommand + "' has failed: " + result,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
String tarName = String.format("%s.tar.gz", name);
String tarFullPath = String.format("%s/%s", tempDir, tarName);
String copyGcovDataCommand =
String.format(COPY_GCOV_DATA_COMMAND_FMT, gcda, gcdaTempDir);
result = device.executeShellV2Command(copyGcovDataCommand);
if (result.getStatus() != CommandStatus.SUCCESS) {
CLog.e("Failed to collect coverage files for %s. %s", name, result);
throw new DeviceRuntimeException(
"'" + copyGcovDataCommand + "' has failed: " + result,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
String tarCommand =
String.format(
TAR_GCOV_DATA_COMMAND_FMT, tarFullPath, tempDir, gcda.substring(1));
result = device.executeShellV2Command(tarCommand);
if (result.getStatus() != CommandStatus.SUCCESS) {
CLog.e("Failed to tar collected files for %s. %s", name, result);
throw new DeviceRuntimeException(
"'" + tarCommand + "' has failed: " + result,
DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
}
try {
// We specify the root's user id here, 0, because framework services may be stopped
// which would cause the non-user id version of this method to fail when it attempts
// to get the current user id which isn't needed.
File coverageTar = device.pullFile(tarFullPath, 0);
verifyNotNull(
coverageTar,
"Failed to pull the native kernel coverage file %s for %s",
tarFullPath,
name);
try (FileInputStreamSource source = new FileInputStreamSource(coverageTar, true)) {
String fileName =
String.format(
"%s_%d_kernel_coverage", name, System.currentTimeMillis());
testLog(fileName, LogDataType.GCOV_KERNEL_COVERAGE, source);
} finally {
FileUtil.deleteFile(coverageTar);
}
} finally {
device.deleteFile(tempDir);
}
}
}