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