aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOliver Nguyen <olivernguyen@google.com>2024-03-19 14:57:03 -0700
committerOliver Nguyen <olivernguyen@google.com>2024-03-28 23:15:12 +0000
commita6e33b1d6b13b79e17710222fd8f4570a969546d (patch)
treeebc37d8898f0a3f2861e3b45fcb0b7fb780c02f2
parent162abc28f00b6f2ca66990a362d934853dfb17bd (diff)
downloadtradefederation-a6e33b1d6b13b79e17710222fd8f4570a969546d.tar.gz
Collect Clang coverage for native host tests.
Host GTests will collect Clang coverage measurements when coverage is enabled. Bug: 278116048 Test: atest adb_test --experimental-coverage Test: atest liblog-host-test --experimental-coverage Test: unit tests Change-Id: Iee5dfd48bb77a1aee1e7a5badf7232301e9924fb
-rw-r--r--javatests/com/android/tradefed/UnitTests.java2
-rw-r--r--javatests/com/android/tradefed/util/ClangProfileIndexerTest.java171
-rw-r--r--src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java59
-rw-r--r--src/com/android/tradefed/util/ClangProfileIndexer.java96
-rw-r--r--test_framework/com/android/tradefed/testtype/HostGTest.java42
5 files changed, 315 insertions, 55 deletions
diff --git a/javatests/com/android/tradefed/UnitTests.java b/javatests/com/android/tradefed/UnitTests.java
index 9abdf4a06..0ea56e08f 100644
--- a/javatests/com/android/tradefed/UnitTests.java
+++ b/javatests/com/android/tradefed/UnitTests.java
@@ -412,6 +412,7 @@ import com.android.tradefed.util.BugreportTest;
import com.android.tradefed.util.BuildTestsZipUtilsTest;
import com.android.tradefed.util.BundletoolUtilTest;
import com.android.tradefed.util.ByteArrayListTest;
+import com.android.tradefed.util.ClangProfileIndexerTest;
import com.android.tradefed.util.ClassPathScannerTest;
import com.android.tradefed.util.ConditionPriorityBlockingQueueTest;
import com.android.tradefed.util.DeviceActionUtilTest;
@@ -1017,6 +1018,7 @@ import org.junit.runners.Suite.SuiteClasses;
BundletoolUtilTest.class,
ByteArrayListTest.class,
CentralDirectoryInfoTest.class,
+ ClangProfileIndexerTest.class,
ClassPathScannerTest.class,
ConditionPriorityBlockingQueueTest.class,
DeviceActionUtilTest.class,
diff --git a/javatests/com/android/tradefed/util/ClangProfileIndexerTest.java b/javatests/com/android/tradefed/util/ClangProfileIndexerTest.java
new file mode 100644
index 000000000..422b1250a
--- /dev/null
+++ b/javatests/com/android/tradefed/util/ClangProfileIndexerTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2024 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.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Queue;
+
+/** Unit tests for {@link ClangProfileIndexer}. */
+@RunWith(JUnit4.class)
+public class ClangProfileIndexerTest {
+ private static final int MAX_PROFILE_FILES = 100;
+
+ @Rule public TemporaryFolder folder = new TemporaryFolder();
+ @Spy CommandArgumentCaptor mCommandArgumentCaptor;
+
+ /** Object under test. */
+ ClangProfileIndexer mIndexer;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mIndexer = new ClangProfileIndexer(new File("/path/to/llvm-tools"), mCommandArgumentCaptor);
+ }
+
+ @Test
+ public void testProfileFiles_includesFiles() throws Exception {
+ mCommandArgumentCaptor.setResult(CommandStatus.SUCCESS);
+ List<String> profileFiles =
+ ImmutableList.of("/path/to/clang-1.profraw", "/path/to/clang-2.profraw");
+ File outputFile = folder.newFile();
+
+ mIndexer.index(profileFiles, outputFile);
+
+ List<String> command = mCommandArgumentCaptor.getCommand();
+
+ // Check contents of the command line contain what we expect.
+ assertThat(command)
+ .containsAtLeast(
+ "/path/to/llvm-tools/bin/llvm-profdata",
+ "merge",
+ outputFile.getAbsolutePath());
+ assertThat(command).containsAtLeastElementsIn(profileFiles);
+ }
+
+ @Test
+ public void testLargeProfileCount_usesFile() throws Exception {
+ mCommandArgumentCaptor.setResult(CommandStatus.SUCCESS);
+ List<String> profileFiles = new ArrayList<>(MAX_PROFILE_FILES + 5);
+ for (int i = 0; i < MAX_PROFILE_FILES + 5; i++) {
+ profileFiles.add(String.format("path/to/clang-%d.profraw", i));
+ }
+
+ mIndexer.index(profileFiles, folder.newFile());
+ List<String> command = mCommandArgumentCaptor.getCommand();
+
+ // Contains the `-f` argument that points to a file containing the list of profile files.
+ assertThat(command.size()).isLessThan(MAX_PROFILE_FILES);
+ assertThat(command).contains("-f");
+ checkListDoesNotContainSuffix(command, ".profraw");
+ }
+
+ @Test
+ public void testSingleFailure_usesFailureMode() throws Exception {
+ mCommandArgumentCaptor.setResult(CommandStatus.FAILED).setResult(CommandStatus.SUCCESS);
+ List<String> profileFiles =
+ ImmutableList.of("/path/to/clang-1.profraw", "/path/to/clang-2.profraw");
+
+ mIndexer.index(profileFiles, folder.newFile());
+
+ List<String> command = mCommandArgumentCaptor.getCommand();
+ assertThat(command).contains("-failure-mode=all");
+ }
+
+ @Test
+ public void testMultipleFailures_throwsException() throws Exception {
+ mCommandArgumentCaptor.setResult(CommandStatus.FAILED).setResult(CommandStatus.FAILED);
+ List<String> profileFiles = ImmutableList.of("/path/to/clang-1.profraw");
+
+ try {
+ mIndexer.index(profileFiles, folder.newFile());
+ fail("should have thrown an exception");
+ } catch (IOException e) {
+ // Expected
+ }
+ }
+
+ abstract static class CommandArgumentCaptor implements IRunUtil {
+ private List<String> mCommand = new ArrayList<>();
+ private Queue<CommandStatus> mResults = new ArrayDeque<>();
+
+ /** Stores the command for retrieval later. */
+ @Override
+ public CommandResult runTimedCmd(long timeout, String... cmd) {
+ mCommand = Arrays.asList(cmd);
+ return new CommandResult(mResults.remove());
+ }
+
+ CommandArgumentCaptor setResult(CommandStatus status) {
+ mResults.add(status);
+ return this;
+ }
+
+ List<String> getCommand() {
+ return mCommand;
+ }
+
+ /** Ignores sleep calls. */
+ @Override
+ public void sleep(long ms) {}
+ }
+
+ /** Utility function to verify that certain suffixes are contained in the List. */
+ void checkListContainsSuffixes(List<String> list, List<String> suffixes) {
+ for (String suffix : suffixes) {
+ boolean found = false;
+ for (String item : list) {
+ if (item.endsWith(suffix)) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ fail("List " + list.toString() + " does not contain suffix '" + suffix + "'");
+ }
+ }
+ }
+
+ void checkListDoesNotContainSuffix(List<String> list, String suffix) {
+ for (String item : list) {
+ if (item.endsWith(suffix)) {
+ fail("List " + list.toString() + " should not contain suffix '" + suffix + "'");
+ }
+ }
+ }
+}
diff --git a/src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java b/src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java
index b747692ba..1f784e95a 100644
--- a/src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java
+++ b/src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java
@@ -26,18 +26,15 @@ 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.error.HarnessRuntimeException;
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.result.error.InfraErrorIdentifier;
import com.android.tradefed.testtype.coverage.CoverageOptions;
import com.android.tradefed.util.AdbRootElevator;
-import com.android.tradefed.util.CommandResult;
-import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.ClangProfileIndexer;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.NativeCodeCoverageFlusher;
@@ -53,13 +50,8 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -70,11 +62,6 @@ import java.util.concurrent.TimeUnit;
*/
public final class ClangCodeCoverageCollector extends BaseDeviceMetricCollector
implements IConfigurationReceiver {
- // Maximum number of profile files before writing the list to a file. Beyond this value,
- // llvm-profdata will use the -f option to read the list from a file to prevent exceeding
- // the command line length limit.
- private static final int MAX_PROFILE_FILES = 100;
-
// 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 =
@@ -152,7 +139,7 @@ public final class ClangCodeCoverageCollector extends BaseDeviceMetricCollector
}
}
- /** Generate the .ec file prefix in format "$moduleName_MODULE_$runName". */
+ /** Generate the .profdata file prefix in format "$moduleName_MODULE_$runName". */
private String generateMeasurementFileName() {
String moduleName = Strings.nullToEmpty(getModuleName());
if (moduleName.length() > 0) {
@@ -171,7 +158,6 @@ public final class ClangCodeCoverageCollector extends BaseDeviceMetricCollector
private void logCoverageMeasurement(ITestDevice device, String runName)
throws DeviceNotAvailableException, IOException {
Map<String, File> untarDirs = new HashMap<>();
- File fileList = null;
File profileTool = null;
File indexedProfileFile = null;
try {
@@ -214,48 +200,12 @@ public final class ClangCodeCoverageCollector extends BaseDeviceMetricCollector
CLog.i("Received %d Clang code coverage measurements.", rawProfileFiles.size());
- // Get the llvm-profdata tool from the build. This tool must match the same one used to
- // compile the build, otherwise this action will fail.
- profileTool = getProfileTool();
- Path profileBin = profileTool.toPath().resolve("bin/llvm-profdata");
- profileBin.toFile().setExecutable(true);
-
- List<String> command = new ArrayList<>();
- command.add(profileBin.toString());
- command.add("merge");
- command.add("-sparse");
-
- if (rawProfileFiles.size() > MAX_PROFILE_FILES) {
- // Write the measurement file list to a temporary file. This allows large numbers
- // of measurements to not exceed the command line length limit.
- fileList = FileUtil.createTempFile("clang_measurements", ".txt");
- Files.write(fileList.toPath(), rawProfileFiles, Charset.defaultCharset());
-
- // Add the file containing the list of .profraw files.
- command.add("-f");
- command.add(fileList.getAbsolutePath());
- } else {
- command.addAll(rawProfileFiles);
- }
+ ClangProfileIndexer indexer = new ClangProfileIndexer(getProfileTool(), mRunUtil);
// Create the output file.
indexedProfileFile =
FileUtil.createTempFile(runName + "_clang_runtime_coverage", ".profdata");
- command.add("-o");
- command.add(indexedProfileFile.getAbsolutePath());
-
- CommandResult result = mRunUtil.runTimedCmd(0, command.toArray(new String[0]));
- if (result.getStatus() != CommandStatus.SUCCESS) {
- // Retry with -failure-mode=all to still be able to report some coverage.
- command.add("-failure-mode=all");
- result = mRunUtil.runTimedCmd(0, command.toArray(new String[0]));
-
- if (result.getStatus() != CommandStatus.SUCCESS) {
- throw new HarnessRuntimeException(
- "Failed to merge Clang profile data:\n" + result.toString(),
- InfraErrorIdentifier.CODE_COVERAGE_ERROR);
- }
- }
+ indexer.index(rawProfileFiles, indexedProfileFile);
try (FileInputStreamSource source =
new FileInputStreamSource(indexedProfileFile, true)) {
@@ -270,7 +220,6 @@ public final class ClangCodeCoverageCollector extends BaseDeviceMetricCollector
for (File untarDir : untarDirs.values()) {
FileUtil.recursiveDelete(untarDir);
}
- FileUtil.deleteFile(fileList);
FileUtil.recursiveDelete(mLlvmProfileTool);
FileUtil.deleteFile(indexedProfileFile);
}
diff --git a/src/com/android/tradefed/util/ClangProfileIndexer.java b/src/com/android/tradefed/util/ClangProfileIndexer.java
new file mode 100644
index 000000000..29e537be7
--- /dev/null
+++ b/src/com/android/tradefed/util/ClangProfileIndexer.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 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.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/** A utility class that indexes Clang code coverage measurements. */
+public final class ClangProfileIndexer {
+ // Maximum number of profile files before writing the list to a file. Beyond this value,
+ // llvm-profdata will use the -f option to read the list from a file to prevent exceeding
+ // the command line length limit.
+ private static final int MAX_PROFILE_FILES = 100;
+
+ private File mProfileTool;
+ private IRunUtil mRunUtil;
+
+ public ClangProfileIndexer(File profileTool) {
+ this(profileTool, RunUtil.getDefault());
+ }
+
+ public ClangProfileIndexer(File profileTool, IRunUtil runUtil) {
+ mProfileTool = profileTool;
+ mRunUtil = runUtil;
+ }
+
+ /**
+ * Indexes raw LLVM profile files and writes the coverage data to the output file.
+ *
+ * @param rawProfileFiles list of .profraw files to index
+ * @param outputFile file to write the results to
+ * @throws IOException on tool failure
+ */
+ public void index(Collection<String> rawProfileFiles, File outputFile) throws IOException {
+ Path profileBin = mProfileTool.toPath().resolve("bin/llvm-profdata");
+ profileBin.toFile().setExecutable(true);
+
+ List<String> command = new ArrayList<>();
+ command.add(profileBin.toString());
+ command.add("merge");
+ command.add("-sparse");
+
+ File fileList = null;
+ try {
+ if (rawProfileFiles.size() > MAX_PROFILE_FILES) {
+ // Write the measurement file list to a temporary file. This allows large numbers of
+ // measurements to not exceed the command line length limit.
+ fileList = FileUtil.createTempFile("clang_measurements", ".txt");
+ Files.write(fileList.toPath(), rawProfileFiles, Charset.defaultCharset());
+
+ // Add the file containing the list of .profraw files.
+ command.add("-f");
+ command.add(fileList.getAbsolutePath());
+ } else {
+ command.addAll(rawProfileFiles);
+ }
+
+ command.add("-o");
+ command.add(outputFile.getAbsolutePath());
+
+ CommandResult result = mRunUtil.runTimedCmd(0, command.toArray(new String[0]));
+ if (result.getStatus() != CommandStatus.SUCCESS) {
+ // Retry with -failure-mode=all to still be able to report some coverage.
+ command.add("-failure-mode=all");
+ result = mRunUtil.runTimedCmd(0, command.toArray(new String[0]));
+
+ if (result.getStatus() != CommandStatus.SUCCESS) {
+ throw new IOException(
+ "Failed to merge Clang profile data:\n" + result.toString());
+ }
+ }
+ } finally {
+ FileUtil.deleteFile(fileList);
+ }
+ }
+}
diff --git a/test_framework/com/android/tradefed/testtype/HostGTest.java b/test_framework/com/android/tradefed/testtype/HostGTest.java
index 358938c14..caee08b40 100644
--- a/test_framework/com/android/tradefed/testtype/HostGTest.java
+++ b/test_framework/com/android/tradefed/testtype/HostGTest.java
@@ -16,6 +16,8 @@
package com.android.tradefed.testtype;
+import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.CLANG;
+
import com.android.ddmlib.IShellOutputReceiver;
import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
import com.android.tradefed.build.DeviceBuildInfo;
@@ -35,6 +37,7 @@ import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.error.TestErrorIdentifier;
import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
+import com.android.tradefed.util.ClangProfileIndexer;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;
@@ -145,6 +148,18 @@ public class HostGTest extends GTestBase implements IBuildReceiver {
runUtil.setEnvVariable("LD_LIBRARY_PATH", ldLibraryPath);
}
+ // Set LLVM_PROFILE_FILE for coverage.
+ File coverageDir = null;
+ if (isClangCoverageEnabled()) {
+ try {
+ coverageDir = FileUtil.createTempDir("clang");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ runUtil.setEnvVariable(
+ "LLVM_PROFILE_FILE", coverageDir.getAbsolutePath() + "/clang-%m.profraw");
+ }
+
// If there's a shell output receiver to pass results along to, then
// ShellOutputReceiverStream will write that into the IShellOutputReceiver. If not, the
// command output will just be ignored.
@@ -186,6 +201,27 @@ public class HostGTest extends GTestBase implements IBuildReceiver {
}
}
FileUtil.deleteFile(stdout);
+
+ if (isClangCoverageEnabled()) {
+ File profdata = null;
+ try {
+ Set<String> profraws = FileUtil.findFiles(coverageDir, ".*\\.profraw");
+ ClangProfileIndexer indexer =
+ new ClangProfileIndexer(
+ getConfiguration().getCoverageOptions().getLlvmProfdataPath());
+ profdata = FileUtil.createTempFile(gtestFile.getName(), ".profdata");
+ indexer.index(profraws, profdata);
+
+ try (FileInputStreamSource source = new FileInputStreamSource(profdata, true)) {
+ logger.testLog(gtestFile.getName(), LogDataType.CLANG_COVERAGE, source);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ FileUtil.deleteFile(profdata);
+ FileUtil.recursiveDelete(coverageDir);
+ }
+ }
}
return result;
}
@@ -421,4 +457,10 @@ public class HostGTest extends GTestBase implements IBuildReceiver {
}
return new LinkedHashSet(seen.values());
}
+
+ /** Returns whether Clang code coverage is enabled. */
+ private boolean isClangCoverageEnabled() {
+ return getConfiguration().getCoverageOptions().isCoverageEnabled()
+ && getConfiguration().getCoverageOptions().getCoverageToolchains().contains(CLANG);
+ }
}