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 package com.android.tradefed.testtype;
     17 
     18 import com.android.ddmlib.testrunner.TestIdentifier;
     19 import com.android.tradefed.build.IBuildInfo;
     20 import com.android.tradefed.build.IFolderBuildInfo;
     21 import com.android.tradefed.config.GlobalConfiguration;
     22 import com.android.tradefed.config.IConfiguration;
     23 import com.android.tradefed.config.IConfigurationReceiver;
     24 import com.android.tradefed.config.Option;
     25 import com.android.tradefed.invoker.IInvocationContext;
     26 import com.android.tradefed.log.LogUtil.CLog;
     27 import com.android.tradefed.result.FileInputStreamSource;
     28 import com.android.tradefed.result.ITestInvocationListener;
     29 import com.android.tradefed.result.LogDataType;
     30 import com.android.tradefed.util.CommandResult;
     31 import com.android.tradefed.util.CommandStatus;
     32 import com.android.tradefed.util.FileUtil;
     33 import com.android.tradefed.util.IRunUtil;
     34 import com.android.tradefed.util.IRunUtil.EnvPriority;
     35 import com.android.tradefed.util.RunUtil;
     36 import com.android.tradefed.util.StreamUtil;
     37 import com.android.tradefed.util.SubprocessTestResultsParser;
     38 import com.android.tradefed.util.TimeUtil;
     39 import com.android.tradefed.util.UniqueMultiMap;
     40 
     41 import org.junit.Assert;
     42 
     43 import java.io.File;
     44 import java.io.FileOutputStream;
     45 import java.io.IOException;
     46 import java.util.ArrayList;
     47 import java.util.Collections;
     48 import java.util.List;
     49 
     50 /**
     51  * A {@link IRemoteTest} for running tests against a separate TF installation.
     52  *
     53  * <p>Launches an external java process to run the tests. Used for running the TF unit or functional
     54  * tests continuously.
     55  */
     56 public abstract class SubprocessTfLauncher
     57         implements IBuildReceiver, IInvocationContextReceiver, IRemoteTest, IConfigurationReceiver {
     58 
     59     @Option(name = "max-run-time", description =
     60             "The maximum time to allow for a TF test run.", isTimeVal = true)
     61     private long mMaxTfRunTime = 20 * 60 * 1000;
     62 
     63     @Option(name = "remote-debug", description =
     64             "Start the TF java process in remote debug mode.")
     65     private boolean mRemoteDebug = false;
     66 
     67     @Option(name = "config-name", description = "The config that runs the TF tests")
     68     private String mConfigName;
     69 
     70     @Option(name = "use-event-streaming", description = "Use a socket to receive results as they"
     71             + "arrived instead of using a temporary file and parsing at the end.")
     72     private boolean mEventStreaming = true;
     73 
     74     @Option(name = "sub-global-config", description = "The global config name to pass to the"
     75             + "sub process, can be local or from jar resources. Be careful of conflicts with "
     76             + "parent process.")
     77     private String mGlobalConfig = null;
     78 
     79     @Option(
     80         name = "inject-invocation-data",
     81         description = "Pass the invocation-data to the subprocess if enabled."
     82     )
     83     private boolean mInjectInvocationData = false;
     84 
     85     // Temp global configuration filtered from the parent process.
     86     private String mFilteredGlobalConfig = null;
     87 
     88     /** Timeout to wait for the events received from subprocess to finish being processed.*/
     89     private static final long EVENT_THREAD_JOIN_TIMEOUT_MS = 30 * 1000;
     90 
     91     protected static final String TF_GLOBAL_CONFIG = "TF_GLOBAL_CONFIG";
     92 
     93     protected IRunUtil mRunUtil =  new RunUtil();
     94 
     95     protected IBuildInfo mBuildInfo = null;
     96     // Temp directory to run the TF process.
     97     protected File mTmpDir = null;
     98     // List of command line arguments to run the TF process.
     99     protected List<String> mCmdArgs = null;
    100     // The absolute path to the build's root directory.
    101     protected String mRootDir = null;
    102     private IInvocationContext mContext;
    103     private IConfiguration mConfig;
    104 
    105     @Override
    106     public void setInvocationContext(IInvocationContext invocationContext) {
    107         mContext = invocationContext;
    108     }
    109 
    110     @Override
    111     public void setConfiguration(IConfiguration configuration) {
    112         mConfig = configuration;
    113     }
    114 
    115     /**
    116      * Set use-event-streaming.
    117      *
    118      * Exposed for unit testing.
    119      */
    120     protected void setEventStreaming(boolean eventStreaming) {
    121         mEventStreaming = eventStreaming;
    122     }
    123 
    124     /**
    125      * Set IRunUtil.
    126      *
    127      * Exposed for unit testing.
    128      */
    129     protected void setRunUtil(IRunUtil runUtil) {
    130         mRunUtil = runUtil;
    131     }
    132 
    133     /**
    134      * Setup before running the test.
    135      */
    136     protected void preRun() {
    137         Assert.assertNotNull(mBuildInfo);
    138         Assert.assertNotNull(mConfigName);
    139         IFolderBuildInfo tfBuild = (IFolderBuildInfo)mBuildInfo;
    140         mRootDir = tfBuild.getRootDir().getAbsolutePath();
    141         String jarClasspath = FileUtil.getPath(mRootDir, "*");
    142 
    143         mCmdArgs = new ArrayList<String>();
    144         mCmdArgs.add("java");
    145 
    146         try {
    147             mTmpDir = FileUtil.createTempDir("subprocess-" + tfBuild.getBuildId());
    148             mCmdArgs.add(String.format("-Djava.io.tmpdir=%s", mTmpDir.getAbsolutePath()));
    149         } catch (IOException e) {
    150             CLog.e(e);
    151             throw new RuntimeException(e);
    152         }
    153 
    154         addJavaArguments(mCmdArgs);
    155 
    156         if (mRemoteDebug) {
    157             mCmdArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=10088");
    158         }
    159         mCmdArgs.add("-cp");
    160 
    161         mCmdArgs.add(jarClasspath);
    162         mCmdArgs.add("com.android.tradefed.command.CommandRunner");
    163         mCmdArgs.add(mConfigName);
    164 
    165         // clear the TF_GLOBAL_CONFIG env, so another tradefed will not reuse the global config file
    166         mRunUtil.unsetEnvVariable(TF_GLOBAL_CONFIG);
    167         if (mGlobalConfig == null) {
    168             // If the global configuration is not set in option, create a filtered global
    169             // configuration for subprocess to use.
    170             try {
    171                 File filteredGlobalConfig =
    172                         FileUtil.createTempFile("filtered_global_config", ".config");
    173                 GlobalConfiguration.getInstance().cloneConfigWithFilter(filteredGlobalConfig, null);
    174                 mFilteredGlobalConfig = filteredGlobalConfig.getAbsolutePath();
    175                 mGlobalConfig = mFilteredGlobalConfig;
    176             } catch (IOException e) {
    177                 CLog.e("Failed to create filtered global configuration");
    178                 CLog.e(e);
    179             }
    180         }
    181         if (mGlobalConfig != null) {
    182             // We allow overriding this global config and then set it for the subprocess.
    183             mRunUtil.setEnvVariablePriority(EnvPriority.SET);
    184             mRunUtil.setEnvVariable(TF_GLOBAL_CONFIG, mGlobalConfig);
    185         }
    186     }
    187 
    188     /**
    189      * Allow to add extra java parameters to the subprocess invocation.
    190      *
    191      * @param args the current list of arguments to which we need to add the extra ones.
    192      */
    193     protected void addJavaArguments(List<String> args) {}
    194 
    195     /**
    196      * Actions to take after the TF test is finished.
    197      *
    198      * @param listener the original {@link ITestInvocationListener} where to report results.
    199      * @param exception True if exception was raised inside the test.
    200      * @param elapsedTime the time taken to run the tests.
    201      */
    202     protected void postRun(ITestInvocationListener listener, boolean exception, long elapsedTime) {}
    203 
    204     /** Pipe to the subprocess the invocation-data so that it can use them if needed. */
    205     private void addInvocationData() {
    206         if (!mInjectInvocationData) {
    207             return;
    208         }
    209         UniqueMultiMap<String, String> data = mConfig.getCommandOptions().getInvocationData();
    210         for (String key : data.keySet()) {
    211             for (String value : data.get(key)) {
    212                 mCmdArgs.add("--invocation-data");
    213                 mCmdArgs.add(key);
    214                 mCmdArgs.add(value);
    215             }
    216         }
    217     }
    218 
    219     /** {@inheritDoc} */
    220     @Override
    221     public void run(ITestInvocationListener listener) {
    222         preRun();
    223         addInvocationData();
    224 
    225         File stdoutFile = null;
    226         File stderrFile = null;
    227         File eventFile = null;
    228         SubprocessTestResultsParser eventParser = null;
    229         FileOutputStream stdout = null;
    230         FileOutputStream stderr = null;
    231 
    232         boolean exception = false;
    233         long startTime = 0l;
    234         long elapsedTime = -1l;
    235         try {
    236             stdoutFile = FileUtil.createTempFile("stdout_subprocess_", ".log");
    237             stderrFile = FileUtil.createTempFile("stderr_subprocess_", ".log");
    238             stderr = new FileOutputStream(stderrFile);
    239             stdout = new FileOutputStream(stdoutFile);
    240 
    241             eventParser = new SubprocessTestResultsParser(listener, mEventStreaming, mContext);
    242             if (mEventStreaming) {
    243                 mCmdArgs.add("--subprocess-report-port");
    244                 mCmdArgs.add(Integer.toString(eventParser.getSocketServerPort()));
    245             } else {
    246                 eventFile = FileUtil.createTempFile("event_subprocess_", ".log");
    247                 mCmdArgs.add("--subprocess-report-file");
    248                 mCmdArgs.add(eventFile.getAbsolutePath());
    249             }
    250             startTime = System.currentTimeMillis();
    251             CommandResult result = mRunUtil.runTimedCmd(mMaxTfRunTime, stdout,
    252                     stderr, mCmdArgs.toArray(new String[0]));
    253             if (eventParser.getStartTime() != null) {
    254                 startTime = eventParser.getStartTime();
    255             }
    256             elapsedTime = System.currentTimeMillis() - startTime;
    257             // We possibly allow for a little more time if the thread is still processing events.
    258             if (!eventParser.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) {
    259                 elapsedTime = -1l;
    260                 throw new RuntimeException(String.format("Event receiver thread did not complete:"
    261                         + "\n%s", FileUtil.readStringFromFile(stderrFile)));
    262             }
    263             if (result.getStatus().equals(CommandStatus.SUCCESS)) {
    264                 CLog.d("Successfully ran TF tests for build %s", mBuildInfo.getBuildId());
    265                 testCleanStdErr(stderrFile, listener);
    266             } else {
    267                 CLog.w("Failed ran TF tests for build %s, status %s",
    268                         mBuildInfo.getBuildId(), result.getStatus());
    269                 CLog.v("TF tests output:\nstdout:\n%s\nstderror:\n%s",
    270                         result.getStdout(), result.getStderr());
    271                 exception = true;
    272                 String errMessage = null;
    273                 if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
    274                     errMessage = String.format("Timeout after %s",
    275                             TimeUtil.formatElapsedTime(mMaxTfRunTime));
    276                 } else {
    277                     errMessage = FileUtil.readStringFromFile(stderrFile);
    278                 }
    279                 throw new RuntimeException(
    280                         String.format("%s Tests subprocess failed due to:\n%s\n", mConfigName,
    281                                 errMessage));
    282             }
    283         } catch (IOException e) {
    284             exception = true;
    285             throw new RuntimeException(e);
    286         } finally {
    287             StreamUtil.close(stdout);
    288             StreamUtil.close(stderr);
    289             logAndCleanFile(stdoutFile, listener);
    290             logAndCleanFile(stderrFile, listener);
    291             if (eventFile != null) {
    292                 eventParser.parseFile(eventFile);
    293                 logAndCleanFile(eventFile, listener);
    294             }
    295             StreamUtil.close(eventParser);
    296 
    297             postRun(listener, exception, elapsedTime);
    298 
    299             if (mTmpDir != null) {
    300                 FileUtil.recursiveDelete(mTmpDir);
    301             }
    302 
    303             if (mFilteredGlobalConfig != null) {
    304                 FileUtil.deleteFile(new File(mFilteredGlobalConfig));
    305             }
    306         }
    307     }
    308 
    309     /**
    310      * Log the content of given file to listener, then remove the file.
    311      *
    312      * @param fileToExport the {@link File} pointing to the file to log.
    313      * @param listener the {@link ITestInvocationListener} where to report the test.
    314      */
    315     private void logAndCleanFile(File fileToExport, ITestInvocationListener listener) {
    316         if (fileToExport == null)
    317             return;
    318 
    319         FileInputStreamSource inputStream = new FileInputStreamSource(fileToExport);
    320         listener.testLog(fileToExport.getName(), LogDataType.TEXT, inputStream);
    321         inputStream.cancel();
    322         FileUtil.deleteFile(fileToExport);
    323     }
    324 
    325     /**
    326      * {@inheritDoc}
    327      */
    328     @Override
    329     public void setBuild(IBuildInfo buildInfo) {
    330         mBuildInfo = buildInfo;
    331     }
    332 
    333     /**
    334      * Extra test to ensure no abnormal logging is made to stderr when all the tests pass.
    335      *
    336      * @param stdErrFile the stderr log file of the subprocess.
    337      * @param listener the {@link ITestInvocationListener} where to report the test.
    338      */
    339     private void testCleanStdErr(File stdErrFile, ITestInvocationListener listener)
    340             throws IOException {
    341         listener.testRunStarted("StdErr", 1);
    342         TestIdentifier tid = new TestIdentifier("stderr-test", "checkIsEmpty");
    343         listener.testStarted(tid);
    344         if (!FileUtil.readStringFromFile(stdErrFile).isEmpty()) {
    345             String trace =
    346                     String.format(
    347                             "Found some output in stderr:\n%s",
    348                             FileUtil.readStringFromFile(stdErrFile));
    349             listener.testFailed(tid, trace);
    350         }
    351         listener.testEnded(tid, Collections.emptyMap());
    352         listener.testRunEnded(0, Collections.emptyMap());
    353     }
    354 }
    355