diff options
author | Oliver Nguyen <olivernguyen@google.com> | 2024-03-19 14:57:03 -0700 |
---|---|---|
committer | Oliver Nguyen <olivernguyen@google.com> | 2024-03-28 23:15:12 +0000 |
commit | a6e33b1d6b13b79e17710222fd8f4570a969546d (patch) | |
tree | ebc37d8898f0a3f2861e3b45fcb0b7fb780c02f2 | |
parent | 162abc28f00b6f2ca66990a362d934853dfb17bd (diff) | |
download | tradefederation-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
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); + } } |