blob: b6305ee83174624f562431e0e046c9c0517c08d5 [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.testtype;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.build.IBuildProvider;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.metrics.proto.MetricMeasurement;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.testtype.coverage.CoverageOptions;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.proto.TfMetricProtoUtil;
import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.ByteString;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
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.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/** Unit tests for {@link ClangCodeCoverageListener}. */
@RunWith(JUnit4.class)
public class ClangCodeCoverageListenerTest {
private static final String RUN_NAME = "SomeTest";
private static final int TEST_COUNT = 5;
private static final long ELAPSED_TIME = 1000;
@Rule public TemporaryFolder folder = new TemporaryFolder();
private HashMap<String, MetricMeasurement.Metric> mMetrics;
/** Fakes, Mocks and Spies. */
@Mock IBuildInfo mMockBuildInfo;
@Mock IConfiguration mMockConfiguration;
@Mock IBuildProvider mMockBuildProvider;
@Mock ITestDevice mMockDevice;
@Spy CommandArgumentCaptor mCommandArgumentCaptor;
LogFileReader mFakeListener = new LogFileReader();
/** Options for coverage. */
CoverageOptions mCoverageOptions;
OptionSetter mCoverageOptionsSetter = null;
/** Object under test. */
ClangCodeCoverageListener mListener;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
Map<String, String> metric = new HashMap<>();
mMetrics = TfMetricProtoUtil.upgradeConvert(metric);
mCoverageOptions = new CoverageOptions();
mCoverageOptionsSetter = new OptionSetter(mCoverageOptions);
doReturn(mCoverageOptions).when(mMockConfiguration).getCoverageOptions();
doReturn(mMockBuildProvider).when(mMockConfiguration).getBuildProvider();
doReturn(mMockBuildInfo).when(mMockBuildProvider).getBuild();
mListener = new ClangCodeCoverageListener(mMockDevice, mFakeListener);
mListener.setConfiguration(mMockConfiguration);
mListener.setRunUtil(mCommandArgumentCaptor);
}
@Test
public void coverageDisabled_noCoverageLog() {
// Simulate a test run.
mListener.testRunStarted(RUN_NAME, TEST_COUNT);
mListener.testRunEnded(ELAPSED_TIME, mMetrics);
mListener.invocationEnded(ELAPSED_TIME);
// Verify testLog(...) was never called.
assertThat(mFakeListener.getLogs()).isEmpty();
}
@Test
public void clangCoverageDisabled_noCoverageLog() throws Exception {
mCoverageOptionsSetter.setOptionValue("coverage", "true");
// Simulate a test run.
mListener.testRunStarted(RUN_NAME, TEST_COUNT);
mListener.testRunEnded(ELAPSED_TIME, mMetrics);
mListener.invocationEnded(ELAPSED_TIME);
// Verify testLog(...) was never called.
assertThat(mFakeListener.getLogs()).isEmpty();
}
@Test
public void coverageFlushEnabled_flushCalled() throws Exception {
mCoverageOptionsSetter.setOptionValue("coverage", "true");
mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
mCoverageOptionsSetter.setOptionValue("coverage-flush", "true");
// Setup mocks.
doReturn(true).when(mMockDevice).enableAdbRoot();
doReturn(true).when(mMockDevice).isAdbRoot();
doReturn(createTar(ImmutableMap.of())).when(mMockDevice).pullFile(anyString());
// Simulate a test run.
mListener.testRunStarted(RUN_NAME, TEST_COUNT);
mListener.testRunEnded(ELAPSED_TIME, mMetrics);
mListener.invocationEnded(ELAPSED_TIME);
// Verify the flush-all-coverage command was called.
verify(mMockDevice).executeShellCommand("kill -37 -1");
}
@Test
public void testRun_logsCoverageFile() throws Exception {
mCoverageOptionsSetter.setOptionValue("coverage", "true");
mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
// Setup mocks.
doReturn(true).when(mMockDevice).enableAdbRoot();
File tarGz =
createTar(
ImmutableMap.of(
"path/to/coverage.profraw",
ByteString.copyFromUtf8("coverage.profraw"),
"path/to/.hidden/coverage2.profraw",
ByteString.copyFromUtf8("coverage2.profraw")));
doReturn(tarGz).when(mMockDevice).pullFile(anyString());
doReturn(createProfileToolZip()).when(mMockBuildInfo).getFile(anyString());
// Simulate a test run.
mListener.testRunStarted(RUN_NAME, TEST_COUNT);
mListener.testRunEnded(ELAPSED_TIME, mMetrics);
mListener.invocationEnded(ELAPSED_TIME);
// Verify that the command line contains the files above.
List<String> command = mCommandArgumentCaptor.getCommand();
checkListContainsSuffixes(
command,
ImmutableList.of(
"llvm-profdata",
"path/to/coverage.profraw",
"path/to/.hidden/coverage2.profraw"));
// Verify testLog(..) was called with a single indexed profile data.
List<ByteString> logs = mFakeListener.getLogs();
assertThat(logs).hasSize(1);
}
@Test
public void testOtherFileTypes_ignored() throws Exception {
mCoverageOptionsSetter.setOptionValue("coverage", "true");
mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
// Setup mocks.
doReturn(true).when(mMockDevice).enableAdbRoot();
File tarGz =
createTar(
ImmutableMap.of(
"path/to/coverage.profraw",
ByteString.copyFromUtf8("coverage.profraw"),
"path/to/coverage.gcda",
ByteString.copyFromUtf8("coverage.gcda")));
doReturn(tarGz).when(mMockDevice).pullFile(anyString());
doReturn(createProfileToolZip()).when(mMockBuildInfo).getFile(anyString());
// Simulate a test run.
mListener.testRunStarted(RUN_NAME, TEST_COUNT);
mListener.testRunEnded(ELAPSED_TIME, mMetrics);
mListener.invocationEnded(ELAPSED_TIME);
// Verify that the command line contains the files above, not including the .gcda file.
List<String> command = mCommandArgumentCaptor.getCommand();
checkListContainsSuffixes(
command, ImmutableList.of("llvm-profdata", "path/to/coverage.profraw"));
checkListDoesNotContainSuffix(command, "path/to/coverage.gcda");
// Verify testLog(..) was called with a single indexed profile data.
List<ByteString> logs = mFakeListener.getLogs();
assertThat(logs).hasSize(1);
}
@Test
public void testNoClangMeasurements_noLogFile() throws Exception {
mCoverageOptionsSetter.setOptionValue("coverage", "true");
mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
// Setup mocks.
doReturn(true).when(mMockDevice).enableAdbRoot();
doReturn(createTar(ImmutableMap.of())).when(mMockDevice).pullFile(anyString());
// Simulate a test run.
mListener.testRunStarted(RUN_NAME, TEST_COUNT);
mListener.testRunEnded(ELAPSED_TIME, mMetrics);
mListener.invocationEnded(ELAPSED_TIME);
// Verify testLog(..) was never called.
assertThat(mFakeListener.getLogs()).isEmpty();
}
@Test
public void testProfileToolInConfiguration_notFromBuild() throws Exception {
mCoverageOptionsSetter.setOptionValue("coverage", "true");
mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
mCoverageOptionsSetter.setOptionValue("llvm-profdata-path", "/path/to/some/directory");
// Setup mocks.
doReturn(true).when(mMockDevice).enableAdbRoot();
File tarGz =
createTar(
ImmutableMap.of(
"path/to/coverage.profraw",
ByteString.copyFromUtf8("coverage.profraw"),
"path/to/.hidden/coverage2.profraw",
ByteString.copyFromUtf8("coverage2.profraw")));
doReturn(tarGz).when(mMockDevice).pullFile(anyString());
// Simulate a test run.
mListener.testRunStarted(RUN_NAME, TEST_COUNT);
mListener.testRunEnded(ELAPSED_TIME, mMetrics);
mListener.invocationEnded(ELAPSED_TIME);
// Verify that the command line contains the llvm-profile-path set above.
List<String> command = mCommandArgumentCaptor.getCommand();
assertThat(command.get(0)).isEqualTo("/path/to/some/directory/bin/llvm-profdata");
}
@Test
public void testProfileToolNotFound_throwsException() throws Exception {
mCoverageOptionsSetter.setOptionValue("coverage", "true");
mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
// Setup mocks.
doReturn(true).when(mMockDevice).enableAdbRoot();
File tarGz =
createTar(
ImmutableMap.of(
"path/to/coverage.profraw",
ByteString.copyFromUtf8("coverage.profraw"),
"path/to/.hidden/coverage2.profraw",
ByteString.copyFromUtf8("coverage2.profraw")));
doReturn(tarGz).when(mMockDevice).pullFile(anyString());
// Simulate a test run.
try {
mListener.testRunStarted(RUN_NAME, TEST_COUNT);
mListener.testRunEnded(ELAPSED_TIME, mMetrics);
mListener.invocationEnded(ELAPSED_TIME);
fail("an exception should have been thrown");
} catch (VerifyException e) {
// Expected.
assertThat(e).hasMessageThat().contains("llvm-profdata");
}
// Verify testLog(..) was never called.
assertThat(mFakeListener.getLogs()).isEmpty();
}
@Test
public void testProfileToolFailed_throwsException() throws Exception {
mCoverageOptionsSetter.setOptionValue("coverage", "true");
mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
// Setup mocks.
doReturn(true).when(mMockDevice).enableAdbRoot();
File tarGz =
createTar(
ImmutableMap.of(
"path/to/coverage.profraw",
ByteString.copyFromUtf8("coverage.profraw"),
"path/to/.hidden/coverage2.profraw",
ByteString.copyFromUtf8("coverage2.profraw")));
doReturn(tarGz).when(mMockDevice).pullFile(anyString());
doReturn(createProfileToolZip()).when(mMockBuildInfo).getFile(anyString());
mCommandArgumentCaptor.setResult(CommandStatus.FAILED);
// Simulate a test run.
try {
mListener.testRunStarted(RUN_NAME, TEST_COUNT);
mListener.testRunEnded(ELAPSED_TIME, mMetrics);
fail("an exception should have been thrown");
} catch (RuntimeException e) {
// Expected.
assertThat(e).hasMessageThat().contains("merge profile data");
}
mListener.invocationEnded(ELAPSED_TIME);
// Verify testLog(..) was never called.
assertThat(mFakeListener.getLogs()).isEmpty();
}
abstract static class CommandArgumentCaptor implements IRunUtil {
private List<String> mCommand;
private CommandResult mResult = new CommandResult(CommandStatus.SUCCESS);
/** Stores the command for retrieval later. */
@Override
public CommandResult runTimedCmd(long timeout, String... cmd) {
mCommand = Arrays.asList(cmd);
return mResult;
}
void setResult(CommandStatus status) {
mResult = new CommandResult(status);
}
List<String> getCommand() {
return mCommand;
}
}
/** An {@link ITestInvocationListener} which reads test log data streams for verification. */
private static class LogFileReader implements ITestInvocationListener {
private List<ByteString> mLogs = new ArrayList<>();
/** Reads the contents of the {@code dataStream} and saves it in the logs. */
@Override
public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) {
try (InputStream input = dataStream.createInputStream()) {
mLogs.add(ByteString.readFrom(input));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
List<ByteString> getLogs() {
return new ArrayList<>(mLogs);
}
}
/** Utility method to create .tar files. */
private File createTar(Map<String, ByteString> fileContents) throws IOException {
File tarFile = folder.newFile();
try (TarArchiveOutputStream out =
new TarArchiveOutputStream(new FileOutputStream(tarFile))) {
for (Map.Entry<String, ByteString> file : fileContents.entrySet()) {
TarArchiveEntry entry = new TarArchiveEntry(file.getKey());
entry.setSize(file.getValue().size());
out.putArchiveEntry(entry);
file.getValue().writeTo(out);
out.closeArchiveEntry();
}
}
return tarFile;
}
private File createProfileToolZip() throws IOException {
File profileToolZip = folder.newFile("llvm-profdata.zip");
try (FileOutputStream stream = new FileOutputStream(profileToolZip);
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(stream))) {
// Add bin/llvm-profdata.
ZipEntry entry = new ZipEntry("bin/llvm-profdata");
out.putNextEntry(entry);
out.closeEntry();
}
return profileToolZip;
}
/** 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 + "'");
}
}
}
}