blob: ab6c6bdd0b5371a266b8d888f65103bfd82b9773 [file] [log] [blame]
/*
* Copyright (C) 2016 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 com.android.ddmlib.MultiLineReceiver;
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.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.TimeUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Runs Python tests written with the unittest library.
*/
@OptionClass(alias = "python-unit")
public class PythonUnitTestRunner implements IRemoteTest, IBuildReceiver {
@Option(name = "pythonpath", description = "directories to add to the PYTHONPATH")
private List<File> mPathDirs = new ArrayList<>();
@Option(name = "pytest", description = "names of python modules containing the test cases")
private List<String> mTests = new ArrayList<>();
@Option(name = "python-unittest-options",
description = "option string to be passed to the unittest module")
private String mUnitTestOpts;
@Option(name = "min-python-version", description = "minimum required python version")
private String mMinPyVersion = "2.7.0";
@Option(name = "python-binary", description = "python binary to use (optional)")
private String mPythonBin;
@Option(
name = "test-timeout",
description = "maximum amount of time tests are allowed to run",
isTimeVal = true
)
private long mTestTimeout = 1000 * 60 * 5;
private String mPythonPath;
private IBuildInfo mBuildInfo;
private IRunUtil mRunUtil;
private static final String PYTHONPATH = "PYTHONPATH";
private static final String VERSION_REGEX = "(?:(\\d+)\\.)?(?:(\\d+)\\.)?(\\*|\\d+)$";
/** Returns an {@link IRunUtil} that runs the unittest */
protected IRunUtil getRunUtil() {
if (mRunUtil == null) {
mRunUtil = new RunUtil();
}
return mRunUtil;
}
@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
setPythonPath();
if (mPythonBin == null) {
mPythonBin = getPythonBinary();
}
IRunUtil runUtil = getRunUtil();
runUtil.setEnvVariable(PYTHONPATH, mPythonPath);
for (String module : mTests) {
doRunTest(listener, runUtil, module);
}
}
@Override
public void setBuild(IBuildInfo buildInfo) {
mBuildInfo = buildInfo;
}
/** Returns the {@link IBuildInfo} for this invocation. */
protected IBuildInfo getBuild() {
return mBuildInfo;
}
String getMinPythonVersion() {
return mMinPyVersion;
}
void setMinPythonVersion(String version) {
mMinPyVersion = version;
}
private String getPythonBinary() {
IRunUtil runUtil = RunUtil.getDefault();
CommandResult c = runUtil.runTimedCmd(1000, "which", "python");
String pythonBin = c.getStdout().trim();
if (pythonBin.length() == 0) {
throw new RuntimeException("Could not find python binary on host machine");
}
c = runUtil.runTimedCmd(1000, pythonBin, "--version");
// python --version prints to stderr
CLog.i("Found python version: %s", c.getStderr());
checkPythonVersion(c);
return pythonBin;
}
private void setPythonPath() {
StringBuilder sb = new StringBuilder();
sb.append(System.getenv(PYTHONPATH));
for (File pathdir : mPathDirs) {
if (!pathdir.isDirectory()) {
CLog.w("Not adding file %s to PYTHONPATH: expecting directory",
pathdir.getAbsolutePath());
}
sb.append(":");
sb.append(pathdir.getAbsolutePath());
}
if (getBuild().getFile(PYTHONPATH) != null) {
sb.append(":");
sb.append(getBuild().getFile(PYTHONPATH).getAbsolutePath());
}
mPythonPath = sb.toString();
}
protected void checkPythonVersion(CommandResult c) {
Matcher minVersionParts = Pattern.compile(VERSION_REGEX).matcher(mMinPyVersion);
Matcher versionParts = Pattern.compile(VERSION_REGEX).matcher(c.getStderr());
if (minVersionParts.find() == false) {
throw new RuntimeException(
String.format("Could not parse the min version: '%s'", mMinPyVersion));
}
int major = Integer.parseInt(minVersionParts.group(1));
int minor = Integer.parseInt(minVersionParts.group(2));
int revision = Integer.parseInt(minVersionParts.group(3));
if (versionParts.find() == false) {
throw new RuntimeException(
String.format("Could not parse the current version: '%s'", c.getStderr()));
}
int foundMajor = Integer.parseInt(versionParts.group(1));
int foundMinor = Integer.parseInt(versionParts.group(2));
int foundRevision = Integer.parseInt(versionParts.group(3));
boolean check = false;
if (foundMajor > major) {
check = true;
} else if (foundMajor == major) {
if (foundMinor > minor) {
check = true;
} else if (foundMinor == minor) {
if (foundRevision >= revision) {
check = true;
}
}
}
if (check == false) {
throw new RuntimeException(
String.format(
"Current version '%s' does not meet min version: '%s'",
c.getStderr(), mMinPyVersion));
}
}
// Exposed for testing purpose.
void doRunTest(ITestInvocationListener listener, IRunUtil runUtil, String pyModule) {
String[] baseOpts = {mPythonBin, "-m", "unittest", "-v"};
String[] testModule = {pyModule};
String[] cmd;
if (mUnitTestOpts != null) {
cmd = ArrayUtil.buildArray(baseOpts, mUnitTestOpts.split(" "), testModule);
} else {
cmd = ArrayUtil.buildArray(baseOpts, testModule);
}
CommandResult c = runUtil.runTimedCmd(mTestTimeout, cmd);
if (c.getStatus() == CommandStatus.TIMED_OUT) {
CLog.e("Python process timed out");
CLog.e("Stderr: %s", c.getStderr());
CLog.e("Stdout: %s", c.getStdout());
throw new RuntimeException(
String.format(
"Python unit test timed out after %s",
TimeUtil.formatElapsedTime(mTestTimeout)));
}
// If test execution succeeds, regardless of test results the parser will parse the output.
// If test execution fails, result parser will throw an exception.
CLog.i("Parsing test result: %s", c.getStderr());
MultiLineReceiver parser = new PythonUnitTestResultParser(
ArrayUtil.list(listener), pyModule);
parser.processNewLines(c.getStderr().split("\n"));
}
}