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