1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tradefed.testtype; 18 19 import com.android.ddmlib.MultiLineReceiver; 20 import com.android.ddmlib.testrunner.ITestRunListener; 21 import com.android.tradefed.build.IBuildInfo; 22 import com.android.tradefed.config.Option; 23 import com.android.tradefed.config.OptionClass; 24 import com.android.tradefed.device.DeviceNotAvailableException; 25 import com.android.tradefed.log.LogUtil.CLog; 26 import com.android.tradefed.result.ITestInvocationListener; 27 import com.android.tradefed.util.ArrayUtil; 28 import com.android.tradefed.util.CommandResult; 29 import com.android.tradefed.util.CommandStatus; 30 import com.android.tradefed.util.IRunUtil; 31 import com.android.tradefed.util.RunUtil; 32 import com.android.tradefed.util.TimeUtil; 33 34 import org.junit.Assert; 35 36 import java.io.File; 37 import java.util.ArrayList; 38 import java.util.List; 39 import java.util.regex.Matcher; 40 import java.util.regex.Pattern; 41 42 /** 43 * Runs Python tests written with the unittest library. 44 */ 45 @OptionClass(alias = "python-unit") 46 public class PythonUnitTestRunner implements IRemoteTest, IBuildReceiver { 47 48 @Option(name = "pythonpath", description = "directories to add to the PYTHONPATH") 49 private List<File> mPathDirs = new ArrayList<>(); 50 51 @Option(name = "pytest", description = "names of python modules containing the test cases") 52 private List<String> mTests = new ArrayList<>(); 53 54 @Option(name = "python-unittest-options", 55 description = "option string to be passed to the unittest module") 56 private String mUnitTestOpts; 57 58 @Option(name = "min-python-version", description = "minimum required python version") 59 private String mMinPyVersion = "2.7.0"; 60 61 @Option(name = "python-binary", description = "python binary to use (optional)") 62 private String mPythonBin; 63 64 @Option( 65 name = "test-timeout", 66 description = "maximum amount of time tests are allowed to run", 67 isTimeVal = true 68 ) 69 private long mTestTimeout = 1000 * 60 * 5; 70 71 private String mPythonPath; 72 private IBuildInfo mBuildInfo; 73 private IRunUtil mRunUtil; 74 75 private static final String PYTHONPATH = "PYTHONPATH"; 76 private static final String VERSION_REGEX = "(?:(\\d+)\\.)?(?:(\\d+)\\.)?(\\*|\\d+)$"; 77 78 /** Returns an {@link IRunUtil} that runs the unittest */ 79 protected IRunUtil getRunUtil() { 80 if (mRunUtil == null) { 81 mRunUtil = new RunUtil(); 82 } 83 return mRunUtil; 84 } 85 86 @Override 87 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 88 setPythonPath(); 89 if (mPythonBin == null) { 90 mPythonBin = getPythonBinary(); 91 } 92 IRunUtil runUtil = getRunUtil(); 93 runUtil.setEnvVariable(PYTHONPATH, mPythonPath); 94 for (String module : mTests) { 95 doRunTest(listener, runUtil, module); 96 } 97 } 98 99 @Override 100 public void setBuild(IBuildInfo buildInfo) { 101 mBuildInfo = buildInfo; 102 } 103 104 /** Returns the {@link IBuildInfo} for this invocation. */ 105 protected IBuildInfo getBuild() { 106 return mBuildInfo; 107 } 108 109 String getMinPythonVersion() { 110 return mMinPyVersion; 111 } 112 113 void setMinPythonVersion(String version) { 114 mMinPyVersion = version; 115 } 116 117 private String getPythonBinary() { 118 IRunUtil runUtil = RunUtil.getDefault(); 119 CommandResult c = runUtil.runTimedCmd(1000, "which", "python"); 120 String pythonBin = c.getStdout().trim(); 121 if (pythonBin.length() == 0) { 122 throw new RuntimeException("Could not find python binary on host machine"); 123 } 124 c = runUtil.runTimedCmd(1000, pythonBin, "--version"); 125 // python --version prints to stderr 126 CLog.i("Found python version: %s", c.getStderr()); 127 checkPythonVersion(c); 128 return pythonBin; 129 } 130 131 private void setPythonPath() { 132 StringBuilder sb = new StringBuilder(); 133 sb.append(System.getenv(PYTHONPATH)); 134 for (File pathdir : mPathDirs) { 135 if (!pathdir.isDirectory()) { 136 CLog.w("Not adding file %s to PYTHONPATH: expecting directory", 137 pathdir.getAbsolutePath()); 138 } 139 sb.append(":"); 140 sb.append(pathdir.getAbsolutePath()); 141 } 142 if (getBuild().getFile(PYTHONPATH) != null) { 143 sb.append(":"); 144 sb.append(getBuild().getFile(PYTHONPATH).getAbsolutePath()); 145 } 146 mPythonPath = sb.toString(); 147 } 148 149 protected void checkPythonVersion(CommandResult c) { 150 Matcher minVersionParts = Pattern.compile(VERSION_REGEX).matcher(mMinPyVersion); 151 Matcher versionParts = Pattern.compile(VERSION_REGEX).matcher(c.getStderr()); 152 153 Assert.assertTrue(minVersionParts.find()); 154 int major = Integer.parseInt(minVersionParts.group(1)); 155 int minor = Integer.parseInt(minVersionParts.group(2)); 156 int revision = Integer.parseInt(minVersionParts.group(3)); 157 158 Assert.assertTrue(versionParts.find()); 159 int foundMajor = Integer.parseInt(versionParts.group(1)); 160 int foundMinor = Integer.parseInt(versionParts.group(2)); 161 int foundRevision = Integer.parseInt(versionParts.group(3)); 162 163 Assert.assertTrue(foundMajor >= major); 164 if (!(foundMajor > major)) { 165 Assert.assertTrue(foundMinor >= minor); 166 if (!(foundMinor > minor)) { 167 Assert.assertTrue(foundRevision >= revision); 168 } 169 } 170 } 171 172 // Exposed for testing purpose. 173 void doRunTest(ITestRunListener listener, IRunUtil runUtil, String pyModule) { 174 String[] baseOpts = {mPythonBin, "-m", "unittest", "-v"}; 175 String[] testModule = {pyModule}; 176 String[] cmd; 177 if (mUnitTestOpts != null) { 178 cmd = ArrayUtil.buildArray(baseOpts, mUnitTestOpts.split(" "), testModule); 179 } else { 180 cmd = ArrayUtil.buildArray(baseOpts, testModule); 181 } 182 CommandResult c = runUtil.runTimedCmd(mTestTimeout, cmd); 183 184 if (c.getStatus() == CommandStatus.TIMED_OUT) { 185 CLog.e("Python process timed out"); 186 CLog.e("Stderr: %s", c.getStderr()); 187 CLog.e("Stdout: %s", c.getStdout()); 188 throw new RuntimeException( 189 String.format( 190 "Python unit test timed out after %s", 191 TimeUtil.formatElapsedTime(mTestTimeout))); 192 } 193 // If test execution succeeds, regardless of test results the parser will parse the output. 194 // If test execution fails, result parser will throw an exception. 195 CLog.i("Parsing test result: %s", c.getStderr()); 196 MultiLineReceiver parser = new PythonUnitTestResultParser( 197 ArrayUtil.list(listener), pyModule); 198 parser.processNewLines(c.getStderr().split("\n")); 199 } 200 201 }