blob: 8f6c76f2e014c9a6fc12cb7abbecf2a2de7516c1 [file] [log] [blame]
/*
* Copyright (C) 2017 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.python;
import com.android.annotations.VisibleForTesting;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.StubDevice;
import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.ResultForwarder;
import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.ITestFilterReceiver;
import com.android.tradefed.testtype.PythonUnitTestResultParser;
import com.android.tradefed.testtype.TestTimeoutEnforcer;
import com.android.tradefed.util.AdbUtils;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.IRunUtil.EnvPriority;
import com.android.tradefed.util.RunUtil;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Host test meant to run a python binary file from the Android Build system (Soong)
*
* <p>The test runner supports include-filter and exclude-filter. Note that exclude-filter works by
* ignoring the test result, instead of skipping the actual test. The tests specified in the
* exclude-filter will still be executed.
*/
@OptionClass(alias = "python-host")
public class PythonBinaryHostTest implements IRemoteTest, ITestFilterReceiver {
protected static final String ANDROID_SERIAL_VAR = "ANDROID_SERIAL";
protected static final String LD_LIBRARY_PATH = "LD_LIBRARY_PATH";
@VisibleForTesting static final String USE_TEST_OUTPUT_FILE_OPTION = "use-test-output-file";
static final String TEST_OUTPUT_FILE_FLAG = "test-output-file";
private static final String PYTHON_LOG_STDOUT_FORMAT = "%s-stdout";
private static final String PYTHON_LOG_STDERR_FORMAT = "%s-stderr";
private static final String PYTHON_LOG_TEST_OUTPUT_FORMAT = "%s-test-output";
private Set<String> mIncludeFilters = new LinkedHashSet<>();
private Set<String> mExcludeFilters = new LinkedHashSet<>();
private String mLdLibraryPath = null;
@Option(name = "par-file-name", description = "The binary names inside the build info to run.")
private Set<String> mBinaryNames = new HashSet<>();
@Option(
name = "python-binaries",
description = "The full path to a runnable python binary. Can be repeated."
)
private Set<File> mBinaries = new HashSet<>();
@Option(
name = "test-timeout",
description = "Timeout for a single par file to terminate.",
isTimeVal = true
)
private long mTestTimeout = 20 * 1000L;
@Option(
name = "inject-serial-option",
description = "Whether or not to pass a -s <serialnumber> option to the binary")
private boolean mInjectSerial = false;
@Option(
name = "inject-android-serial",
description = "Whether or not to pass a ANDROID_SERIAL variable to the process.")
private boolean mInjectAndroidSerialVar = true;
@Option(
name = "python-options",
description = "Option string to be passed to the binary when running"
)
private List<String> mTestOptions = new ArrayList<>();
@Option(
name = "inject-build-key",
description =
"Link a file from the build by its key to the python subprocess via"
+ " environment. This breaks test dependencies so shouldn't be used in"
+ " standard suites.")
private Set<String> mBuildKeyToLink = new LinkedHashSet<String>();
@Option(
name = USE_TEST_OUTPUT_FILE_OPTION,
description =
"Whether the test should write results to the file specified via the --"
+ TEST_OUTPUT_FILE_FLAG
+ " flag instead of stderr which could contain spurious messages that "
+ "break result parsing. Using this option requires that the Python "
+ "test have the necessary logic to accept the flag and write results "
+ "in the expected format.")
private boolean mUseTestOutputFile = false;
@Option(
name = TestTimeoutEnforcer.TEST_CASE_TIMEOUT_OPTION,
description = TestTimeoutEnforcer.TEST_CASE_TIMEOUT_DESCRIPTION)
private Duration mTestCaseTimeout = Duration.ofSeconds(0L);
private TestInformation mTestInfo;
private IRunUtil mRunUtil;
/** {@inheritDoc} */
@Override
public void addIncludeFilter(String filter) {
mIncludeFilters.add(filter);
}
/** {@inheritDoc} */
@Override
public void addExcludeFilter(String filter) {
mExcludeFilters.add(filter);
}
/** {@inheritDoc} */
@Override
public void addAllIncludeFilters(Set<String> filters) {
mIncludeFilters.addAll(filters);
}
/** {@inheritDoc} */
@Override
public void addAllExcludeFilters(Set<String> filters) {
mExcludeFilters.addAll(filters);
}
/** {@inheritDoc} */
@Override
public void clearIncludeFilters() {
mIncludeFilters.clear();
}
/** {@inheritDoc} */
@Override
public void clearExcludeFilters() {
mExcludeFilters.clear();
}
/** {@inheritDoc} */
@Override
public Set<String> getIncludeFilters() {
return mIncludeFilters;
}
/** {@inheritDoc} */
@Override
public Set<String> getExcludeFilters() {
return mExcludeFilters;
}
@Override
public final void run(TestInformation testInfo, ITestInvocationListener listener)
throws DeviceNotAvailableException {
mTestInfo = testInfo;
File testDir = mTestInfo.executionFiles().get(FilesKey.HOST_TESTS_DIRECTORY);
if (testDir == null || !testDir.exists()) {
testDir = mTestInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY);
}
List<String> ldLibraryPath = new ArrayList<>();
if (testDir != null && testDir.exists()) {
List<String> libPaths =
Arrays.asList("lib", "lib64", "host/testcases/lib", "host/testcases/lib64");
for (String path : libPaths) {
File libDir = new File(testDir, path);
if (libDir.exists()) {
ldLibraryPath.add(libDir.getAbsolutePath());
}
}
if (!ldLibraryPath.isEmpty()) {
mLdLibraryPath = Joiner.on(":").join(ldLibraryPath);
}
}
List<File> pythonFilesList = findParFiles();
for (File pyFile : pythonFilesList) {
if (!pyFile.exists()) {
CLog.d(
"ignoring %s which doesn't look like a test file.",
pyFile.getAbsolutePath());
continue;
}
// Complete the LD_LIBRARY_PATH with possible libs
String path = mLdLibraryPath;
List<String> paths = findAllSubdir(pyFile.getParentFile(), ldLibraryPath);
if (mLdLibraryPath != null) {
paths.add(0, mLdLibraryPath);
}
mLdLibraryPath = Joiner.on(":").join(paths);
pyFile.setExecutable(true);
runSinglePythonFile(listener, testInfo, pyFile);
mLdLibraryPath = path;
}
}
private List<File> findParFiles() {
List<File> files = new ArrayList<>();
for (String parFileName : mBinaryNames) {
File res = null;
// search tests dir
try {
res = mTestInfo.getDependencyFile(parFileName, /* targetFirst */ false);
files.add(res);
} catch (FileNotFoundException e) {
throw new RuntimeException(
String.format("Couldn't find a par file %s", parFileName));
}
}
files.addAll(mBinaries);
return files;
}
private void runSinglePythonFile(
ITestInvocationListener listener, TestInformation testInfo, File pyFile) {
List<String> commandLine = new ArrayList<>();
commandLine.add(pyFile.getAbsolutePath());
// If we have a physical device, pass it to the python test by serial
if (!(mTestInfo.getDevice().getIDevice() instanceof StubDevice) && mInjectSerial) {
// TODO: support multi-device python tests?
commandLine.add("-s");
commandLine.add(mTestInfo.getDevice().getSerialNumber());
}
// Set the process working dir as the directory of the main binary
getRunUtil().setWorkingDir(pyFile.getParentFile());
// Set the parent dir on the PATH
String separator = System.getProperty("path.separator");
List<String> paths = new ArrayList<>();
// Bundle binaries / dependencies have priorities over existing PATH
paths.addAll(findAllSubdir(pyFile.getParentFile(), new ArrayList<>()));
paths.add(System.getenv("PATH"));
String path = paths.stream().distinct().collect(Collectors.joining(separator));
CLog.d("Using updated $PATH: %s", path);
getRunUtil().setEnvVariablePriority(EnvPriority.SET);
getRunUtil().setEnvVariable("PATH", path);
if (mLdLibraryPath != null) {
getRunUtil().setEnvVariable(LD_LIBRARY_PATH, mLdLibraryPath);
}
if (mInjectAndroidSerialVar) {
getRunUtil()
.setEnvVariable(ANDROID_SERIAL_VAR, mTestInfo.getDevice().getSerialNumber());
}
// This is not standard, but sometimes non-module data artifacts might be needed
for (String key : mBuildKeyToLink) {
if (mTestInfo.getBuildInfo().getFile(key) != null) {
getRunUtil()
.setEnvVariable(
key, mTestInfo.getBuildInfo().getFile(key).getAbsolutePath());
}
}
File tempTestOutputFile = null;
if (mUseTestOutputFile) {
try {
tempTestOutputFile = FileUtil.createTempFile("python-test-output", ".txt");
} catch (IOException e) {
throw new RuntimeException(e);
}
commandLine.add("--" + TEST_OUTPUT_FILE_FLAG);
commandLine.add(tempTestOutputFile.getAbsolutePath());
}
AdbUtils.updateAdb(testInfo, getRunUtil(), getAdbPath());
// Add all the other options
commandLine.addAll(mTestOptions);
// Prepare the parser if needed
String runName = pyFile.getName();
PythonForwarder forwarder = new PythonForwarder(listener, runName);
ITestInvocationListener receiver = forwarder;
if (mTestCaseTimeout.toMillis() > 0L) {
receiver =
new TestTimeoutEnforcer(
mTestCaseTimeout.toMillis(), TimeUnit.MILLISECONDS, receiver);
}
PythonUnitTestResultParser pythonParser =
new PythonUnitTestResultParser(
Arrays.asList(receiver), "python-run", mIncludeFilters, mExcludeFilters);
CommandResult result = null;
File stderrFile = null;
try {
stderrFile = FileUtil.createTempFile("python-res", ".txt");
if (mUseTestOutputFile) {
result = getRunUtil().runTimedCmd(mTestTimeout, commandLine.toArray(new String[0]));
} else {
try (FileOutputStream fileOutputParser = new FileOutputStream(stderrFile)) {
result =
getRunUtil()
.runTimedCmd(
mTestTimeout,
null,
fileOutputParser,
commandLine.toArray(new String[0]));
fileOutputParser.flush();
}
}
if (!Strings.isNullOrEmpty(result.getStdout())) {
CLog.i("\nstdout:\n%s", result.getStdout());
try (InputStreamSource data =
new ByteArrayInputStreamSource(result.getStdout().getBytes())) {
listener.testLog(
String.format(PYTHON_LOG_STDOUT_FORMAT, runName),
LogDataType.TEXT,
data);
}
}
if (!Strings.isNullOrEmpty(result.getStderr())) {
CLog.i("\nstderr:\n%s", result.getStderr());
}
File testOutputFile = stderrFile;
if (mUseTestOutputFile) {
testOutputFile = tempTestOutputFile;
testLogFile(
listener,
String.format(PYTHON_LOG_TEST_OUTPUT_FORMAT, runName),
testOutputFile);
}
String testOutput = FileUtil.readStringFromFile(testOutputFile);
pythonParser.processNewLines(testOutput.split("\n"));
} catch (RuntimeException e) {
StringBuilder message = new StringBuilder();
String stderr = "";
try {
stderr = FileUtil.readStringFromFile(stderrFile);
} catch (IOException ioe) {
CLog.e(ioe);
}
message.append(
String.format(
"Failed to parse the python logs: %s. Please ensure that verbosity of "
+ "output is high enough to be parsed."
+ " Stderr: %s",
e.getMessage(), stderr));
if (mUseTestOutputFile) {
message.append(
String.format(
" Make sure that your test writes its output to the file specified "
+ "by the --%s flag and that its contents (%s) are in the "
+ "format expected by the test runner.",
TEST_OUTPUT_FILE_FLAG,
String.format(PYTHON_LOG_TEST_OUTPUT_FORMAT, runName)));
}
reportFailure(listener, runName, message.toString());
CLog.e(e);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (stderrFile != null) {
// Note that we still log stderr when parsing results from a test-written output
// file since it most likely contains useful debugging information.
try {
if (mUseTestOutputFile) {
FileUtil.writeToFile(result.getStderr(), stderrFile);
}
testLogFile(
listener, String.format(PYTHON_LOG_STDERR_FORMAT, runName), stderrFile);
} catch (IOException e) {
CLog.e(e);
}
}
FileUtil.deleteFile(stderrFile);
FileUtil.deleteFile(tempTestOutputFile);
}
}
@VisibleForTesting
IRunUtil getRunUtil() {
if (mRunUtil == null) {
mRunUtil = new RunUtil();
}
return mRunUtil;
}
@VisibleForTesting
String getAdbPath() {
return GlobalConfiguration.getDeviceManagerInstance().getAdbPath();
}
private List<String> findAllSubdir(File parentDir, List<String> knownPaths) {
List<String> subDir = new ArrayList<>();
subDir.add(parentDir.getAbsolutePath());
if (parentDir.listFiles() == null) {
return subDir;
}
for (File child : parentDir.listFiles()) {
if (child != null
&& child.isDirectory()
&& !knownPaths.contains(child.getAbsolutePath())) {
subDir.addAll(findAllSubdir(child, knownPaths));
}
}
return subDir;
}
private void reportFailure(
ITestInvocationListener listener, String runName, String errorMessage) {
listener.testRunStarted(runName, 0);
FailureDescription description =
FailureDescription.create(errorMessage, FailureStatus.TEST_FAILURE);
listener.testRunFailed(description);
listener.testRunEnded(0L, new HashMap<String, Metric>());
}
private static void testLogFile(ITestInvocationListener listener, String dataName, File f) {
try (FileInputStreamSource data = new FileInputStreamSource(f)) {
listener.testLog(dataName, LogDataType.TEXT, data);
}
}
/** Result forwarder to replace the run name by the binary name. */
public static class PythonForwarder extends ResultForwarder {
private String mRunName;
/** Ctor with the run name using the binary name. */
public PythonForwarder(ITestInvocationListener listener, String name) {
super(listener);
mRunName = name;
}
@Override
public void testRunStarted(String runName, int testCount) {
// Replace run name
testRunStarted(runName, testCount, 0);
}
@Override
public void testRunStarted(String runName, int testCount, int attempt) {
// Replace run name
testRunStarted(runName, testCount, attempt, System.currentTimeMillis());
}
@Override
public void testRunStarted(String runName, int testCount, int attempt, long startTime) {
// Replace run name
super.testRunStarted(mRunName, testCount, attempt, startTime);
}
}
}