blob: caee08b408c2d0153d9cda7b5c036bb9634df2dd [file] [log] [blame]
/*
* Copyright (C) 2018 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.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;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.error.HarnessRuntimeException;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.invoker.TestInvocation;
import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.FileInputStreamSource;
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;
import com.android.tradefed.util.IRunUtil.EnvPriority;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.ShellOutputReceiverStream;
import com.android.tradefed.util.TestRunnerUtil;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/** A Test that runs a native test package. */
@OptionClass(alias = "hostgtest")
public class HostGTest extends GTestBase implements IBuildReceiver {
private static final long DEFAULT_HOST_COMMAND_TIMEOUT_MS = 2 * 60 * 1000;
private IBuildInfo mBuildInfo = null;
@Option(
name = "use-updated-shard-retry",
description = "Whether to use the updated logic for retry with sharding.")
private boolean mUseUpdatedShardRetry = true;
/** Whether any incomplete test is found in the current run. */
private boolean mIncompleteTestFound = false;
/** List of tests that failed in the current test run when test run was complete. */
private Set<String> mCurFailedTests = new LinkedHashSet<>();
@Override
public void setBuild(IBuildInfo buildInfo) {
this.mBuildInfo = buildInfo;
}
/**
* @param cmd command that want to execute in host
* @return the {@link CommandResult} of command
*/
public CommandResult executeHostCommand(String cmd) {
return executeHostCommand(cmd, DEFAULT_HOST_COMMAND_TIMEOUT_MS);
}
/**
* @param cmd command that want to execute in host
* @param timeoutMs timeout for command in milliseconds
* @return the {@link CommandResult} of command
*/
public CommandResult executeHostCommand(String cmd, long timeoutMs) {
String[] cmds = cmd.split("\\s+");
return RunUtil.getDefault().runTimedCmd(timeoutMs, cmds);
}
/**
* @param gtestFile file pointing to the binary to be executed
* @param cmd command that want to execute in host
* @param timeoutMs timeout for command in milliseconds
* @param receiver the result parser
* @return the {@link CommandResult} of command
*/
private CommandResult executeHostGTestCommand(
File gtestFile,
String cmd,
long timeoutMs,
IShellOutputReceiver receiver,
ITestLogger logger) {
RunUtil runUtil = new RunUtil();
String[] cmds = cmd.split("\\s+");
if (getShardCount() > 0) {
if (isCollectTestsOnly()) {
CLog.w(
"--collect-tests-only option ignores sharding parameters, and will cause "
+ "each shard to collect all tests.");
}
runUtil.setEnvVariable("GTEST_SHARD_INDEX", Integer.toString(getShardIndex()));
runUtil.setEnvVariable("GTEST_TOTAL_SHARDS", Integer.toString(getShardCount()));
}
// Set the RunUtil to combine stderr with stdout so that they are interleaved correctly.
runUtil.setRedirectStderrToStdout(true);
// Set the working dir to the folder containing the binary to execute from the same path.
runUtil.setWorkingDir(gtestFile.getParentFile());
String separator = System.getProperty("path.separator");
List<String> paths = new ArrayList<>();
paths.add(System.getenv("PATH"));
paths.add(gtestFile.getParentFile().getAbsolutePath());
String path = paths.stream().distinct().collect(Collectors.joining(separator));
CLog.d("Using updated $PATH: %s", path);
runUtil.setEnvVariablePriority(EnvPriority.SET);
runUtil.setEnvVariable("PATH", path);
// Update LD_LIBRARY_PATH
String ldLibraryPath = TestRunnerUtil.getLdLibraryPath(gtestFile);
if (ldLibraryPath != null) {
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.
CommandResult result = null;
File stdout = null;
try {
stdout =
FileUtil.createTempFile(
String.format("%s-output", gtestFile.getName()), ".txt");
try (ShellOutputReceiverStream stream =
new ShellOutputReceiverStream(receiver, new FileOutputStream(stdout))) {
result = runUtil.runTimedCmd(timeoutMs, stream, null, cmds);
} catch (IOException e) {
throw new RuntimeException(
"Should never happen, ShellOutputReceiverStream.close is a no-op", e);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// Flush before the log to ensure order of events
receiver.flush();
try {
// Add a small extra log to the output for verification sake.
FileUtil.writeToFile(
String.format(
"\nBinary '%s' still exists: %s", gtestFile, gtestFile.exists()),
stdout,
true);
} catch (IOException e) {
// Ignore
}
if (stdout != null && stdout.length() > 0L) {
try (FileInputStreamSource source = new FileInputStreamSource(stdout)) {
logger.testLog(
String.format("%s-output", gtestFile.getName()),
LogDataType.TEXT,
source);
}
}
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;
}
/** {@inheritDoc} */
@Override
public String loadFilter(String binaryOnHost) {
try {
CLog.i("Loading filter from file for key: '%s'", getTestFilterKey());
String filterFileName = String.format("%s%s", binaryOnHost, FILTER_EXTENSION);
File filterFile = new File(filterFileName);
if (filterFile.exists()) {
CommandResult cmdResult =
executeHostCommand(String.format("cat %s", filterFileName));
String content = cmdResult.getStdout();
if (content != null && !content.isEmpty()) {
JSONObject filter = new JSONObject(content);
String key = getTestFilterKey();
JSONObject filterObject = filter.getJSONObject(key);
return filterObject.getString("filter");
}
CLog.e("Error with content of the filter file %s: %s", filterFile, content);
} else {
CLog.e("Filter file %s not found", filterFile);
}
} catch (JSONException e) {
CLog.e(e);
}
return null;
}
/**
* Run the given gtest binary
*
* @param resultParser the test run output parser
* @param gtestFile file pointing to gtest binary
* @param flags gtest execution flags
*/
private void runTest(
final IShellOutputReceiver resultParser,
final File gtestFile,
final String flags,
ITestLogger logger) {
for (String cmd : getBeforeTestCmd()) {
CommandResult result = executeHostCommand(cmd);
if (!result.getStatus().equals(CommandStatus.SUCCESS)) {
throw new RuntimeException("'Before test' command failed: " + result.getStderr());
}
}
long maxTestTimeMs = getMaxTestTimeMs();
String cmd = getGTestCmdLine(gtestFile.getAbsolutePath(), flags);
CommandResult testResult =
executeHostGTestCommand(gtestFile, cmd, maxTestTimeMs, resultParser, logger);
// TODO: Switch throwing exceptions to use ITestInvocation.testRunFailed
switch (testResult.getStatus()) {
case TIMED_OUT:
throw new HarnessRuntimeException(
String.format("Command run timed out after %d ms", maxTestTimeMs),
TestErrorIdentifier.TEST_BINARY_TIMED_OUT);
case EXCEPTION:
throw new RuntimeException("Command run failed with exception");
case FAILED:
// Check the command exit code. If it's 1, then this is just a red herring;
// gtest returns 1 when a test fails.
final Integer exitCode = testResult.getExitCode();
if (exitCode == null || exitCode != 1) {
// No need to handle it as the parser would have reported it already.
CLog.e("Command run failed with exit code %s", exitCode);
}
break;
default:
break;
}
// Execute the host command if nothing failed badly before.
for (String afterCmd : getAfterTestCmd()) {
CommandResult result = executeHostCommand(afterCmd);
if (!result.getStatus().equals(CommandStatus.SUCCESS)) {
throw new RuntimeException("'After test' command failed: " + result.getStderr());
}
}
}
/** {@inheritDoc} */
@Override
public void run(TestInformation testInfo, ITestInvocationListener listener)
throws DeviceNotAvailableException { // DNAE is part of IRemoteTest.
try {
// Reset flags that are used to track results of current test run.
mIncompleteTestFound = false;
mCurFailedTests = new LinkedHashSet<>();
// Get testcases directory using the key HOST_LINKED_DIR first.
// If the directory is null, then get testcase directory from getTestDir() since *TS
// will invoke setTestDir().
List<File> scanDirs = new ArrayList<>();
File hostLinkedDir = mBuildInfo.getFile(BuildInfoFileKey.HOST_LINKED_DIR);
if (hostLinkedDir != null) {
scanDirs.add(hostLinkedDir);
}
File testsDir = ((DeviceBuildInfo) mBuildInfo).getTestsDir();
if (testsDir != null) {
scanDirs.add(testsDir);
}
String moduleName = getTestModule();
Set<File> gTestFiles;
try {
gTestFiles =
FileUtil.findFiles(
moduleName, getAbi(), false, scanDirs.toArray(new File[] {}));
gTestFiles = applyFileExclusionFilters(gTestFiles);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (gTestFiles == null || gTestFiles.isEmpty()) {
// If we ended up here we most likely failed to find the proper file as is, so we
// search for it with a potential suffix (which is allowed).
try {
gTestFiles =
FileUtil.findFiles(
moduleName + ".*",
getAbi(),
false,
scanDirs.toArray(new File[] {}));
gTestFiles = applyFileExclusionFilters(gTestFiles);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (gTestFiles == null || gTestFiles.isEmpty()) {
throw new RuntimeException(
String.format(
"Fail to find native test %s in directory %s.",
moduleName, scanDirs));
}
// Since we searched files in multiple directories, it is possible that we may have the
// same file in different source directories. Exclude duplicates.
gTestFiles = excludeDuplicateFiles(gTestFiles);
for (File gTestFile : gTestFiles) {
if (!gTestFile.canExecute()) {
CLog.i("%s is not executable! Skipping.", gTestFile.getAbsolutePath());
continue;
}
listener = getGTestListener(listener);
// TODO: Need to support XML test output based on isEnableXmlOutput
IShellOutputReceiver resultParser =
createResultParser(gTestFile.getName(), listener);
String flags = getAllGTestFlags(gTestFile.getName());
CLog.i("Running gtest %s %s", gTestFile.getName(), flags);
try {
runTest(resultParser, gTestFile, flags, listener);
} finally {
if (resultParser instanceof GTestResultParser) {
if (((GTestResultParser) resultParser).isTestRunIncomplete()) {
mIncompleteTestFound = true;
} else {
// if test run is complete, collect the failed tests so that they can be
// retried
mCurFailedTests.addAll(
((GTestResultParser) resultParser).getFailedTests());
}
}
}
}
} catch (Throwable t) {
// if we encounter any errors, count it as test Incomplete so that retry attempts
// during sharding uses a full retry.
mIncompleteTestFound = true;
throw t;
} finally {
if (mUseUpdatedShardRetry) {
// notify of test execution will enable the new sharding retry behavior since Gtest
// will be aware of retries.
notifyTestExecution(mIncompleteTestFound, mCurFailedTests);
}
}
}
private void reportFailure(
ITestInvocationListener listener, String runName, RuntimeException exception) {
listener.testRunStarted(runName, 0);
listener.testRunFailed(createFailure(exception));
listener.testRunEnded(0L, new HashMap<String, Metric>());
}
private FailureDescription createFailure(Exception e) {
return TestInvocation.createFailureFromException(e, FailureStatus.TEST_FAILURE);
}
/**
* Apply exclusion filters and return the remaining files.
*
* @param filesToFilterFrom a set of files which need to be filtered.
* @return a set of files
*/
private Set<File> applyFileExclusionFilters(Set<File> filesToFilterFrom) {
Set<File> retFiles = new LinkedHashSet<>();
List<String> fileExclusionFilterRegex = getFileExclusionFilterRegex();
for (File file : filesToFilterFrom) {
boolean matchedRegex = false;
for (String regex : fileExclusionFilterRegex) {
if (file.getPath().matches(regex)) {
CLog.i(
"File %s matches exclusion file regex %s, skipping",
file.getPath(), regex);
matchedRegex = true;
break;
}
}
if (!matchedRegex) {
retFiles.add(file);
}
}
return retFiles;
}
/** exclude files with same names */
private Set<File> excludeDuplicateFiles(Set<File> files) {
Map<String, File> seen = new LinkedHashMap<>();
for (File file : files) {
if (seen.containsKey(file.getName())) {
CLog.i(
"File %s already exists in location %s. skipping %s.",
file.getName(),
seen.get(file.getName()).getAbsolutePath(),
file.getAbsolutePath());
} else {
seen.put(file.getName(), file);
}
}
return new LinkedHashSet(seen.values());
}
/** Returns whether Clang code coverage is enabled. */
private boolean isClangCoverageEnabled() {
return getConfiguration().getCoverageOptions().isCoverageEnabled()
&& getConfiguration().getCoverageOptions().getCoverageToolchains().contains(CLANG);
}
}