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