Home | History | Annotate | Download | only in testtype
      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 }