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