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.config.proxy.AutomatedReporters; 26 import com.android.tradefed.device.DeviceNotAvailableException; 27 import com.android.tradefed.error.HarnessRuntimeException; 28 import com.android.tradefed.invoker.DelegatedInvocationExecution; 29 import com.android.tradefed.invoker.IInvocationContext; 30 import com.android.tradefed.invoker.RemoteInvocationExecution; 31 import com.android.tradefed.invoker.TestInformation; 32 import com.android.tradefed.log.LogUtil.CLog; 33 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 34 import com.android.tradefed.result.FailureDescription; 35 import com.android.tradefed.result.FileInputStreamSource; 36 import com.android.tradefed.result.ITestInvocationListener; 37 import com.android.tradefed.result.LogDataType; 38 import com.android.tradefed.result.TestDescription; 39 import com.android.tradefed.result.error.InfraErrorIdentifier; 40 import com.android.tradefed.result.proto.StreamProtoReceiver; 41 import com.android.tradefed.result.proto.StreamProtoResultReporter; 42 import com.android.tradefed.service.TradefedFeatureServer; 43 import com.android.tradefed.util.CommandResult; 44 import com.android.tradefed.util.CommandStatus; 45 import com.android.tradefed.util.FileUtil; 46 import com.android.tradefed.util.IRunUtil; 47 import com.android.tradefed.util.IRunUtil.EnvPriority; 48 import com.android.tradefed.util.RunUtil; 49 import com.android.tradefed.util.StreamUtil; 50 import com.android.tradefed.util.StringEscapeUtils; 51 import com.android.tradefed.util.SubprocessExceptionParser; 52 import com.android.tradefed.util.SubprocessTestResultsParser; 53 import com.android.tradefed.util.SystemUtil; 54 import com.android.tradefed.util.TimeUtil; 55 import com.android.tradefed.util.UniqueMultiMap; 56 57 import org.junit.Assert; 58 59 import java.io.File; 60 import java.io.FileOutputStream; 61 import java.io.IOException; 62 import java.util.ArrayList; 63 import java.util.Arrays; 64 import java.util.HashMap; 65 import java.util.LinkedHashSet; 66 import java.util.List; 67 import java.util.Set; 68 69 /** 70 * A {@link IRemoteTest} for running tests against a separate TF installation. 71 * 72 * <p>Launches an external java process to run the tests. Used for running the TF unit or functional 73 * tests continuously. 74 */ 75 public abstract class SubprocessTfLauncher 76 implements IBuildReceiver, IInvocationContextReceiver, IRemoteTest, IConfigurationReceiver { 77 78 /** The tag that will be passed to the TF subprocess to differentiate it */ 79 public static final String SUBPROCESS_TAG_NAME = "subprocess"; 80 81 public static final String PARENT_PROC_TAG_NAME = "parentprocess"; 82 /** Env. variable that affects adb selection. */ 83 public static final String ANDROID_SERIAL_VAR = "ANDROID_SERIAL"; 84 85 @Option(name = "max-run-time", description = 86 "The maximum time to allow for a TF test run.", isTimeVal = true) 87 private long mMaxTfRunTime = 20 * 60 * 1000; 88 89 @Option(name = "remote-debug", description = 90 "Start the TF java process in remote debug mode.") 91 private boolean mRemoteDebug = false; 92 93 @Option(name = "config-name", description = "The config that runs the TF tests") 94 private String mConfigName; 95 96 @Option( 97 name = "local-sharding-mode", 98 description = 99 "If sharding is requested, allow the launcher to run with local sharding.") 100 private boolean mLocalShardingMode = false; 101 102 @Option(name = "use-event-streaming", description = "Use a socket to receive results as they" 103 + "arrived instead of using a temporary file and parsing at the end.") 104 private boolean mEventStreaming = true; 105 106 @Option( 107 name = "use-proto-reporting", 108 description = "Use a proto result reporter for the results from the subprocess.") 109 private boolean mUseProtoReporting = true; 110 111 @Option(name = "sub-global-config", description = "The global config name to pass to the" 112 + "sub process, can be local or from jar resources. Be careful of conflicts with " 113 + "parent process.") 114 private String mGlobalConfig = null; 115 116 @Option( 117 name = "inject-invocation-data", 118 description = "Pass the invocation-data to the subprocess if enabled.") 119 private boolean mInjectInvocationData = true; 120 121 @Option(name = "ignore-test-log", description = "Only rely on logAssociation for logs.") 122 private boolean mIgnoreTestLog = true; 123 124 @Option( 125 name = "disable-stderr-test", 126 description = "Whether or not to disable the stderr validation check." 127 ) 128 private boolean mDisableStderrTest = false; 129 130 @Option( 131 name = "disable-add-opens", 132 description = "Whether or not to add the java add-opens flags" 133 ) 134 private boolean mDisableJavaOpens = false; 135 136 @Option(name = "add-opens", description = "Whether or not to add the java add-opens flags") 137 private Set<String> mAddOpens = 138 new LinkedHashSet<>( 139 Arrays.asList( 140 "java.base/java.nio", 141 "java.base/sun.reflect.annotation", 142 "java.base/java.io")); 143 144 // Represents all the args to be passed to the sub process 145 @Option(name = "sub-params", description = "Parameters to feed the subprocess.") 146 private List<String> mSubParams = new ArrayList<String>(); 147 148 // Temp global configuration filtered from the parent process. 149 private String mFilteredGlobalConfig = null; 150 151 private static final List<String> TRADEFED_JARS = 152 new ArrayList<>( 153 Arrays.asList( 154 // Loganalysis 155 "loganalysis.jar", 156 "loganalysis-tests.jar", 157 // Aosp Tf jars 158 "tradefed.jar", 159 "tradefed-tests.jar", 160 // AVD util test jar 161 "^tradefed-avd-util-tests.jar", 162 // libs 163 "tools-common-prebuilt.jar", 164 // jar in older branches 165 "tf-prod-tests.jar", 166 "tf-prod-metatests.jar", 167 // Aosp contrib jars 168 "tradefed-contrib.jar", 169 "tf-contrib-tests.jar", 170 // Google Tf jars 171 "google-tf-prod-tests.jar", 172 "google-tf-prod-metatests.jar", 173 "google-tradefed.jar", 174 "google-tradefed-tests.jar", 175 // Google contrib jars 176 "google-tradefed-contrib.jar", 177 // Older jar required for coverage tests 178 "jack-jacoco-reporter.jar", 179 "emmalib.jar")); 180 181 /** Timeout to wait for the events received from subprocess to finish being processed.*/ 182 private static final long EVENT_THREAD_JOIN_TIMEOUT_MS = 30 * 1000; 183 184 protected IRunUtil mRunUtil = new RunUtil(); 185 186 protected IBuildInfo mBuildInfo = null; 187 // Temp directory to run the TF process. 188 protected File mTmpDir = null; 189 // List of command line arguments to run the TF process. 190 protected List<String> mCmdArgs = null; 191 // The absolute path to the build's root directory. 192 protected String mRootDir = null; 193 protected IConfiguration mConfig; 194 private IInvocationContext mContext; 195 196 @Override setInvocationContext(IInvocationContext invocationContext)197 public void setInvocationContext(IInvocationContext invocationContext) { 198 mContext = invocationContext; 199 } 200 201 @Override setConfiguration(IConfiguration configuration)202 public void setConfiguration(IConfiguration configuration) { 203 mConfig = configuration; 204 } 205 setProtoReporting(boolean protoReporting)206 protected void setProtoReporting(boolean protoReporting) { 207 mUseProtoReporting = protoReporting; 208 } 209 210 /** 211 * Set use-event-streaming. 212 * 213 * Exposed for unit testing. 214 */ setEventStreaming(boolean eventStreaming)215 protected void setEventStreaming(boolean eventStreaming) { 216 mEventStreaming = eventStreaming; 217 } 218 219 /** 220 * Set IRunUtil. 221 * 222 * Exposed for unit testing. 223 */ setRunUtil(IRunUtil runUtil)224 protected void setRunUtil(IRunUtil runUtil) { 225 mRunUtil = runUtil; 226 } 227 228 /** Returns the {@link IRunUtil} that will be used for the subprocess command. */ getRunUtil()229 protected IRunUtil getRunUtil() { 230 return mRunUtil; 231 } 232 233 /** 234 * Setup before running the test. 235 */ preRun()236 protected void preRun() { 237 Assert.assertNotNull(mBuildInfo); 238 Assert.assertNotNull(mConfigName); 239 IFolderBuildInfo tfBuild = (IFolderBuildInfo) mBuildInfo; 240 File rootDirFile = tfBuild.getRootDir(); 241 mRootDir = rootDirFile.getAbsolutePath(); 242 String jarClasspath = ""; 243 List<String> paths = new ArrayList<>(); 244 for (String jar : TRADEFED_JARS) { 245 File f = FileUtil.findFile(rootDirFile, jar); 246 if (f != null && f.exists()) { 247 paths.add(f.getAbsolutePath()); 248 } 249 } 250 jarClasspath = String.join(":", paths); 251 252 mCmdArgs = new ArrayList<String>(); 253 mCmdArgs.add(getJava()); 254 255 try { 256 mTmpDir = FileUtil.createTempDir("subprocess-" + tfBuild.getBuildId()); 257 mCmdArgs.add(String.format("-Djava.io.tmpdir=%s", mTmpDir.getAbsolutePath())); 258 } catch (IOException e) { 259 CLog.e(e); 260 throw new RuntimeException(e); 261 } 262 263 addJavaArguments(mCmdArgs); 264 265 if (mRemoteDebug) { 266 mCmdArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=10088"); 267 } 268 // This prevent the illegal reflective access warnings by allowing some packages. 269 if (!mDisableJavaOpens) { 270 for (String modulePackage : mAddOpens) { 271 mCmdArgs.add("--add-opens=" + modulePackage + "=ALL-UNNAMED"); 272 } 273 } 274 mCmdArgs.add("-cp"); 275 276 mCmdArgs.add(jarClasspath); 277 mCmdArgs.add("com.android.tradefed.command.CommandRunner"); 278 mCmdArgs.add(mConfigName); 279 280 Integer shardCount = mConfig.getCommandOptions().getShardCount(); 281 if (mLocalShardingMode && shardCount != null & shardCount > 1) { 282 mCmdArgs.add("--shard-count"); 283 mCmdArgs.add(Integer.toString(shardCount)); 284 } 285 286 if (!mSubParams.isEmpty()) { 287 mCmdArgs.addAll(StringEscapeUtils.paramsToArgs(mSubParams)); 288 } 289 290 // clear the TF_GLOBAL_CONFIG env, so another tradefed will not reuse the global config file 291 mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE); 292 mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE); 293 mRunUtil.unsetEnvVariable(ANDROID_SERIAL_VAR); 294 mRunUtil.unsetEnvVariable(DelegatedInvocationExecution.DELEGATED_MODE_VAR); 295 for (String variable : AutomatedReporters.REPORTER_MAPPING) { 296 mRunUtil.unsetEnvVariable(variable); 297 } 298 // Handle feature server 299 getRunUtil().unsetEnvVariable(RemoteInvocationExecution.START_FEATURE_SERVER); 300 getRunUtil().unsetEnvVariable(TradefedFeatureServer.TF_SERVICE_PORT); 301 getRunUtil().setEnvVariablePriority(EnvPriority.SET); 302 getRunUtil() 303 .setEnvVariable( 304 TradefedFeatureServer.TF_SERVICE_PORT, 305 Integer.toString(TradefedFeatureServer.getPort())); 306 307 if (mGlobalConfig == null) { 308 // If the global configuration is not set in option, create a filtered global 309 // configuration for subprocess to use. 310 try { 311 File filteredGlobalConfig = 312 GlobalConfiguration.getInstance().cloneConfigWithFilter(); 313 mFilteredGlobalConfig = filteredGlobalConfig.getAbsolutePath(); 314 mGlobalConfig = mFilteredGlobalConfig; 315 } catch (IOException e) { 316 CLog.e("Failed to create filtered global configuration"); 317 CLog.e(e); 318 } 319 } 320 if (mGlobalConfig != null) { 321 // We allow overriding this global config and then set it for the subprocess. 322 mRunUtil.setEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE, mGlobalConfig); 323 } 324 } 325 326 /** 327 * Allow to add extra java parameters to the subprocess invocation. 328 * 329 * @param args the current list of arguments to which we need to add the extra ones. 330 */ addJavaArguments(List<String> args)331 protected void addJavaArguments(List<String> args) {} 332 333 /** 334 * Actions to take after the TF test is finished. 335 * 336 * @param listener the original {@link ITestInvocationListener} where to report results. 337 * @param exception True if exception was raised inside the test. 338 * @param elapsedTime the time taken to run the tests. 339 */ postRun(ITestInvocationListener listener, boolean exception, long elapsedTime)340 protected void postRun(ITestInvocationListener listener, boolean exception, long elapsedTime) {} 341 342 /** Pipe to the subprocess the invocation-data so that it can use them if needed. */ addInvocationData()343 private void addInvocationData() { 344 if (!mInjectInvocationData) { 345 return; 346 } 347 UniqueMultiMap<String, String> data = mConfig.getCommandOptions().getInvocationData(); 348 for (String key : data.keySet()) { 349 for (String value : data.get(key)) { 350 mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA); 351 mCmdArgs.add(key); 352 mCmdArgs.add(value); 353 } 354 } 355 // Finally add one last more to tag the subprocess 356 mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA); 357 mCmdArgs.add(SUBPROCESS_TAG_NAME); 358 mCmdArgs.add("true"); 359 // Tag the parent invocation 360 mBuildInfo.addBuildAttribute(PARENT_PROC_TAG_NAME, "true"); 361 } 362 363 /** {@inheritDoc} */ 364 @Override run(TestInformation testInfo, ITestInvocationListener listener)365 public void run(TestInformation testInfo, ITestInvocationListener listener) 366 throws DeviceNotAvailableException { 367 preRun(); 368 addInvocationData(); 369 370 File stdoutFile = null; 371 File stderrFile = null; 372 File eventFile = null; 373 SubprocessTestResultsParser eventParser = null; 374 StreamProtoReceiver protoReceiver = null; 375 FileOutputStream stdout = null; 376 FileOutputStream stderr = null; 377 378 boolean exception = false; 379 long startTime = 0L; 380 long elapsedTime = -1L; 381 try { 382 stdoutFile = FileUtil.createTempFile("stdout_subprocess_", ".log"); 383 stderrFile = FileUtil.createTempFile("stderr_subprocess_", ".log"); 384 stderr = new FileOutputStream(stderrFile); 385 stdout = new FileOutputStream(stdoutFile); 386 if (mUseProtoReporting) { 387 // Skip merging properties to avoid contaminating metrics with unit tests 388 protoReceiver = 389 new StreamProtoReceiver( 390 listener, mContext, false, false, true, "subprocess-", false); 391 mCmdArgs.add("--" + StreamProtoResultReporter.PROTO_REPORT_PORT_OPTION); 392 mCmdArgs.add(Integer.toString(protoReceiver.getSocketServerPort())); 393 } else { 394 eventParser = new SubprocessTestResultsParser(listener, mEventStreaming, mContext); 395 if (mEventStreaming) { 396 mCmdArgs.add("--subprocess-report-port"); 397 mCmdArgs.add(Integer.toString(eventParser.getSocketServerPort())); 398 } else { 399 eventFile = FileUtil.createTempFile("event_subprocess_", ".log"); 400 mCmdArgs.add("--subprocess-report-file"); 401 mCmdArgs.add(eventFile.getAbsolutePath()); 402 } 403 eventParser.setIgnoreTestLog(mIgnoreTestLog); 404 } 405 startTime = System.currentTimeMillis(); 406 CommandResult result = mRunUtil.runTimedCmd(mMaxTfRunTime, stdout, 407 stderr, mCmdArgs.toArray(new String[0])); 408 409 if (eventParser != null) { 410 if (eventParser.getStartTime() != null) { 411 startTime = eventParser.getStartTime(); 412 } 413 elapsedTime = System.currentTimeMillis() - startTime; 414 // We possibly allow for a little more time if the thread is still processing 415 // events. 416 if (!eventParser.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) { 417 elapsedTime = -1L; 418 throw new RuntimeException( 419 String.format( 420 "Event receiver thread did not complete:" + "\n%s", 421 FileUtil.readStringFromFile(stderrFile))); 422 } 423 } else if (protoReceiver != null) { 424 if (!protoReceiver.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) { 425 elapsedTime = -1L; 426 throw new RuntimeException( 427 String.format( 428 "Event receiver thread did not complete:" + "\n%s", 429 FileUtil.readStringFromFile(stderrFile))); 430 } 431 protoReceiver.completeModuleEvents(); 432 } 433 if (result.getStatus().equals(CommandStatus.SUCCESS)) { 434 CLog.d("Successfully ran TF tests for build %s", mBuildInfo.getBuildId()); 435 testCleanStdErr(stderrFile, listener); 436 } else { 437 CLog.w("Failed ran TF tests for build %s, status %s", 438 mBuildInfo.getBuildId(), result.getStatus()); 439 CLog.v( 440 "TF tests output:\nstdout:\n%s\nstderr:\n%s", 441 result.getStdout(), result.getStderr()); 442 exception = true; 443 String errMessage = null; 444 if (result.getStatus().equals(CommandStatus.TIMED_OUT)) { 445 errMessage = String.format("Timeout after %s", 446 TimeUtil.formatElapsedTime(mMaxTfRunTime)); 447 throw new HarnessRuntimeException( 448 String.format( 449 "%s Tests subprocess failed due to:\n%s\n", 450 mConfigName, errMessage), 451 InfraErrorIdentifier.INVOCATION_TIMEOUT); 452 } else if (eventParser != null && !eventParser.reportedInvocationFailed()) { 453 SubprocessExceptionParser.handleStderrException(result); 454 } 455 } 456 } catch (IOException e) { 457 exception = true; 458 throw new RuntimeException(e); 459 } finally { 460 StreamUtil.close(stdout); 461 StreamUtil.close(stderr); 462 logAndCleanFile(stdoutFile, listener); 463 logAndCleanFile(stderrFile, listener); 464 if (eventFile != null) { 465 eventParser.parseFile(eventFile); 466 logAndCleanFile(eventFile, listener); 467 } 468 StreamUtil.close(eventParser); 469 StreamUtil.close(protoReceiver); 470 471 if (mGlobalConfig != null && new File(mGlobalConfig).exists()) { 472 logAndCleanFile(new File(mGlobalConfig), listener); 473 } 474 475 postRun(listener, exception, elapsedTime); 476 477 if (mTmpDir != null) { 478 FileUtil.recursiveDelete(mTmpDir); 479 } 480 481 if (mFilteredGlobalConfig != null) { 482 FileUtil.deleteFile(new File(mFilteredGlobalConfig)); 483 } 484 } 485 } 486 487 /** 488 * Log the content of given file to listener, then remove the file. 489 * 490 * @param fileToExport the {@link File} pointing to the file to log. 491 * @param listener the {@link ITestInvocationListener} where to report the test. 492 */ logAndCleanFile(File fileToExport, ITestInvocationListener listener)493 private void logAndCleanFile(File fileToExport, ITestInvocationListener listener) { 494 if (fileToExport == null) { 495 return; 496 } 497 498 try (FileInputStreamSource inputStream = new FileInputStreamSource(fileToExport, true)) { 499 listener.testLog(fileToExport.getName(), LogDataType.TEXT, inputStream); 500 } catch (RuntimeException e) { 501 CLog.e(e); 502 } 503 } 504 505 /** 506 * {@inheritDoc} 507 */ 508 @Override setBuild(IBuildInfo buildInfo)509 public void setBuild(IBuildInfo buildInfo) { 510 mBuildInfo = buildInfo; 511 } 512 513 /** 514 * Extra test to ensure no abnormal logging is made to stderr when all the tests pass. 515 * 516 * @param stdErrFile the stderr log file of the subprocess. 517 * @param listener the {@link ITestInvocationListener} where to report the test. 518 */ testCleanStdErr(File stdErrFile, ITestInvocationListener listener)519 private void testCleanStdErr(File stdErrFile, ITestInvocationListener listener) 520 throws IOException { 521 if (mDisableStderrTest) { 522 return; 523 } 524 listener.testRunStarted("StdErr", 1); 525 TestDescription tid = new TestDescription("stderr-test", "checkIsEmpty"); 526 listener.testStarted(tid); 527 if (!FileUtil.readStringFromFile(stdErrFile).isEmpty()) { 528 String trace = 529 String.format( 530 "Found some output in stderr:\n%s", 531 FileUtil.readStringFromFile(stdErrFile)); 532 listener.testFailed(tid, FailureDescription.create(trace)); 533 } 534 listener.testEnded(tid, new HashMap<String, Metric>()); 535 listener.testRunEnded(0, new HashMap<String, Metric>()); 536 } 537 getJava()538 protected String getJava() { 539 return SystemUtil.getRunningJavaBinaryPath().getAbsolutePath(); 540 } 541 } 542